vector_config_macros/
configurable_component.rs

1use 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    /// Creates a new `TypedComponent` from the given path.
25    ///
26    /// If the path does not matches a known component type, `None` is returned. Otherwise,
27    /// `Some(...)` is returned with a valid `TypedComponent`.
28    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    /// Creates a new `TypedComponent` from the given meta list.
40    ///
41    /// If the meta list does not have a path that matches a known component type, `None` is
42    /// returned. Otherwise, `Some(...)` is returned with a valid `TypedComponent`.
43    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    /// Gets the component name, if one was specified.
69    fn get_component_name(&self) -> Option<String> {
70        self.component_name.as_ref().map(|s| s.value())
71    }
72
73    /// Creates the component description registration code based on the original derive input.
74    ///
75    /// If this typed component does not have a name, `None` will be returned, as only named
76    /// components can be described.
77    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            // Derive the human-friendly name from the component name.
109            let label = generate_human_friendly_string(&component_name.value());
110
111            // Derive the logical name from the config type, with the trailing "Config" dropped.
112            let logical_name = config_ty.to_string();
113            let logical_name = logical_name.strip_suffix("Config").unwrap_or(&logical_name);
114
115            // TODO: Make this an `expect` once all component types have been converted.
116            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    /// Creates the component name registration code.
136    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    /// Component type details, if specified.
154    ///
155    /// While the macro `#[configurable_component]` sort of belies an implication that any item
156    /// being annotated is a component, we make a distinction here in terms of what can be a
157    /// component in a Vector topology, versus simply what is allowed as a configurable "component"
158    /// within a Vector configuration.
159    typed_component: Option<TypedComponent>,
160
161    /// Whether to disable the automatic derive for `serde::Serialize`.
162    no_ser: bool,
163
164    /// Whether to disable the automatic derive for `serde::Deserialize`.
165    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                // Disable automatically deriving `serde::Serialize`.
179                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                // Disable automatically deriving `serde::Deserialize`.
188                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                // Marked as a typed component that requires a name.
197                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                // Marked as a typed component that requires a name, but it was not specified.
213                //
214                // When marked as a typed component, but no name is specified, we still want to
215                // generate our normal derive output, as we let the `NamedComponent` derive handle
216                // emitting an error to tell the user that the component type requires a name,
217                //
218                // We don't emit those errors here because errors in attribute macros will cause a
219                // cascading set of errors that are too noisy.
220                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    // If the component is typed (see `TypedComponent`/`ComponentType`), we do a few additional
281    // things:
282    // - we add a metadata attribute to indicate the component type
283    // - we potentially add an attribute so the component's configuration type becomes "named",
284    //   which drives the component config trait impl (i.e. `SourceConfig`) and will eventually
285    //   drive the value that `serde` uses to deserialize the given component variant in the Big
286    //   Enum model. this only happens if the component is actually named, and only sources are
287    //   named at the moment.
288    // - we automatically generate the call to register the component config type via `inventory`
289    //   which powers the `vector generate` subcommand by maintaining a name -> config type map
290    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    // Generate and apply all of the necessary derives.
314    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    // Final assembly.
332    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
343/// Gets the ident of the component type-specific helper attribute for the `NamedComponent` derive.
344///
345/// When we emit code for a configurable item that has been marked as a typed component, we
346/// optionally emit the code to generate an implementation of `NamedComponent` if that component
347/// is supposed to be named.
348///
349/// This function returns the appropriate ident for the helper attribute specific to the
350/// component, as we must pass the component type being named -- source vs transform, etc --
351/// down to the derive for `NamedComponent`. This allows it to emit error messages that _look_
352/// like they're coming from `configurable_component`, even though they're coming from the
353/// derive for `NamedComponent`.
354fn 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}