vector_config_macros/
configurable_component.rs1use darling::{ast::NestedMeta, Error, FromMeta};
2use proc_macro::TokenStream;
3use proc_macro2::{Ident, Span};
4use quote::{quote, quote_spanned};
5use syn::{
6 parse_macro_input, parse_quote, parse_quote_spanned, punctuated::Punctuated, spanned::Spanned,
7 token::Comma, DeriveInput, Lit, LitStr, Meta, MetaList, Path,
8};
9use vector_config_common::{
10 constants::ComponentType, human_friendly::generate_human_friendly_string,
11};
12
13use crate::attrs;
14
15#[derive(Clone, Debug)]
16struct TypedComponent {
17 span: Span,
18 component_type: ComponentType,
19 component_name: Option<LitStr>,
20 description: Option<LitStr>,
21}
22
23impl TypedComponent {
24 fn from_path(path: &Path) -> Option<Self> {
29 ComponentType::try_from(path)
30 .ok()
31 .map(|component_type| Self {
32 span: path.span(),
33 component_type,
34 component_name: None,
35 description: None,
36 })
37 }
38
39 fn from_meta_list(ml: &MetaList) -> Option<Self> {
44 let mut items = ml
45 .parse_args_with(Punctuated::<NestedMeta, Comma>::parse_terminated)
46 .unwrap_or_default()
47 .into_iter();
48 ComponentType::try_from(&ml.path)
49 .ok()
50 .map(|component_type| {
51 let component_name = match items.next() {
52 Some(NestedMeta::Lit(Lit::Str(component_name))) => Some(component_name),
53 _ => None,
54 };
55 let description = match items.next() {
56 Some(NestedMeta::Lit(Lit::Str(description))) => Some(description),
57 _ => None,
58 };
59 Self {
60 span: ml.span(),
61 component_type,
62 component_name,
63 description,
64 }
65 })
66 }
67
68 fn get_component_name(&self) -> Option<String> {
70 self.component_name.as_ref().map(|s| s.value())
71 }
72
73 fn get_component_desc_registration(
78 &self,
79 input: &DeriveInput,
80 ) -> Option<proc_macro2::TokenStream> {
81 self.component_name.as_ref().map(|component_name| {
82 let config_ty = &input.ident;
83 let desc_ty: syn::Type = match self.component_type {
84 ComponentType::Api => {
85 parse_quote! { ::vector_config::component::ApiDescription }
86 }
87 ComponentType::EnrichmentTable => {
88 parse_quote! { ::vector_config::component::EnrichmentTableDescription }
89 }
90 ComponentType::GlobalOption => {
91 parse_quote! { ::vector_config::component::GlobalOptionDescription }
92 }
93 ComponentType::Provider => {
94 parse_quote! { ::vector_config::component::ProviderDescription }
95 }
96 ComponentType::Secrets => {
97 parse_quote! { ::vector_config::component::SecretsDescription }
98 }
99 ComponentType::Sink => parse_quote! { ::vector_config::component::SinkDescription },
100 ComponentType::Source => {
101 parse_quote! { ::vector_config::component::SourceDescription }
102 }
103 ComponentType::Transform => {
104 parse_quote! { ::vector_config::component::TransformDescription }
105 }
106 };
107
108 let label = generate_human_friendly_string(&component_name.value());
110
111 let logical_name = config_ty.to_string();
113 let logical_name = logical_name.strip_suffix("Config").unwrap_or(&logical_name);
114
115 let description = self
117 .description
118 .as_ref()
119 .map(LitStr::value)
120 .unwrap_or_else(|| "This component is missing a description.".into());
121
122 quote! {
123 ::inventory::submit! {
124 #desc_ty::new::<#config_ty>(
125 #component_name,
126 #label,
127 #logical_name,
128 #description,
129 )
130 }
131 }
132 })
133 }
134
135 fn get_component_name_registration(&self) -> proc_macro2::TokenStream {
137 let helper_attr = get_named_component_helper_ident(self.component_type);
138 match self.component_name.as_ref() {
139 None => quote_spanned! {self.span=>
140 #[derive(::vector_config::NamedComponent)]
141 #[#helper_attr]
142 },
143 Some(component_name) => quote_spanned! {self.span=>
144 #[derive(::vector_config::NamedComponent)]
145 #[#helper_attr(#component_name)]
146 },
147 }
148 }
149}
150
151#[derive(Debug)]
152struct Options {
153 typed_component: Option<TypedComponent>,
160
161 no_ser: bool,
163
164 no_deser: bool,
166}
167
168impl FromMeta for Options {
169 fn from_list(items: &[NestedMeta]) -> darling::Result<Self> {
170 let mut typed_component = None;
171 let mut no_ser = false;
172 let mut no_deser = false;
173
174 let mut errors = Error::accumulator();
175
176 for nm in items {
177 match nm {
178 NestedMeta::Meta(Meta::Path(p)) if p == attrs::NO_SER => {
180 if no_ser {
181 errors.push(Error::duplicate_field_path(p));
182 } else {
183 no_ser = true;
184 }
185 }
186
187 NestedMeta::Meta(Meta::Path(p)) if p == attrs::NO_DESER => {
189 if no_deser {
190 errors.push(Error::duplicate_field_path(p));
191 } else {
192 no_deser = true;
193 }
194 }
195
196 NestedMeta::Meta(Meta::List(ml)) if ComponentType::is_valid_type(&ml.path) => {
198 if typed_component.is_some() {
199 errors.push(
200 Error::custom("already marked as a typed component").with_span(ml),
201 );
202 } else {
203 let result = TypedComponent::from_meta_list(ml);
204 if result.is_none() {
205 return Err(Error::custom("meta list matched named component type, but failed to parse into TypedComponent").with_span(&ml));
206 }
207
208 typed_component = result;
209 }
210 }
211
212 NestedMeta::Meta(Meta::Path(p)) if ComponentType::is_valid_type(p) => {
221 if typed_component.is_some() {
222 errors.push(
223 Error::custom("already marked as a typed component").with_span(p),
224 );
225 } else {
226 let result = TypedComponent::from_path(p);
227 if result.is_none() {
228 return Err(Error::custom("path matched component type, but failed to parse into TypedComponent").with_span(p));
229 }
230
231 typed_component = result;
232 }
233 }
234
235 NestedMeta::Meta(m) => {
236 let error = "expected one of: `enrichment_table(\"...\")`, `provider(\"...\")`, `source(\"...\")`, `transform(\"...\")`, `secrets(\"...\")`, `sink(\"...\")`, `no_ser`, or `no_deser`";
237 errors.push(Error::custom(error).with_span(m));
238 }
239
240 NestedMeta::Lit(lit) => errors.push(Error::unexpected_lit_type(lit)),
241 }
242 }
243
244 errors.finish().map(|()| Self {
245 typed_component,
246 no_ser,
247 no_deser,
248 })
249 }
250}
251
252impl Options {
253 fn typed_component(&self) -> Option<TypedComponent> {
254 self.typed_component.clone()
255 }
256
257 fn skip_derive_ser(&self) -> bool {
258 self.no_ser
259 }
260
261 fn skip_derive_deser(&self) -> bool {
262 self.no_deser
263 }
264}
265
266pub fn configurable_component_impl(args: TokenStream, item: TokenStream) -> TokenStream {
267 let args: Vec<NestedMeta> =
268 parse_macro_input!(args with Punctuated::<NestedMeta, Comma>::parse_terminated)
269 .into_iter()
270 .collect();
271 let input = parse_macro_input!(item as DeriveInput);
272
273 let options = match Options::from_list(&args) {
274 Ok(v) => v,
275 Err(e) => {
276 return TokenStream::from(e.write_errors());
277 }
278 };
279
280 let component_type = options.typed_component().map(|tc| {
291 let component_type = tc.component_type.as_str();
292 quote! {
293 #[configurable(metadata(docs::component_type = #component_type))]
294 }
295 });
296
297 let maybe_component_name = options.typed_component().map(|tc| {
298 let maybe_component_name_registration = tc.get_component_name_registration();
299 let maybe_component_name_metadata = tc
300 .get_component_name()
301 .map(|name| quote! { #[configurable(metadata(docs::component_name = #name))] });
302
303 quote! {
304 #maybe_component_name_metadata
305 #maybe_component_name_registration
306 }
307 });
308
309 let maybe_component_desc = options
310 .typed_component()
311 .map(|tc| tc.get_component_desc_registration(&input));
312
313 let mut derives = Punctuated::<Path, Comma>::new();
315 derives.push(parse_quote_spanned! {input.ident.span()=>
316 ::vector_config::Configurable
317 });
318
319 if !options.skip_derive_ser() {
320 derives.push(parse_quote_spanned! {input.ident.span()=>
321 ::serde::Serialize
322 });
323 }
324
325 if !options.skip_derive_deser() {
326 derives.push(parse_quote_spanned! {input.ident.span()=>
327 ::serde::Deserialize
328 });
329 }
330
331 let derived = quote! {
333 #[derive(#derives)]
334 #component_type
335 #maybe_component_name
336 #input
337 #maybe_component_desc
338 };
339
340 derived.into()
341}
342
343fn get_named_component_helper_ident(component_type: ComponentType) -> Ident {
355 let attr = match component_type {
356 ComponentType::Api => attrs::API_COMPONENT,
357 ComponentType::EnrichmentTable => attrs::ENRICHMENT_TABLE_COMPONENT,
358 ComponentType::GlobalOption => attrs::GLOBAL_OPTION_COMPONENT,
359 ComponentType::Provider => attrs::PROVIDER_COMPONENT,
360 ComponentType::Secrets => attrs::SECRETS_COMPONENT,
361 ComponentType::Sink => attrs::SINK_COMPONENT,
362 ComponentType::Source => attrs::SOURCE_COMPONENT,
363 ComponentType::Transform => attrs::TRANSFORM_COMPONENT,
364 };
365
366 attr.as_ident(Span::call_site())
367}