vector_config_macros/
configurable.rs

1use proc_macro::TokenStream;
2use proc_macro2::Span;
3use quote::{quote, quote_spanned};
4use syn::{
5    parse_macro_input, parse_quote, spanned::Spanned, token::PathSep, DeriveInput, ExprPath, Ident,
6    PathArguments, Type,
7};
8use vector_config_common::validation::Validation;
9
10use crate::ast::{Container, Data, Field, LazyCustomAttribute, Style, Tagging, Variant};
11
12pub fn derive_configurable_impl(input: TokenStream) -> TokenStream {
13    // Parse our input token stream as a derive input, and process the container, and the
14    // container's children, that the macro is applied to.
15    let input = parse_macro_input!(input as DeriveInput);
16    let container = match Container::from_derive_input(&input) {
17        Ok(container) => container,
18        Err(e) => {
19            // This should only occur when used on a union, as that's the only time `serde` will get
20            // angry enough to not parse the derive AST at all, so we just return the context errors
21            // we have, which will say as much, because also, if it gave us `None`, it should have
22            // registered an error in the context as well.
23            return e.write_errors().into();
24        }
25    };
26
27    let mut generics = container.generics().clone();
28
29    // We need to construct an updated where clause that properly constrains any generic types which are used as fields
30    // on the container. We _only_ care about fields that are pure generic types, because anything that's a concrete
31    // type -- Foo<T> -- will be checked when the schema is generated, but we want generic types to be able to be
32    // resolved for compatibility at the point of usage, not the point of definition.
33    let generic_field_types = container.generic_field_types();
34    if !generic_field_types.is_empty() {
35        let where_clause = generics.make_where_clause();
36        for typ in generic_field_types {
37            let ty = &typ.ident;
38            let predicate = parse_quote! { #ty: ::vector_config::Configurable + ::serde::Serialize + ::vector_config::ToValue };
39
40            where_clause.predicates.push(predicate);
41        }
42    }
43
44    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
45
46    // Now we can go ahead and actually generate the method bodies for our `Configurable` impl,
47    // which are varied based on whether we have a struct or enum container.
48    let metadata_fn = build_metadata_fn(&container);
49    let generate_schema_fn = match container.virtual_newtype() {
50        Some(virtual_ty) => build_virtual_newtype_schema_fn(virtual_ty),
51        None => match container.data() {
52            Data::Struct(style, fields) => {
53                build_struct_generate_schema_fn(&container, style, fields)
54            }
55            Data::Enum(variants) => build_enum_generate_schema_fn(&container, variants),
56        },
57    };
58
59    let to_value_fn = build_to_value_fn(&container);
60
61    let name = container.ident();
62    let ref_name = container.name();
63    let configurable_impl = quote! {
64        const _: () = {
65            #[automatically_derived]
66            #[allow(unused_qualifications)]
67            impl #impl_generics ::vector_config::Configurable for #name #ty_generics #where_clause {
68                fn referenceable_name() -> Option<&'static str> {
69                    // If the type name we get back from `std::any::type_name` doesn't start with
70                    // the module path, use a concatenated version.
71                    //
72                    // We do this because `std::any::type_name` states it may or may not return a
73                    // fully-qualified type path, as that behavior is not stabilized, so we want to
74                    // avoid using non-fully-qualified paths since we might encounter collisions
75                    // with schema reference names otherwise.
76                    //
77                    // The reason we don't _only_ use the manually-concatenated version is because
78                    // it's a little difficult to get it to emit a clean name, as we can't emit
79                    // pretty-printed tokens directly -- i.e. just emit the tokens that represent
80                    // `MyStructName<T, U, ...>` -- and would need to format the string to do so,
81                    // which would mean we wouldn't be able to return `&'static str`.
82                    //
83                    // We'll likely relax that in the future, given the inconsequential nature of
84                    // allocations during configuration schema generation... but this works well for
85                    // now and at least will be consistent within the same Rust version.
86
87                    let self_type_name = ::std::any::type_name::<Self>();
88                    if !self_type_name.starts_with(std::module_path!()) {
89                        Some(std::concat!(std::module_path!(), "::", #ref_name))
90                    } else {
91                        Some(self_type_name)
92                    }
93                }
94
95                #metadata_fn
96
97                #generate_schema_fn
98            }
99
100            impl #impl_generics ::vector_config::ToValue for #name #ty_generics #where_clause {
101                #to_value_fn
102            }
103        };
104    };
105
106    configurable_impl.into()
107}
108
109fn build_metadata_fn(container: &Container<'_>) -> proc_macro2::TokenStream {
110    let meta_ident = Ident::new("metadata", Span::call_site());
111    let container_metadata = generate_container_metadata(&meta_ident, container);
112
113    quote! {
114        fn metadata() -> ::vector_config::Metadata {
115            #container_metadata
116            #meta_ident
117        }
118    }
119}
120
121fn build_to_value_fn(_container: &Container<'_>) -> proc_macro2::TokenStream {
122    quote! {
123        fn to_value(&self) -> ::vector_config::serde_json::Value {
124            ::vector_config::serde_json::to_value(self)
125                .expect("Could not convert value to JSON")
126        }
127    }
128}
129
130fn build_virtual_newtype_schema_fn(virtual_ty: Type) -> proc_macro2::TokenStream {
131    quote! {
132        fn generate_schema(schema_gen: &::std::cell::RefCell<::vector_config::schema::SchemaGenerator>) -> std::result::Result<::vector_config::schema::SchemaObject, ::vector_config::GenerateError> {
133            ::vector_config::schema::get_or_generate_schema(
134                &<#virtual_ty as ::vector_config::Configurable>::as_configurable_ref(),
135                schema_gen,
136                None,
137            )
138        }
139    }
140}
141
142fn build_enum_generate_schema_fn(
143    container: &Container,
144    variants: &[Variant<'_>],
145) -> proc_macro2::TokenStream {
146    // First, figure out if we have a potentially "ambiguous" enum schema. This will influence the
147    // code we generate, which will, at runtime, attempt to figure out if we need to emit an `anyOf`
148    // schema, rather than a `oneOf` schema, to handle validation of enums where variants overlap in
149    // ambiguous ways.
150    let is_potentially_ambiguous = is_enum_schema_potentially_ambiguous(container, variants);
151
152    // Now we'll generate the code for building the schema for each individual variant. This will be
153    // slightly influenced by whether or not we think the enum schema is potentially ambiguous. If
154    // so, we generate some extra code that populates the necessary data to make the call at runtime.
155    let mapped_variants = variants
156        .iter()
157        // Don't map this variant if it's marked to be skipped for both serialization and deserialization.
158        .filter(|variant| variant.visible())
159        .map(|variant| generate_enum_variant_schema(variant, is_potentially_ambiguous));
160
161    // Generate a small little code block that will try and vary the schema approach between `anyOf`
162    // and `oneOf` if we determine that the data in the discriminant map indicates ambiguous variant
163    // schemas.
164    //
165    // If we never generate any entries in the discriminant map, then this will end up just calling
166    // the `oneOf` method.
167    let generate_block = quote! {
168        if ::vector_config::schema::has_ambiguous_discriminants(&discriminant_map) {
169            Ok(::vector_config::schema::generate_any_of_schema(&subschemas))
170        } else {
171            Ok(::vector_config::schema::generate_one_of_schema(&subschemas))
172        }
173    };
174
175    quote! {
176        fn generate_schema(schema_gen: &::std::cell::RefCell<::vector_config::schema::SchemaGenerator>) -> std::result::Result<::vector_config::schema::SchemaObject, ::vector_config::GenerateError> {
177            let mut subschemas = ::std::vec::Vec::new();
178            let mut discriminant_map = ::std::collections::HashMap::new();
179
180            #(#mapped_variants)*
181
182            #generate_block
183        }
184    }
185}
186
187fn is_enum_schema_potentially_ambiguous(container: &Container, variants: &[Variant]) -> bool {
188    let tagging = container
189        .tagging()
190        .expect("enums must always have a tagging mode");
191    match tagging {
192        Tagging::None => {
193            // If we have fewer than two variants, then there's no ambiguity.
194            if variants.len() < 2 {
195                return false;
196            }
197
198            // All variants must be struct variants (i.e. named fields) otherwise we cannot
199            // reasonably determine if they're ambiguous or not.
200            variants.iter().all(|variant| {
201                let fields = variant.fields();
202                !fields.is_empty() && fields.iter().all(|field| field.ident().is_some())
203            })
204        }
205
206        // All other tagging modes have a discriminant, and so can never be ambiguous.
207        _ => false,
208    }
209}
210
211fn build_struct_generate_schema_fn(
212    container: &Container<'_>,
213    style: &Style,
214    fields: &[Field<'_>],
215) -> proc_macro2::TokenStream {
216    match style {
217        Style::Struct => build_named_struct_generate_schema_fn(container, fields),
218        Style::Tuple => build_tuple_struct_generate_schema_fn(fields),
219        Style::Newtype => build_newtype_struct_generate_schema_fn(fields),
220        Style::Unit => panic!("unit structs should be rejected during AST parsing"),
221    }
222}
223
224fn generate_struct_field(field: &Field<'_>) -> proc_macro2::TokenStream {
225    let field_metadata_ref = Ident::new("field_metadata", Span::call_site());
226    let field_metadata = generate_field_metadata(&field_metadata_ref, field);
227    let field_schema_ty = get_field_schema_ty(field);
228
229    let spanned_generate_schema = quote_spanned! {field.span()=>
230        ::vector_config::schema::get_or_generate_schema(
231            &<#field_schema_ty as ::vector_config::Configurable>::as_configurable_ref(),
232            schema_gen,
233            Some(#field_metadata_ref),
234        )?
235    };
236
237    quote! {
238        #field_metadata
239
240        let mut subschema = #spanned_generate_schema;
241    }
242}
243
244fn generate_named_struct_field(
245    container: &Container<'_>,
246    field: &Field<'_>,
247) -> proc_macro2::TokenStream {
248    let field_name = field
249        .ident()
250        .expect("named struct fields must always have an ident");
251    let field_schema_ty = get_field_schema_ty(field);
252    let field_already_contained = format!(
253        "schema properties already contained entry for `{field_name}`, this should not occur"
254    );
255    let field_key = field.name();
256
257    let field_schema = generate_struct_field(field);
258
259    // If the field is flattened, we store it into a different list of flattened subschemas vs adding it directly as a
260    // field via `properties`/`required`.
261    //
262    // If any flattened subschemas are present when we generate the struct schema overall, we do the merging of those at
263    // the end.
264    let integrate_field = if field.flatten() {
265        quote! {
266            flattened_subschemas.push(subschema);
267        }
268    } else {
269        // If there is no default value specified for either the field itself, or the container the
270        // field is a part of, then we consider it required unless the field type itself is inherently
271        // optional, such as being `Option<T>`.
272        let spanned_is_optional = quote_spanned! {field.span()=>
273            <#field_schema_ty as ::vector_config::Configurable>::is_optional()
274        };
275        let maybe_field_required =
276            if container.default_value().is_none() && field.default_value().is_none() {
277                Some(quote! {
278                    if !#spanned_is_optional {
279                        assert!(required.insert(#field_key.to_string()), #field_already_contained);
280                    }
281                })
282            } else {
283                None
284            };
285
286        quote! {
287            if let Some(_) = properties.insert(#field_key.to_string(), subschema) {
288                panic!(#field_already_contained);
289            }
290
291            #maybe_field_required
292        }
293    };
294
295    quote! {
296        {
297            #field_schema
298            #integrate_field
299        }
300    }
301}
302
303fn generate_tuple_struct_field(field: &Field<'_>) -> proc_macro2::TokenStream {
304    let field_schema = generate_struct_field(field);
305
306    quote! {
307        {
308            #field_schema
309            subschemas.push(subschema);
310        }
311    }
312}
313
314fn build_named_struct_generate_schema_fn(
315    container: &Container<'_>,
316    fields: &[Field<'_>],
317) -> proc_macro2::TokenStream {
318    let mapped_fields = fields
319        .iter()
320        // Don't map this field if it's marked to be skipped for both serialization and deserialization.
321        .filter(|field| field.visible())
322        .map(|field| generate_named_struct_field(container, field));
323
324    quote! {
325        fn generate_schema(schema_gen: &::std::cell::RefCell<::vector_config::schema::SchemaGenerator>) -> std::result::Result<::vector_config::schema::SchemaObject, ::vector_config::GenerateError> {
326            let mut properties = ::vector_config::indexmap::IndexMap::new();
327            let mut required = ::std::collections::BTreeSet::new();
328            let mut flattened_subschemas = ::std::vec::Vec::new();
329
330            let metadata = <Self as ::vector_config::Configurable>::metadata();
331            #(#mapped_fields)*
332
333            let had_unflatted_properties = !properties.is_empty();
334
335            let additional_properties = None;
336            let mut schema = ::vector_config::schema::generate_struct_schema(
337                properties,
338                required,
339                additional_properties,
340            );
341
342            // If we have any flattened subschemas, deal with them now.
343            if !flattened_subschemas.is_empty() {
344                // A niche case here is if all fields were flattened, which would leave our main
345                // schema as simply validating that the value is an object, and _nothing_ else.
346                //
347                // That's kind of useless, and ends up as noise in the schema, so if we didn't have
348                // any of our own unflattened properties, then steal the first flattened subschema
349                // and swap our main schema for it before flattening things overall.
350                if !had_unflatted_properties {
351                    schema = flattened_subschemas.remove(0);
352                }
353
354                ::vector_config::schema::convert_to_flattened_schema(&mut schema, flattened_subschemas);
355            }
356
357            Ok(schema)
358        }
359    }
360}
361
362fn build_tuple_struct_generate_schema_fn(fields: &[Field<'_>]) -> proc_macro2::TokenStream {
363    let mapped_fields = fields
364        .iter()
365        // Don't map this field if it's marked to be skipped for both serialization and deserialization.
366        .filter(|field| field.visible())
367        .map(generate_tuple_struct_field);
368
369    quote! {
370        fn generate_schema(schema_gen: &::std::cell::RefCell<::vector_config::schema::SchemaGenerator>) -> std::result::Result<::vector_config::schema::SchemaObject, ::vector_config::GenerateError> {
371            let mut subschemas = ::std::collections::Vec::new();
372
373            #(#mapped_fields)*
374
375            Ok(::vector_config::schema::generate_tuple_schema(&subschemas))
376        }
377    }
378}
379
380fn build_newtype_struct_generate_schema_fn(fields: &[Field<'_>]) -> proc_macro2::TokenStream {
381    // Map the fields normally, but we should end up with a single field at the end.
382    let mut mapped_fields = fields
383        .iter()
384        // Don't map this field if it's marked to be skipped for both serialization and deserialization.
385        .filter(|field| field.visible())
386        .map(generate_struct_field)
387        .collect::<Vec<_>>();
388
389    if mapped_fields.len() != 1 {
390        panic!("newtype structs should never have more than one field");
391    }
392
393    let field_schema = mapped_fields.remove(0);
394
395    quote! {
396        fn generate_schema(schema_gen: &::std::cell::RefCell<::vector_config::schema::SchemaGenerator>) -> std::result::Result<::vector_config::schema::SchemaObject, ::vector_config::GenerateError> {
397            #field_schema
398
399            Ok(subschema)
400        }
401    }
402}
403
404fn generate_container_metadata(
405    meta_ident: &Ident,
406    container: &Container<'_>,
407) -> proc_macro2::TokenStream {
408    let maybe_title = get_metadata_title(meta_ident, container.title());
409    let maybe_description = get_metadata_description(meta_ident, container.description());
410    let maybe_default_value = get_metadata_default_value(meta_ident, container.default_value());
411    let maybe_deprecated = get_metadata_deprecated(meta_ident, container.deprecated());
412    let maybe_custom_attributes = get_metadata_custom_attributes(meta_ident, container.metadata());
413
414    // We add a special metadata that informs consumers of the schema what the "tagging mode" of
415    // this enum is. This is important because when we're using the schema to generate
416    // documentation, it can be hard to generate something that is as succinct as how you might
417    // otherwise describe the configuration behavior using natural language. Additionally, we
418    // typically allow deserialization such that fields are overlapped, and if variants had, for
419    // example, 3 shared fields between all variants, and each variant only had 1 unique field, we
420    // wouldn't want to relist all the shared fields per variant.... we just want to be able to
421    // describe which variant has to be used for its unique (variant specific) fields to be
422    // relevant.
423    let enum_metadata_attrs = container
424        .tagging()
425        .map(|tagging| tagging.as_enum_metadata());
426    let enum_metadata =
427        get_metadata_custom_attributes(meta_ident, enum_metadata_attrs.into_iter().flatten());
428
429    quote! {
430        let mut #meta_ident = ::vector_config::Metadata::default();
431        #maybe_title
432        #maybe_description
433        #maybe_default_value
434        #maybe_deprecated
435        #maybe_custom_attributes
436        #enum_metadata
437    }
438}
439
440fn generate_field_metadata(meta_ident: &Ident, field: &Field<'_>) -> proc_macro2::TokenStream {
441    let field_ty = field.ty();
442    let field_schema_ty = get_field_schema_ty(field);
443
444    let maybe_title = get_metadata_title(meta_ident, field.title());
445    let maybe_description = get_metadata_description(meta_ident, field.description());
446    let maybe_clear_title_description = field
447        .title()
448        .or_else(|| field.description())
449        .is_some()
450        .then(|| {
451            quote! {
452                // Fields with a title/description of their own cannot merge with the title/description
453                // of the field type itself, as this will generally lead to confusing output, so we
454                // explicitly clear the title/description first if we're about to set our own
455                // title/description.
456                #meta_ident.clear_title();
457                #meta_ident.clear_description();
458            }
459        });
460    let maybe_default_value = if field_ty != field_schema_ty {
461        get_metadata_default_value_delegated(meta_ident, field_schema_ty, field.default_value())
462    } else {
463        get_metadata_default_value(meta_ident, field.default_value())
464    };
465    let maybe_deprecated = get_metadata_deprecated(meta_ident, field.deprecated());
466    let maybe_deprecated_message =
467        get_metadata_deprecated_message(meta_ident, field.deprecated_message());
468    let maybe_transparent = get_metadata_transparent(meta_ident, field.transparent());
469    let maybe_validation = get_metadata_validation(meta_ident, field.validation());
470    let maybe_custom_attributes = get_metadata_custom_attributes(meta_ident, field.metadata());
471
472    quote! {
473        let mut #meta_ident = ::vector_config::Metadata::default();
474        #maybe_clear_title_description
475        #maybe_title
476        #maybe_description
477        #maybe_default_value
478        #maybe_deprecated
479        #maybe_deprecated_message
480        #maybe_transparent
481        #maybe_validation
482        #maybe_custom_attributes
483    }
484}
485
486fn generate_variant_metadata(
487    meta_ident: &Ident,
488    variant: &Variant<'_>,
489) -> proc_macro2::TokenStream {
490    let maybe_title = get_metadata_title(meta_ident, variant.title());
491    let maybe_description = get_metadata_description(meta_ident, variant.description());
492    let maybe_deprecated = get_metadata_deprecated(meta_ident, variant.deprecated());
493
494    // We have to mark variants as transparent, so that if we're dealing with an untagged enum, we
495    // don't panic if their description is intentionally left out.
496    let maybe_transparent =
497        get_metadata_transparent(meta_ident, variant.tagging() == &Tagging::None);
498    let maybe_custom_attributes = get_metadata_custom_attributes(meta_ident, variant.metadata());
499
500    // We add a special metadata key (`logical_name`) that informs consumers of the schema what the
501    // variant name is for this variant's subschema. While the doc comments being coerced into title
502    // and/or description are typically good enough, sometimes we need a more mechanical mapping of
503    // the variant's name since shoving it into the title would mean doc comments with redundant
504    // information.
505    //
506    // You can think of this as an enum-specific additional title.
507    let logical_name_attrs = vec![LazyCustomAttribute::kv(
508        "logical_name",
509        variant.ident().to_string(),
510    )];
511    let variant_logical_name =
512        get_metadata_custom_attributes(meta_ident, logical_name_attrs.into_iter());
513
514    // We specifically use `()` as the type here because we need to generate the metadata for this
515    // variant, but there's no unique concrete type for a variant, only the type of the enum
516    // container it exists within. We also don't want to use the metadata of the enum container, as
517    // it might have values that would conflict with the metadata of this specific variant.
518    quote! {
519        let mut #meta_ident = ::vector_config::Metadata::default();
520        #maybe_title
521        #maybe_description
522        #maybe_deprecated
523        #maybe_transparent
524        #maybe_custom_attributes
525        #variant_logical_name
526    }
527}
528
529fn generate_variant_tag_metadata(
530    meta_ident: &Ident,
531    variant: &Variant<'_>,
532) -> proc_macro2::TokenStream {
533    // For enum variant tags, all we care about is shuttling the title/description of the variant
534    // itself along with the tag field to make downstream consumption and processing easier.
535    let maybe_title = get_metadata_title(meta_ident, variant.title());
536    let maybe_description = get_metadata_description(meta_ident, variant.description());
537
538    // We specifically use `()` as the type here because we need to generate the metadata for this
539    // variant, but there's no unique concrete type for a variant, only the type of the enum
540    // container it exists within. We also don't want to use the metadata of the enum container, as
541    // it might have values that would conflict with the metadata of this specific variant.
542    quote! {
543        let mut #meta_ident = ::vector_config::Metadata::default();
544        #maybe_title
545        #maybe_description
546    }
547}
548
549fn get_metadata_title(
550    meta_ident: &Ident,
551    title: Option<&String>,
552) -> Option<proc_macro2::TokenStream> {
553    title.map(|title| {
554        quote! {
555            #meta_ident.set_title(#title);
556        }
557    })
558}
559
560fn get_metadata_description(
561    meta_ident: &Ident,
562    description: Option<&String>,
563) -> Option<proc_macro2::TokenStream> {
564    description.map(|description| {
565        quote! {
566            #meta_ident.set_description(#description);
567        }
568    })
569}
570
571fn get_metadata_default_value(
572    meta_ident: &Ident,
573    default_value: Option<ExprPath>,
574) -> Option<proc_macro2::TokenStream> {
575    default_value.map(|value| {
576        quote! {
577            #meta_ident.set_default_value(#value());
578        }
579    })
580}
581
582fn get_metadata_default_value_delegated(
583    meta_ident: &Ident,
584    default_ty: &syn::Type,
585    default_value: Option<ExprPath>,
586) -> Option<proc_macro2::TokenStream> {
587    default_value.map(|value| {
588        let default_ty = get_ty_for_expr_pos(default_ty);
589
590        quote! {
591            #meta_ident.set_default_value(#default_ty::from(#value()));
592        }
593    })
594}
595
596fn get_metadata_deprecated(
597    meta_ident: &Ident,
598    deprecated: bool,
599) -> Option<proc_macro2::TokenStream> {
600    deprecated.then(|| {
601        quote! {
602            #meta_ident.set_deprecated();
603        }
604    })
605}
606
607fn get_metadata_deprecated_message(
608    meta_ident: &Ident,
609    message: Option<&String>,
610) -> Option<proc_macro2::TokenStream> {
611    message.map(|message| {
612        quote! {
613            #meta_ident.set_deprecated_message(#message);
614        }
615    })
616}
617
618fn get_metadata_transparent(
619    meta_ident: &Ident,
620    transparent: bool,
621) -> Option<proc_macro2::TokenStream> {
622    transparent.then(|| {
623        quote! {
624            #meta_ident.set_transparent();
625        }
626    })
627}
628
629fn get_metadata_validation(
630    meta_ident: &Ident,
631    validation: &[Validation],
632) -> proc_macro2::TokenStream {
633    let mapped_validation = validation
634        .iter()
635        .map(|v| quote! { #meta_ident.add_validation(#v); });
636
637    quote! {
638        #(#mapped_validation)*
639    }
640}
641
642fn get_metadata_custom_attributes(
643    meta_ident: &Ident,
644    custom_attributes: impl Iterator<Item = LazyCustomAttribute>,
645) -> proc_macro2::TokenStream {
646    let mapped_custom_attributes = custom_attributes
647        .map(|attr| match attr {
648            LazyCustomAttribute::Flag(key) => quote! {
649                #meta_ident.add_custom_attribute(::vector_config::attributes::CustomAttribute::flag(#key));
650            },
651            LazyCustomAttribute::KeyValue { key, value } => quote! {
652                #meta_ident.add_custom_attribute(::vector_config::attributes::CustomAttribute::kv(
653                    #key, #value
654                ));
655            },
656        });
657
658    quote! {
659        #(#mapped_custom_attributes)*
660    }
661}
662
663fn get_field_schema_ty<'a>(field: &'a Field<'a>) -> &'a syn::Type {
664    // If there's a delegated type being used for field (de)serialization, that's ultimately the type
665    // we use to declare the schema, because we have to generate the schema for whatever type is
666    // actually being (de)serialized, not the final type that the intermediate value ends up getting
667    // converted to.
668    //
669    // Otherwise, we just use the actual field type.
670    field.delegated_ty().unwrap_or_else(|| field.ty())
671}
672
673fn generate_named_enum_field(field: &Field<'_>) -> proc_macro2::TokenStream {
674    let field_name = field.ident().expect("field should be named");
675    let field_ty = field.ty();
676    let field_already_contained = format!(
677        "schema properties already contained entry for `{field_name}`, this should not occur"
678    );
679    let field_key = field.name().to_string();
680
681    let field_schema = generate_struct_field(field);
682
683    // Fields that have no default value are inherently required.  Unlike fields on a normal
684    // struct, we can't derive a default value for an individual field because `serde`
685    // doesn't allow even specifying a default value for an enum overall, only structs.
686    let spanned_is_optional = quote_spanned! {field.span()=>
687        <#field_ty as ::vector_config::Configurable>::is_optional()
688    };
689    let maybe_field_required = if field.default_value().is_none() {
690        Some(quote! {
691        if !#spanned_is_optional {
692                if !required.insert(#field_key.to_string()) {
693                    panic!(#field_already_contained);
694                }
695            }
696        })
697    } else {
698        None
699    };
700
701    if field.flatten() {
702        quote! {
703            {
704                #field_schema
705                flattened_subschemas.push(subschema);
706            }
707        }
708    } else {
709        quote! {
710            {
711                #field_schema
712
713                if let Some(_) = properties.insert(#field_key.to_string(), subschema) {
714                    panic!(#field_already_contained);
715                }
716
717                #maybe_field_required
718            }
719        }
720    }
721}
722
723fn generate_enum_struct_named_variant_schema(
724    variant: &Variant<'_>,
725    post_fields: Option<proc_macro2::TokenStream>,
726    is_potentially_ambiguous: bool,
727) -> proc_macro2::TokenStream {
728    let mapped_fields = variant.fields().iter().map(generate_named_enum_field);
729
730    // If this variant is part of a potentially ambiguous enum schema, we add this variant's
731    // required fields to the discriminant map, keyed off of the variant name.
732    let maybe_fill_discriminant_map = is_potentially_ambiguous.then(|| {
733        let variant_name = variant.ident().to_string();
734        quote! {
735            discriminant_map.insert(#variant_name, required.clone());
736        }
737    });
738
739    quote! {
740        {
741            let mut properties = ::vector_config::indexmap::IndexMap::new();
742            let mut required = ::std::collections::BTreeSet::new();
743            let mut flattened_subschemas = ::std::vec::Vec::new();
744
745            #(#mapped_fields)*
746
747            #post_fields
748
749            #maybe_fill_discriminant_map
750
751            let mut schema = ::vector_config::schema::generate_struct_schema(
752                properties,
753                required,
754                None
755            );
756
757            // If we have any flattened subschemas, deal with them now.
758            if !flattened_subschemas.is_empty() {
759                ::vector_config::schema::convert_to_flattened_schema(&mut schema, flattened_subschemas);
760            }
761
762            schema
763        }
764    }
765}
766
767fn generate_enum_newtype_struct_variant_schema(variant: &Variant<'_>) -> proc_macro2::TokenStream {
768    // When we only have a single unnamed field, we basically just treat it as a
769    // passthrough, and we generate the schema for that field directly, without any
770    // metadata or anything, since things like defaults can't travel from the enum
771    // container to a specific variant anyways.
772    let field = variant.fields().first().expect("must exist");
773    let field_schema = generate_struct_field(field);
774
775    quote! {
776        {
777            #field_schema
778            subschema
779        }
780    }
781}
782
783fn generate_enum_variant_tag_schema(variant: &Variant<'_>) -> proc_macro2::TokenStream {
784    let variant_name = variant.name();
785    let apply_variant_tag_metadata = generate_enum_variant_tag_apply_metadata(variant);
786
787    quote! {
788        {
789            let mut tag_subschema = ::vector_config::schema::generate_const_string_schema(#variant_name.to_string());
790            #apply_variant_tag_metadata
791            tag_subschema
792        }
793    }
794}
795
796fn generate_enum_variant_schema(
797    variant: &Variant<'_>,
798    is_potentially_ambiguous: bool,
799) -> proc_macro2::TokenStream {
800    // For the sake of all examples below, we'll use JSON syntax to represent the following enum
801    // variants:
802    //
803    // enum ExampleEnum {
804    //   Struct { some_field: bool },
805    //   Unnamed(bool),
806    //   Unit,
807    // }
808    let variant_name = variant.name();
809    let variant_schema = match variant.tagging() {
810        // The variant is represented "externally" by wrapping the contents of the variant as an
811        // object pointed to by a property whose name is the name of the variant.
812        //
813        // This is when the rendered output looks like the following:
814        //
815        // # Struct form.
816        // { "field_using_enum": { "VariantName": { "some_field": false } } }
817        //
818        // # Struct form with unnamed field.
819        // { "field_using_enum": { "VariantName": false } }
820        //
821        // # Unit form.
822        // { "field_using_enum": "VariantName" }
823        Tagging::External => {
824            let (wrapped, variant_schema) = match variant.style() {
825                Style::Struct => (
826                    true,
827                    generate_enum_struct_named_variant_schema(variant, None, false),
828                ),
829                Style::Tuple => panic!("tuple variants should be rejected during AST parsing"),
830                Style::Newtype => (true, generate_enum_newtype_struct_variant_schema(variant)),
831                Style::Unit => (false, generate_enum_variant_tag_schema(variant)),
832            };
833
834            // In external mode, we don't wrap the schema for unit variants, because they're
835            // interpreted directly as the value of the field using the enum.
836            //
837            // TODO: we can maybe reuse the existing struct schema gen stuff here, but we'd need
838            // a way to force being required + customized metadata
839            if wrapped {
840                generate_single_field_struct_schema(variant_name, variant_schema)
841            } else {
842                variant_schema
843            }
844        }
845        // The variant is represented "internally" by adding a new property to the contents of the
846        // variant whose name is the value of `tag` and must match the name of the variant.
847        //
848        // This is when the rendered output looks like the following:
849        //
850        // # Struct form.
851        // { "field_using_enum": { "<tag>": "VariantName", "some_field": false } }
852        //
853        // # Struct form with unnamed field is not valid here.  See comments below.
854        //
855        // # Unit form.
856        // { "field_using_enum": { "<tag>": "VariantName" } }
857        Tagging::Internal { tag } => match variant.style() {
858            Style::Struct => {
859                let tag_already_contained = format!("enum tag `{tag}` already contained as a field in variant; tag cannot overlap with any fields in any variant");
860
861                // Just generate the tag field directly and pass it along to be included in the
862                // struct schema.
863                let tag_schema = generate_enum_variant_tag_schema(variant);
864                let tag_field = quote! {
865                    {
866                        if let Some(_) = properties.insert(#tag.to_string(), #tag_schema) {
867                            panic!(#tag_already_contained);
868                        }
869
870                        if !required.insert(#tag.to_string()) {
871                            panic!(#tag_already_contained);
872                        }
873                    }
874                };
875                generate_enum_struct_named_variant_schema(variant, Some(tag_field), false)
876            }
877            Style::Tuple => panic!("tuple variants should be rejected during AST parsing"),
878            Style::Newtype => {
879                // We have to delegate viability to `serde`, essentially, because using internal tagging for a newtype
880                // variant is only possible when the inner field is a struct or map, and we can't access that type of
881                // information here, which is why `serde` does it at compile-time.
882
883                // As such, we generate the schema for the single field, like we would normally do for a newtype
884                // variant, and then we follow the struct flattening logic where we layer on our tag field schema on the
885                // schema of the wrapped field... and since it has to be a struct or map to be valid for `serde`, that
886                // means it will also be an object schema in both cases, which means our flattening logic will be
887                // correct if the caller is doing The Right Thing (tm).
888                let newtype_schema = generate_enum_newtype_struct_variant_schema(variant);
889                let tag_schema = generate_enum_variant_tag_schema(variant);
890
891                quote! {
892                    let tag_schema = ::vector_config::schema::generate_internal_tagged_variant_schema(#tag.to_string(), #tag_schema);
893                    let mut flattened_subschemas = ::std::vec::Vec::new();
894                    flattened_subschemas.push(tag_schema);
895
896                    let mut newtype_schema = #newtype_schema;
897                    ::vector_config::schema::convert_to_flattened_schema(&mut newtype_schema, flattened_subschemas);
898
899                    newtype_schema
900                }
901            }
902            Style::Unit => {
903                // Internally-tagged unit variants are basically just a play on externally-tagged
904                // struct variants.
905                let variant_schema = generate_enum_variant_tag_schema(variant);
906                generate_single_field_struct_schema(tag, variant_schema)
907            }
908        },
909        // The variant is represented "adjacent" to the content, such that the variant name is in a
910        // field whose name is the value of `tag` and the content of the variant is in a field whose
911        // name is the value of `content`.
912        //
913        // This is when the rendered output looks like the following:
914        //
915        // # Struct form.
916        // { "field_using_enum": { "<tag>": "VariantName", "<content>": { "some_field": false } } }
917        //
918        // # Struct form with unnamed field.
919        // { "field_using_enum": { "<tag>": "VariantName", "<content>": false } }
920        //
921        // # Unit form.
922        // { "field_using_enum": { "<tag>": "VariantName" } }
923        Tagging::Adjacent { tag, content } => {
924            // For struct-type variants, just generate their schema as normal, and we'll wrap it up
925            // in a new object.  For unit variants, adjacent tagging is identical to internal
926            // tagging, so we handle that one by hand.
927            let tag_schema = generate_enum_variant_tag_schema(variant);
928            let maybe_content_schema = match variant.style() {
929                Style::Struct => Some(generate_enum_struct_named_variant_schema(
930                    variant, None, false,
931                )),
932                Style::Tuple => panic!("tuple variants should be rejected during AST parsing"),
933                Style::Newtype => Some(generate_enum_newtype_struct_variant_schema(variant)),
934                Style::Unit => None,
935            }
936            .map(|content_schema| {
937                quote! {
938                    wrapper_properties.insert(#content.to_string(), #content_schema);
939                    wrapper_required.insert(#content.to_string());
940                }
941            });
942
943            quote! {
944                let mut wrapper_properties = ::vector_config::indexmap::IndexMap::new();
945                let mut wrapper_required = ::std::collections::BTreeSet::new();
946
947                wrapper_properties.insert(#tag.to_string(), #tag_schema);
948                wrapper_required.insert(#tag.to_string());
949
950                #maybe_content_schema
951
952                ::vector_config::schema::generate_struct_schema(
953                    wrapper_properties,
954                    wrapper_required,
955                    None
956                )
957            }
958        }
959        Tagging::None => {
960            // This is simply when it's a free-for-all and `serde` tries to deserialize the data as
961            // each variant until it finds one that can deserialize the data correctly. Essentially,
962            // we encode the variant solely based on its contents, which for a unit variant, would
963            // be nothing: a literal `null` in JSON.
964            //
965            // Accordingly, there is a higher-level check before we get here that yells at the user
966            // that using `#[serde(untagged)]` with an enum where some variants that have
967            // duplicate contents, compared to their siblings, is not allowed because doing so
968            // provides unstable deserialization.
969            //
970            // This is when the rendered output looks like the following:
971            //
972            // # Struct form.
973            // { "field_using_enum": { "some_field": false } }
974            //
975            // # Struct form with unnamed field.
976            // { "field_using_enum": false }
977            //
978            // # Unit form.
979            // { "field_using_enum": null }
980            //
981            // TODO: actually implement the aforementioned higher-level check
982
983            match variant.style() {
984                Style::Struct => generate_enum_struct_named_variant_schema(
985                    variant,
986                    None,
987                    is_potentially_ambiguous,
988                ),
989                Style::Tuple => panic!("tuple variants should be rejected during AST parsing"),
990                Style::Newtype => generate_enum_newtype_struct_variant_schema(variant),
991                Style::Unit => quote! { ::vector_config::schema::generate_null_schema() },
992            }
993        }
994    };
995
996    generate_enum_variant_subschema(variant, variant_schema)
997}
998
999fn generate_single_field_struct_schema(
1000    property_name: &str,
1001    property_schema: proc_macro2::TokenStream,
1002) -> proc_macro2::TokenStream {
1003    quote! {
1004        {
1005            let mut wrapper_properties = ::vector_config::indexmap::IndexMap::new();
1006            let mut wrapper_required = ::std::collections::BTreeSet::new();
1007
1008            wrapper_properties.insert(#property_name.to_string(), #property_schema);
1009            wrapper_required.insert(#property_name.to_string());
1010
1011            ::vector_config::schema::generate_struct_schema(
1012                wrapper_properties,
1013                wrapper_required,
1014                None
1015            )
1016        }
1017    }
1018}
1019
1020fn generate_enum_variant_apply_metadata(variant: &Variant<'_>) -> proc_macro2::TokenStream {
1021    let variant_metadata_ref = Ident::new("variant_metadata", Span::call_site());
1022    let variant_metadata = generate_variant_metadata(&variant_metadata_ref, variant);
1023
1024    quote! {
1025        #variant_metadata
1026        ::vector_config::schema::apply_base_metadata(&mut subschema, #variant_metadata_ref);
1027    }
1028}
1029
1030fn generate_enum_variant_tag_apply_metadata(variant: &Variant<'_>) -> proc_macro2::TokenStream {
1031    let variant_tag_metadata_ref = Ident::new("variant_tag_metadata", Span::call_site());
1032    let variant_tag_metadata = generate_variant_tag_metadata(&variant_tag_metadata_ref, variant);
1033
1034    quote! {
1035        #variant_tag_metadata
1036        ::vector_config::schema::apply_base_metadata(&mut tag_subschema, #variant_tag_metadata_ref);
1037    }
1038}
1039
1040fn generate_enum_variant_subschema(
1041    variant: &Variant<'_>,
1042    variant_schema: proc_macro2::TokenStream,
1043) -> proc_macro2::TokenStream {
1044    let apply_variant_metadata = generate_enum_variant_apply_metadata(variant);
1045
1046    quote! {
1047        {
1048            let mut subschema = { #variant_schema };
1049            #apply_variant_metadata
1050
1051            subschemas.push(subschema);
1052        }
1053    }
1054}
1055
1056/// Gets a type token suitable for use in expression position.
1057///
1058/// Normally, we refer to types with generic type parameters using their condensed form: `T<...>`.
1059/// Sometimes, however, we must refer to them with their disambiguated form: `T::<...>`. This is due
1060/// to a limitation in syntax parsing between types in statement versus expression position.
1061///
1062/// Statement position would be somewhere like declaring a field on a struct, where using angle
1063/// brackets has no ambiguous meaning, as you can't compare two items as part of declaring a struct
1064/// field. Conversely, expression position implies anywhere we could normally provide an expression,
1065/// and expressions can certainly contain comparisons. As such, we need to use the disambiguated
1066/// form in expression position.
1067///
1068/// While most commonly used for passing generic type parameters to functions/methods themselves,
1069/// this is also known as the "turbofish" syntax.
1070fn get_ty_for_expr_pos(ty: &syn::Type) -> syn::Type {
1071    match ty {
1072        syn::Type::Path(tp) => {
1073            let mut new_tp = tp.clone();
1074            for segment in new_tp.path.segments.iter_mut() {
1075                if let PathArguments::AngleBracketed(ab) = &mut segment.arguments {
1076                    ab.colon2_token = Some(PathSep::default());
1077                }
1078            }
1079
1080            syn::Type::Path(new_tp)
1081        }
1082        _ => ty.clone(),
1083    }
1084}