Skip to main content

tier_derive/
lib.rs

1#![warn(missing_docs)]
2#![doc = include_str!("../README.md")]
3
4use proc_macro::TokenStream;
5use quote::{format_ident, quote};
6use std::collections::{HashMap, HashSet};
7use syn::{
8    Attribute, Data, DataEnum, DataStruct, DeriveInput, Expr, Field, Fields, FieldsNamed,
9    FieldsUnnamed, GenericArgument, Lit, LitStr, Meta, PathArguments, Type, parse_macro_input,
10    punctuated::Punctuated, spanned::Spanned,
11};
12
13#[proc_macro_derive(TierConfig, attributes(tier, serde))]
14/// Derives `tier::TierMetadata` for nested configuration structs.
15pub fn derive_tier_config(input: TokenStream) -> TokenStream {
16    let input = parse_macro_input!(input as DeriveInput);
17    match expand_tier_config(input) {
18        Ok(tokens) => tokens.into(),
19        Err(error) => error.to_compile_error().into(),
20    }
21}
22
23#[proc_macro_derive(TierPatch, attributes(tier, serde))]
24/// Derives `tier::TierPatch` for typed sparse override structs.
25pub fn derive_tier_patch(input: TokenStream) -> TokenStream {
26    let input = parse_macro_input!(input as DeriveInput);
27    match expand_tier_patch(input) {
28        Ok(tokens) => tokens.into(),
29        Err(error) => error.to_compile_error().into(),
30    }
31}
32
33fn expand_tier_config(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
34    let tier_attrs = parse_tier_container_attrs(&input.attrs)?;
35    let ident = input.ident;
36    let generics = input.generics;
37    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
38    let container_attrs = parse_serde_container_attrs(&input.attrs)?;
39    let field_tokens = match input.data {
40        Data::Struct(data_struct) => expand_struct_metadata(data_struct, &container_attrs)?,
41        Data::Enum(data_enum) => expand_enum_metadata(data_enum, &container_attrs)?,
42        Data::Union(union) => {
43            return Err(syn::Error::new_spanned(
44                union.union_token,
45                "TierConfig cannot be derived for unions",
46            ));
47        }
48    };
49    let check_tokens = container_check_tokens(&tier_attrs);
50
51    Ok(quote! {
52        impl #impl_generics ::tier::TierMetadata for #ident #ty_generics #where_clause {
53            fn metadata() -> ::tier::ConfigMetadata {
54                let mut metadata = ::tier::ConfigMetadata::new();
55                #(#field_tokens)*
56                #(#check_tokens)*
57                metadata
58            }
59        }
60    })
61}
62
63fn expand_tier_patch(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
64    let ident = input.ident;
65    let generics = input.generics;
66    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
67    let container_attrs = parse_serde_container_attrs(&input.attrs)?;
68    ensure_struct_patch_container_attrs(&container_attrs)?;
69
70    let field_tokens = match input.data {
71        Data::Struct(data_struct) => match data_struct.fields {
72            Fields::Named(fields) => expand_patch_fields_metadata(
73                fields,
74                SerdeFieldContext::for_struct(&container_attrs),
75            )?,
76            Fields::Unnamed(fields) => {
77                return Err(syn::Error::new_spanned(
78                    fields,
79                    "TierPatch only supports structs with named fields",
80                ));
81            }
82            Fields::Unit => Vec::new(),
83        },
84        Data::Enum(data_enum) => {
85            return Err(syn::Error::new_spanned(
86                data_enum.enum_token,
87                "TierPatch cannot be derived for enums",
88            ));
89        }
90        Data::Union(union) => {
91            return Err(syn::Error::new_spanned(
92                union.union_token,
93                "TierPatch cannot be derived for unions",
94            ));
95        }
96    };
97
98    Ok(quote! {
99        impl #impl_generics ::tier::TierPatch for #ident #ty_generics #where_clause {
100            fn write_layer(
101                &self,
102                __tier_builder: &mut ::tier::patch::PatchLayerBuilder,
103                __tier_prefix: &str,
104            ) -> ::std::result::Result<(), ::tier::ConfigError> {
105                #(#field_tokens)*
106                Ok(())
107            }
108        }
109    })
110}
111
112fn expand_struct_metadata(
113    data_struct: DataStruct,
114    container_attrs: &SerdeContainerAttrs,
115) -> syn::Result<Vec<proc_macro2::TokenStream>> {
116    ensure_struct_container_attrs(container_attrs)?;
117
118    match data_struct.fields {
119        Fields::Named(fields) => expand_named_fields_metadata(
120            fields,
121            SerdeFieldContext::for_struct(container_attrs),
122            &format_ident!("metadata"),
123            None,
124        ),
125        Fields::Unnamed(fields) => {
126            expand_newtype_struct_metadata(fields, &format_ident!("metadata"))
127        }
128        Fields::Unit => Ok(Vec::new()),
129    }
130}
131
132fn expand_enum_metadata(
133    data_enum: DataEnum,
134    container_attrs: &SerdeContainerAttrs,
135) -> syn::Result<Vec<proc_macro2::TokenStream>> {
136    let representation = enum_representation(container_attrs)?;
137    let conflicts = non_external_variant_field_conflicts(&data_enum, container_attrs)?;
138    let mut tokens = vec![quote! {
139        metadata.push(
140            ::tier::FieldMetadata::new("").merge_strategy(::tier::MergeStrategy::Replace)
141        );
142    }];
143    if let Some(tag) = representation.tag_field() {
144        let tag_lit = LitStr::new(tag, proc_macro2::Span::call_site());
145        tokens.push(quote! {
146            metadata.push(::tier::FieldMetadata::new(#tag_lit));
147        });
148    }
149
150    for variant in data_enum.variants {
151        let variant_ident = variant.ident.clone();
152        let variant_attrs =
153            parse_serde_variant_attrs(&variant.attrs, &variant_ident, container_attrs)?;
154        if variant_attrs.skip_metadata {
155            continue;
156        }
157
158        match variant.fields {
159            Fields::Named(fields) => {
160                let field_tokens = expand_named_fields_metadata(
161                    fields,
162                    SerdeFieldContext::for_enum_variant_fields(container_attrs),
163                    &format_ident!("variant_metadata"),
164                    Some(&conflicts),
165                )?;
166                push_variant_tokens(
167                    &mut tokens,
168                    field_tokens,
169                    &variant_attrs,
170                    &representation,
171                    variant_ident.span(),
172                );
173            }
174            Fields::Unnamed(fields) => {
175                let field_tokens = expand_newtype_variant_metadata(
176                    fields,
177                    &representation,
178                    variant_ident.span(),
179                    &format_ident!("variant_metadata"),
180                )?;
181                push_variant_tokens(
182                    &mut tokens,
183                    field_tokens,
184                    &variant_attrs,
185                    &representation,
186                    variant_ident.span(),
187                );
188            }
189            Fields::Unit => {}
190        }
191    }
192
193    Ok(tokens)
194}
195
196fn push_variant_tokens(
197    tokens: &mut Vec<proc_macro2::TokenStream>,
198    variant_tokens: Vec<proc_macro2::TokenStream>,
199    variant_attrs: &SerdeVariantAttrs,
200    representation: &EnumRepresentation,
201    span: proc_macro2::Span,
202) {
203    let variant_name_lit = LitStr::new(&variant_attrs.canonical_name, span);
204    let variant_alias_lits = variant_attrs
205        .aliases
206        .iter()
207        .map(|alias| LitStr::new(alias, span))
208        .collect::<Vec<_>>();
209
210    match representation {
211        EnumRepresentation::External => {
212            tokens.push(quote! {
213                {
214                    let mut variant_metadata = ::tier::ConfigMetadata::new();
215                    #(#variant_tokens)*
216                    metadata.extend(::tier::metadata::prefixed_metadata(
217                        #variant_name_lit,
218                        ::std::vec![#(::std::string::String::from(#variant_alias_lits)),*],
219                        variant_metadata,
220                    ));
221                }
222            });
223        }
224        EnumRepresentation::Adjacent { content, .. } => {
225            let content_lit = LitStr::new(content, span);
226            tokens.push(quote! {
227                {
228                    let mut variant_metadata = ::tier::ConfigMetadata::new();
229                    #(#variant_tokens)*
230                    metadata.extend(::tier::metadata::prefixed_metadata(
231                        #content_lit,
232                        ::std::vec![],
233                        variant_metadata,
234                    ));
235                }
236            });
237        }
238        EnumRepresentation::Internal { .. } | EnumRepresentation::Untagged => {
239            tokens.push(quote! {
240                {
241                    let mut variant_metadata = ::tier::ConfigMetadata::new();
242                    #(#variant_tokens)*
243                    metadata.extend(variant_metadata);
244                }
245            });
246        }
247    }
248}
249
250fn expand_named_fields_metadata(
251    fields: FieldsNamed,
252    context: SerdeFieldContext,
253    accumulator: &proc_macro2::Ident,
254    conflicts: Option<&NonExternalFieldConflicts>,
255) -> syn::Result<Vec<proc_macro2::TokenStream>> {
256    let mut field_tokens = Vec::new();
257
258    for field in fields.named {
259        field_tokens.extend(expand_named_field_metadata(
260            field,
261            context,
262            accumulator,
263            conflicts,
264        )?);
265    }
266
267    Ok(field_tokens)
268}
269
270fn expand_named_field_metadata(
271    field: Field,
272    context: SerdeFieldContext,
273    accumulator: &proc_macro2::Ident,
274    conflicts: Option<&NonExternalFieldConflicts>,
275) -> syn::Result<Vec<proc_macro2::TokenStream>> {
276    let field_ident = field.ident.expect("named field");
277    let mut serde_attrs = parse_serde_field_attrs(&field.attrs, &field_ident, context)?;
278    let mut attrs = parse_tier_attrs(&field.attrs)?;
279    if attrs.doc.is_none() {
280        attrs.doc = doc_comment(&field.attrs);
281    }
282
283    if serde_attrs.skip_metadata {
284        if attrs.has_any() {
285            return Err(syn::Error::new_spanned(
286                field_ident,
287                "skipped fields cannot use tier metadata attributes",
288            ));
289        }
290        return Ok(Vec::new());
291    }
292
293    if serde_attrs.flatten && attrs.has_any() {
294        return Err(syn::Error::new_spanned(
295            field_ident,
296            "flattened fields cannot use tier metadata attributes",
297        ));
298    }
299
300    if let Some(conflicts) = conflicts {
301        if conflicts
302            .skipped_fields
303            .contains(&serde_attrs.canonical_name)
304        {
305            return Ok(Vec::new());
306        }
307        serde_attrs
308            .aliases
309            .retain(|alias| !conflicts.skipped_aliases.contains(alias));
310        if attrs
311            .env
312            .as_ref()
313            .is_some_and(|env| conflicts.skipped_envs.contains(env))
314        {
315            attrs.env = None;
316        }
317    }
318
319    validate_merge_strategy(&attrs, &field.ty)?;
320    validate_validation_attrs(&attrs, &field_ident)?;
321
322    let field_type = field.ty;
323    let metadata_ty = metadata_target_type(&field_type);
324    let canonical_name_lit = LitStr::new(&serde_attrs.canonical_name, field_ident.span());
325    let alias_lits = serde_attrs
326        .aliases
327        .iter()
328        .map(|alias| LitStr::new(alias, field_ident.span()))
329        .collect::<Vec<_>>();
330
331    if serde_attrs.flatten {
332        return Ok(vec![quote! {
333            #accumulator.extend(<#metadata_ty as ::tier::TierMetadata>::metadata());
334        }]);
335    }
336
337    let nested_metadata = if attrs.leaf {
338        quote! { ::tier::ConfigMetadata::new() }
339    } else {
340        quote! { <#metadata_ty as ::tier::TierMetadata>::metadata() }
341    };
342
343    Ok(vec![
344        quote! {
345            #accumulator.extend(::tier::metadata::prefixed_metadata(
346                #canonical_name_lit,
347                ::std::vec![#(::std::string::String::from(#alias_lits)),*],
348                #nested_metadata,
349            ));
350        },
351        direct_field_metadata_tokens(
352            accumulator,
353            &canonical_name_lit,
354            &alias_lits,
355            &serde_attrs,
356            &attrs,
357            is_secret_type(metadata_ty),
358        )?,
359    ])
360}
361
362fn expand_newtype_struct_metadata(
363    fields: FieldsUnnamed,
364    accumulator: &proc_macro2::Ident,
365) -> syn::Result<Vec<proc_macro2::TokenStream>> {
366    if fields.unnamed.len() != 1 {
367        return Err(syn::Error::new_spanned(
368            fields,
369            "TierConfig only supports tuple structs with exactly one field",
370        ));
371    }
372
373    let field = fields.unnamed.into_iter().next().expect("single field");
374    if parse_tier_attrs(&field.attrs)?.has_any() || has_field_naming_attrs(&field.attrs)? {
375        return Err(syn::Error::new_spanned(
376            field,
377            "tuple struct wrappers cannot use field-level tier or serde naming attributes",
378        ));
379    }
380
381    let metadata_ty = metadata_target_type(&field.ty);
382    Ok(vec![quote! {
383        #accumulator.extend(<#metadata_ty as ::tier::TierMetadata>::metadata());
384    }])
385}
386
387fn expand_newtype_variant_metadata(
388    fields: FieldsUnnamed,
389    representation: &EnumRepresentation,
390    span: proc_macro2::Span,
391    accumulator: &proc_macro2::Ident,
392) -> syn::Result<Vec<proc_macro2::TokenStream>> {
393    if fields.unnamed.len() != 1 {
394        return Err(syn::Error::new(
395            span,
396            "TierConfig only supports enum tuple variants with exactly one field",
397        ));
398    }
399
400    if matches!(representation, EnumRepresentation::Internal { .. }) {
401        return Err(syn::Error::new(
402            span,
403            "internally tagged enums with tuple variants are not supported by TierConfig metadata",
404        ));
405    }
406
407    let field = fields.unnamed.into_iter().next().expect("single field");
408    if parse_tier_attrs(&field.attrs)?.has_any() || has_field_naming_attrs(&field.attrs)? {
409        return Err(syn::Error::new_spanned(
410            field,
411            "tuple enum variants cannot use field-level tier or serde naming attributes",
412        ));
413    }
414
415    let metadata_ty = metadata_target_type(&field.ty);
416    Ok(vec![quote! {
417        #accumulator.extend(<#metadata_ty as ::tier::TierMetadata>::metadata());
418    }])
419}
420
421fn ensure_struct_patch_container_attrs(container_attrs: &SerdeContainerAttrs) -> syn::Result<()> {
422    if container_attrs.rename_all_fields_serialize.is_some()
423        || container_attrs.rename_all_fields_deserialize.is_some()
424        || container_attrs.tag.is_some()
425        || container_attrs.content.is_some()
426        || container_attrs.untagged
427    {
428        return Err(syn::Error::new(
429            proc_macro2::Span::call_site(),
430            "TierPatch only supports struct-style serde container attributes",
431        ));
432    }
433
434    Ok(())
435}
436
437fn expand_patch_fields_metadata(
438    fields: FieldsNamed,
439    context: SerdeFieldContext,
440) -> syn::Result<Vec<proc_macro2::TokenStream>> {
441    let mut field_tokens = Vec::new();
442
443    for field in fields.named {
444        field_tokens.push(expand_patch_field_metadata(field, context)?);
445    }
446
447    Ok(field_tokens)
448}
449
450fn expand_patch_field_metadata(
451    field: Field,
452    context: SerdeFieldContext,
453) -> syn::Result<proc_macro2::TokenStream> {
454    let field_ident = field.ident.expect("named field");
455    let serde_attrs = parse_serde_field_attrs(&field.attrs, &field_ident, context)?;
456    let attrs = parse_patch_attrs(&field.attrs)?;
457
458    if serde_attrs.skip_metadata {
459        if attrs.path.is_some() || attrs.path_expr.is_some() || attrs.nested {
460            return Err(syn::Error::new_spanned(
461                field_ident,
462                "skipped fields cannot use tier patch attributes",
463            ));
464        }
465        return Ok(quote! {});
466    }
467
468    if attrs.path.is_some() && attrs.path_expr.is_some() {
469        return Err(syn::Error::new_spanned(
470            field_ident,
471            "patch fields must use either tier(path = ...) or tier(path_expr = ...), not both",
472        ));
473    }
474
475    if serde_attrs.flatten && (attrs.path.is_some() || attrs.path_expr.is_some()) {
476        return Err(syn::Error::new_spanned(
477            field_ident,
478            "flattened patch fields cannot override their tier path",
479        ));
480    }
481
482    let path_expr = if serde_attrs.flatten {
483        quote! { ::std::string::String::from(__tier_prefix) }
484    } else if let Some(path_expr) = attrs.path_expr {
485        quote! { ::tier::patch::join_patch_prefix(__tier_prefix, #path_expr) }
486    } else {
487        let default_path = attrs
488            .path
489            .clone()
490            .unwrap_or_else(|| serde_attrs.canonical_name.clone());
491        let path_lit = LitStr::new(&default_path, field_ident.span());
492        quote! { ::tier::patch::join_patch_prefix(__tier_prefix, #path_lit) }
493    };
494    let field_access = quote! { &self.#field_ident };
495
496    if serde_attrs.flatten || attrs.nested {
497        return Ok(generate_nested_patch_tokens(
498            &field.ty,
499            field_access,
500            path_expr,
501        ));
502    }
503
504    Ok(generate_leaf_patch_tokens(
505        &field.ty,
506        field_access,
507        path_expr,
508    ))
509}
510
511fn generate_nested_patch_tokens(
512    field_ty: &Type,
513    field_access: proc_macro2::TokenStream,
514    path_expr: proc_macro2::TokenStream,
515) -> proc_macro2::TokenStream {
516    if option_inner_type(field_ty).is_some() {
517        quote! {
518            if let ::std::option::Option::Some(value) = #field_access {
519                let __tier_path = #path_expr;
520                ::tier::TierPatch::write_layer(value, __tier_builder, &__tier_path)?;
521            }
522        }
523    } else if patch_inner_type(field_ty).is_some() {
524        quote! {
525            if let ::std::option::Option::Some(value) = #field_access.as_ref() {
526                let __tier_path = #path_expr;
527                ::tier::TierPatch::write_layer(value, __tier_builder, &__tier_path)?;
528            }
529        }
530    } else {
531        quote! {
532            {
533                let __tier_path = #path_expr;
534                ::tier::TierPatch::write_layer(#field_access, __tier_builder, &__tier_path)?;
535            }
536        }
537    }
538}
539
540fn generate_leaf_patch_tokens(
541    field_ty: &Type,
542    field_access: proc_macro2::TokenStream,
543    path_expr: proc_macro2::TokenStream,
544) -> proc_macro2::TokenStream {
545    if option_inner_type(field_ty).is_some() {
546        quote! {
547            if let ::std::option::Option::Some(value) = #field_access {
548                let __tier_path = #path_expr;
549                __tier_builder.insert_serialized(&__tier_path, value)?;
550            }
551        }
552    } else if patch_inner_type(field_ty).is_some() {
553        quote! {
554            if let ::std::option::Option::Some(value) = #field_access.as_ref() {
555                let __tier_path = #path_expr;
556                __tier_builder.insert_serialized(&__tier_path, value)?;
557            }
558        }
559    } else {
560        quote! {
561            {
562                let __tier_path = #path_expr;
563                __tier_builder.insert_serialized(&__tier_path, #field_access)?;
564            }
565        }
566    }
567}
568
569#[derive(Debug, Default)]
570struct TierAttrs {
571    secret: bool,
572    leaf: bool,
573    sources: Vec<TierSourceKind>,
574    env: Option<String>,
575    doc: Option<String>,
576    example: Option<String>,
577    deprecated: Option<String>,
578    merge: Option<String>,
579    non_empty: bool,
580    min: Option<NumericLiteral>,
581    max: Option<NumericLiteral>,
582    min_length: Option<usize>,
583    max_length: Option<usize>,
584    min_items: Option<usize>,
585    max_items: Option<usize>,
586    min_properties: Option<usize>,
587    max_properties: Option<usize>,
588    multiple_of: Option<NumericLiteral>,
589    pattern: Option<String>,
590    unique_items: bool,
591    one_of: Vec<Expr>,
592    hostname: bool,
593    url: bool,
594    email: bool,
595    ip_addr: bool,
596    socket_addr: bool,
597    absolute_path: bool,
598    env_decode: Option<String>,
599}
600
601impl TierAttrs {
602    fn has_any(&self) -> bool {
603        self.secret
604            || self.leaf
605            || !self.sources.is_empty()
606            || self.env.is_some()
607            || self.doc.is_some()
608            || self.example.is_some()
609            || self.deprecated.is_some()
610            || self.merge.is_some()
611            || self.non_empty
612            || self.min.is_some()
613            || self.max.is_some()
614            || self.min_length.is_some()
615            || self.max_length.is_some()
616            || self.min_items.is_some()
617            || self.max_items.is_some()
618            || self.min_properties.is_some()
619            || self.max_properties.is_some()
620            || self.multiple_of.is_some()
621            || self.pattern.is_some()
622            || self.unique_items
623            || !self.one_of.is_empty()
624            || self.hostname
625            || self.url
626            || self.email
627            || self.ip_addr
628            || self.socket_addr
629            || self.absolute_path
630            || self.env_decode.is_some()
631    }
632}
633
634#[derive(Debug, Default)]
635struct PatchAttrs {
636    path: Option<String>,
637    path_expr: Option<Expr>,
638    nested: bool,
639}
640
641#[derive(Debug, Default)]
642struct TierContainerAttrs {
643    checks: Vec<ContainerValidationCheck>,
644}
645
646#[derive(Debug, Clone)]
647struct NumericLiteral {
648    tokens: proc_macro2::TokenStream,
649    value: f64,
650}
651
652#[derive(Debug, Clone, Copy)]
653enum TierSourceKind {
654    Default,
655    File,
656    Environment,
657    Arguments,
658    Normalization,
659    Custom,
660}
661
662impl TierSourceKind {
663    fn parse(value: &str, span: proc_macro2::Span) -> syn::Result<Self> {
664        match value {
665            "default" => Ok(Self::Default),
666            "file" => Ok(Self::File),
667            "env" | "environment" => Ok(Self::Environment),
668            "cli" | "arguments" => Ok(Self::Arguments),
669            "normalize" | "normalization" => Ok(Self::Normalization),
670            "custom" => Ok(Self::Custom),
671            _ => Err(syn::Error::new(
672                span,
673                "unsupported tier source kind, expected default|file|env|cli|normalize|custom",
674            )),
675        }
676    }
677
678    fn tokens(self) -> proc_macro2::TokenStream {
679        match self {
680            Self::Default => quote! { ::tier::SourceKind::Default },
681            Self::File => quote! { ::tier::SourceKind::File },
682            Self::Environment => quote! { ::tier::SourceKind::Environment },
683            Self::Arguments => quote! { ::tier::SourceKind::Arguments },
684            Self::Normalization => quote! { ::tier::SourceKind::Normalization },
685            Self::Custom => quote! { ::tier::SourceKind::Custom },
686        }
687    }
688}
689
690#[derive(Debug, Clone)]
691enum ContainerPathSpec {
692    String(String),
693    Expr(Expr),
694}
695
696#[derive(Debug, Clone)]
697enum ContainerPathListSpec {
698    Strings(Vec<String>),
699    Exprs(Vec<Expr>),
700}
701
702#[derive(Debug, Clone)]
703enum ContainerValidationCheck {
704    AtLeastOneOf(ContainerPathListSpec),
705    ExactlyOneOf(ContainerPathListSpec),
706    MutuallyExclusive(ContainerPathListSpec),
707    RequiredWith {
708        path: ContainerPathSpec,
709        requires: ContainerPathListSpec,
710    },
711    RequiredIf {
712        path: ContainerPathSpec,
713        equals: Expr,
714        requires: ContainerPathListSpec,
715    },
716}
717
718#[derive(Debug, Default)]
719struct SerdeContainerAttrs {
720    rename_all_serialize: Option<RenameRule>,
721    rename_all_deserialize: Option<RenameRule>,
722    rename_all_fields_serialize: Option<RenameRule>,
723    rename_all_fields_deserialize: Option<RenameRule>,
724    default_fields: bool,
725    tag: Option<String>,
726    content: Option<String>,
727    untagged: bool,
728}
729
730#[derive(Debug, Clone, Copy, Default)]
731struct SerdeFieldContext {
732    rename_serialize: Option<RenameRule>,
733    rename_deserialize: Option<RenameRule>,
734    default_fields: bool,
735}
736
737impl SerdeFieldContext {
738    fn for_struct(container_attrs: &SerdeContainerAttrs) -> Self {
739        Self {
740            rename_serialize: container_attrs.rename_all_serialize,
741            rename_deserialize: container_attrs.rename_all_deserialize,
742            default_fields: container_attrs.default_fields,
743        }
744    }
745
746    fn for_enum_variant_fields(container_attrs: &SerdeContainerAttrs) -> Self {
747        Self {
748            rename_serialize: container_attrs.rename_all_fields_serialize,
749            rename_deserialize: container_attrs.rename_all_fields_deserialize,
750            default_fields: false,
751        }
752    }
753}
754
755#[derive(Debug, Default)]
756struct SerdeFieldAttrs {
757    canonical_name: String,
758    aliases: Vec<String>,
759    flatten: bool,
760    skip_metadata: bool,
761    has_default: bool,
762}
763
764#[derive(Debug, Default)]
765struct SerdeVariantAttrs {
766    canonical_name: String,
767    aliases: Vec<String>,
768    skip_metadata: bool,
769}
770
771#[derive(Debug, Default)]
772struct NonExternalFieldConflicts {
773    skipped_fields: HashSet<String>,
774    skipped_aliases: HashSet<String>,
775    skipped_envs: HashSet<String>,
776}
777
778#[derive(Debug, Clone)]
779enum EnumRepresentation {
780    External,
781    Internal { tag: String },
782    Adjacent { tag: String, content: String },
783    Untagged,
784}
785
786impl EnumRepresentation {
787    fn tag_field(&self) -> Option<&str> {
788        match self {
789            Self::Internal { tag } => Some(tag.as_str()),
790            Self::Adjacent { tag, .. } => Some(tag.as_str()),
791            Self::External | Self::Untagged => None,
792        }
793    }
794}
795
796#[derive(Debug, Clone, Copy, PartialEq, Eq)]
797enum RenameRule {
798    Lower,
799    Upper,
800    Pascal,
801    Camel,
802    Snake,
803    ScreamingSnake,
804    Kebab,
805    ScreamingKebab,
806}
807
808impl RenameRule {
809    fn parse(value: &str, span: proc_macro2::Span) -> syn::Result<Self> {
810        match value {
811            "lowercase" => Ok(Self::Lower),
812            "UPPERCASE" => Ok(Self::Upper),
813            "PascalCase" => Ok(Self::Pascal),
814            "camelCase" => Ok(Self::Camel),
815            "snake_case" => Ok(Self::Snake),
816            "SCREAMING_SNAKE_CASE" => Ok(Self::ScreamingSnake),
817            "kebab-case" => Ok(Self::Kebab),
818            "SCREAMING-KEBAB-CASE" => Ok(Self::ScreamingKebab),
819            _ => Err(syn::Error::new(
820                span,
821                "unsupported serde rename rule for TierConfig",
822            )),
823        }
824    }
825
826    fn apply_to_field(self, value: &str) -> String {
827        match self {
828            Self::Lower | Self::Snake => value.to_owned(),
829            Self::Upper | Self::ScreamingSnake => value.to_ascii_uppercase(),
830            Self::Pascal => {
831                let mut output = String::new();
832                let mut capitalize = true;
833                for ch in value.chars() {
834                    if ch == '_' {
835                        capitalize = true;
836                    } else if capitalize {
837                        output.push(ch.to_ascii_uppercase());
838                        capitalize = false;
839                    } else {
840                        output.push(ch);
841                    }
842                }
843                output
844            }
845            Self::Camel => {
846                let pascal = Self::Pascal.apply_to_field(value);
847                lowercase_first_char(&pascal)
848            }
849            Self::Kebab => value.replace('_', "-"),
850            Self::ScreamingKebab => value.replace('_', "-").to_ascii_uppercase(),
851        }
852    }
853
854    fn apply_to_variant(self, value: &str) -> String {
855        match self {
856            Self::Lower => value.to_ascii_lowercase(),
857            Self::Upper => value.to_ascii_uppercase(),
858            Self::Pascal => value.to_owned(),
859            Self::Camel => lowercase_first_char(value),
860            Self::Snake => {
861                let mut output = String::new();
862                for (index, ch) in value.char_indices() {
863                    if index > 0 && ch.is_uppercase() {
864                        output.push('_');
865                    }
866                    output.push(ch.to_ascii_lowercase());
867                }
868                output
869            }
870            Self::ScreamingSnake => Self::Snake.apply_to_variant(value).to_ascii_uppercase(),
871            Self::Kebab => Self::Snake.apply_to_variant(value).replace('_', "-"),
872            Self::ScreamingKebab => Self::Kebab.apply_to_variant(value).to_ascii_uppercase(),
873        }
874    }
875}
876
877fn lowercase_first_char(value: &str) -> String {
878    let mut chars = value.chars();
879    let Some(first) = chars.next() else {
880        return String::new();
881    };
882
883    let mut output = first.to_ascii_lowercase().to_string();
884    output.push_str(chars.as_str());
885    output
886}
887
888fn parse_tier_attrs(attributes: &[Attribute]) -> syn::Result<TierAttrs> {
889    let mut attrs = TierAttrs::default();
890    for attribute in attributes {
891        if !attribute.path().is_ident("tier") {
892            continue;
893        }
894        attribute.parse_nested_meta(|meta| {
895            if meta.path.is_ident("secret") {
896                attrs.secret = true;
897                return Ok(());
898            }
899            if meta.path.is_ident("leaf") {
900                attrs.leaf = true;
901                consume_unused_meta(meta)?;
902                return Ok(());
903            }
904            if meta.path.is_ident("sources") {
905                attrs.sources = parse_source_kind_list(meta)?;
906                return Ok(());
907            }
908            if meta.path.is_ident("env") {
909                attrs.env = Some(parse_string_value(meta)?);
910                return Ok(());
911            }
912            if meta.path.is_ident("doc") {
913                attrs.doc = Some(parse_string_value(meta)?);
914                return Ok(());
915            }
916            if meta.path.is_ident("example") {
917                attrs.example = Some(parse_string_value(meta)?);
918                return Ok(());
919            }
920            if meta.path.is_ident("deprecated") {
921                attrs.deprecated = Some(if meta.input.peek(syn::Token![=]) {
922                    parse_string_value(meta)?
923                } else {
924                    "this field is deprecated".to_owned()
925                });
926                return Ok(());
927            }
928            if meta.path.is_ident("merge") {
929                attrs.merge = Some(parse_string_value(meta)?);
930                return Ok(());
931            }
932            if meta.path.is_ident("non_empty") {
933                attrs.non_empty = true;
934                consume_unused_meta(meta)?;
935                return Ok(());
936            }
937            if meta.path.is_ident("min") {
938                attrs.min = Some(parse_numeric_literal(meta)?);
939                return Ok(());
940            }
941            if meta.path.is_ident("max") {
942                attrs.max = Some(parse_numeric_literal(meta)?);
943                return Ok(());
944            }
945            if meta.path.is_ident("min_length") {
946                attrs.min_length = Some(parse_usize_value(meta)?);
947                return Ok(());
948            }
949            if meta.path.is_ident("max_length") {
950                attrs.max_length = Some(parse_usize_value(meta)?);
951                return Ok(());
952            }
953            if meta.path.is_ident("min_items") {
954                attrs.min_items = Some(parse_usize_value(meta)?);
955                return Ok(());
956            }
957            if meta.path.is_ident("max_items") {
958                attrs.max_items = Some(parse_usize_value(meta)?);
959                return Ok(());
960            }
961            if meta.path.is_ident("min_properties") {
962                attrs.min_properties = Some(parse_usize_value(meta)?);
963                return Ok(());
964            }
965            if meta.path.is_ident("max_properties") {
966                attrs.max_properties = Some(parse_usize_value(meta)?);
967                return Ok(());
968            }
969            if meta.path.is_ident("multiple_of") {
970                attrs.multiple_of = Some(parse_numeric_literal(meta)?);
971                return Ok(());
972            }
973            if meta.path.is_ident("pattern") {
974                attrs.pattern = Some(parse_string_value(meta)?);
975                return Ok(());
976            }
977            if meta.path.is_ident("unique_items") {
978                attrs.unique_items = true;
979                consume_unused_meta(meta)?;
980                return Ok(());
981            }
982            if meta.path.is_ident("one_of") {
983                attrs.one_of = parse_literal_expr_list(meta)?;
984                return Ok(());
985            }
986            if meta.path.is_ident("hostname") {
987                attrs.hostname = true;
988                consume_unused_meta(meta)?;
989                return Ok(());
990            }
991            if meta.path.is_ident("url") {
992                attrs.url = true;
993                consume_unused_meta(meta)?;
994                return Ok(());
995            }
996            if meta.path.is_ident("email") {
997                attrs.email = true;
998                consume_unused_meta(meta)?;
999                return Ok(());
1000            }
1001            if meta.path.is_ident("ip_addr") {
1002                attrs.ip_addr = true;
1003                consume_unused_meta(meta)?;
1004                return Ok(());
1005            }
1006            if meta.path.is_ident("socket_addr") {
1007                attrs.socket_addr = true;
1008                consume_unused_meta(meta)?;
1009                return Ok(());
1010            }
1011            if meta.path.is_ident("absolute_path") {
1012                attrs.absolute_path = true;
1013                consume_unused_meta(meta)?;
1014                return Ok(());
1015            }
1016            if meta.path.is_ident("env_decode") {
1017                attrs.env_decode = Some(parse_string_value(meta)?);
1018                return Ok(());
1019            }
1020            Err(meta.error("unsupported tier attribute"))
1021        })?;
1022    }
1023    Ok(attrs)
1024}
1025
1026fn parse_patch_attrs(attributes: &[Attribute]) -> syn::Result<PatchAttrs> {
1027    let mut attrs = PatchAttrs::default();
1028    for attribute in attributes {
1029        if !attribute.path().is_ident("tier") {
1030            continue;
1031        }
1032        attribute.parse_nested_meta(|meta| {
1033            if meta.path.is_ident("path") {
1034                attrs.path = Some(parse_string_value(meta)?);
1035                return Ok(());
1036            }
1037            if meta.path.is_ident("path_expr") {
1038                attrs.path_expr = Some(parse_expr_value(meta)?);
1039                return Ok(());
1040            }
1041            if meta.path.is_ident("nested") {
1042                attrs.nested = true;
1043                consume_unused_meta(meta)?;
1044                return Ok(());
1045            }
1046            Err(meta.error("unsupported tier patch attribute"))
1047        })?;
1048    }
1049    Ok(attrs)
1050}
1051
1052fn parse_tier_container_attrs(attributes: &[Attribute]) -> syn::Result<TierContainerAttrs> {
1053    let mut attrs = TierContainerAttrs::default();
1054
1055    for attribute in attributes {
1056        if !attribute.path().is_ident("tier") {
1057            continue;
1058        }
1059
1060        attribute.parse_nested_meta(|meta| {
1061            if meta.path.is_ident("at_least_one_of") {
1062                attrs.checks.push(ContainerValidationCheck::AtLeastOneOf(
1063                    ContainerPathListSpec::Strings(parse_string_list_call(meta)?),
1064                ));
1065                return Ok(());
1066            }
1067            if meta.path.is_ident("at_least_one_of_expr") {
1068                attrs.checks.push(ContainerValidationCheck::AtLeastOneOf(
1069                    ContainerPathListSpec::Exprs(parse_expr_list_call(meta)?),
1070                ));
1071                return Ok(());
1072            }
1073            if meta.path.is_ident("exactly_one_of") {
1074                attrs.checks.push(ContainerValidationCheck::ExactlyOneOf(
1075                    ContainerPathListSpec::Strings(parse_string_list_call(meta)?),
1076                ));
1077                return Ok(());
1078            }
1079            if meta.path.is_ident("exactly_one_of_expr") {
1080                attrs.checks.push(ContainerValidationCheck::ExactlyOneOf(
1081                    ContainerPathListSpec::Exprs(parse_expr_list_call(meta)?),
1082                ));
1083                return Ok(());
1084            }
1085            if meta.path.is_ident("mutually_exclusive") {
1086                attrs
1087                    .checks
1088                    .push(ContainerValidationCheck::MutuallyExclusive(
1089                        ContainerPathListSpec::Strings(parse_string_list_call(meta)?),
1090                    ));
1091                return Ok(());
1092            }
1093            if meta.path.is_ident("mutually_exclusive_expr") {
1094                attrs
1095                    .checks
1096                    .push(ContainerValidationCheck::MutuallyExclusive(
1097                        ContainerPathListSpec::Exprs(parse_expr_list_call(meta)?),
1098                    ));
1099                return Ok(());
1100            }
1101            if meta.path.is_ident("required_with") {
1102                attrs
1103                    .checks
1104                    .push(parse_required_with_container_check(meta)?);
1105                return Ok(());
1106            }
1107            if meta.path.is_ident("required_if") {
1108                attrs.checks.push(parse_required_if_container_check(meta)?);
1109                return Ok(());
1110            }
1111            Err(meta.error("unsupported tier container attribute"))
1112        })?;
1113    }
1114
1115    Ok(attrs)
1116}
1117
1118fn parse_expr_value(meta: syn::meta::ParseNestedMeta<'_>) -> syn::Result<Expr> {
1119    meta.value()?.parse()
1120}
1121
1122fn parse_serde_container_attrs(attributes: &[Attribute]) -> syn::Result<SerdeContainerAttrs> {
1123    let mut attrs = SerdeContainerAttrs::default();
1124    for attribute in attributes {
1125        if !attribute.path().is_ident("serde") {
1126            continue;
1127        }
1128
1129        attribute.parse_nested_meta(|meta| {
1130            if meta.path.is_ident("rename_all") {
1131                parse_rename_all_meta(
1132                    meta,
1133                    &mut attrs.rename_all_serialize,
1134                    &mut attrs.rename_all_deserialize,
1135                )?;
1136                return Ok(());
1137            }
1138            if meta.path.is_ident("rename_all_fields") {
1139                parse_rename_all_meta(
1140                    meta,
1141                    &mut attrs.rename_all_fields_serialize,
1142                    &mut attrs.rename_all_fields_deserialize,
1143                )?;
1144                return Ok(());
1145            }
1146            if meta.path.is_ident("default") {
1147                attrs.default_fields = true;
1148                consume_unused_meta(meta)?;
1149                return Ok(());
1150            }
1151            if meta.path.is_ident("tag") {
1152                attrs.tag = Some(parse_string_value(meta)?);
1153                return Ok(());
1154            }
1155            if meta.path.is_ident("content") {
1156                attrs.content = Some(parse_string_value(meta)?);
1157                return Ok(());
1158            }
1159            if meta.path.is_ident("untagged") {
1160                attrs.untagged = true;
1161                consume_unused_meta(meta)?;
1162                return Ok(());
1163            }
1164            consume_unused_meta(meta)?;
1165            Ok(())
1166        })?;
1167    }
1168
1169    Ok(attrs)
1170}
1171
1172fn parse_serde_field_attrs(
1173    attributes: &[Attribute],
1174    field_ident: &syn::Ident,
1175    context: SerdeFieldContext,
1176) -> syn::Result<SerdeFieldAttrs> {
1177    let base_name = unraw(field_ident);
1178    let mut rename_serialize = None;
1179    let mut rename_deserialize = None;
1180    let mut aliases = Vec::new();
1181    let mut flatten = false;
1182    let mut skip_metadata = false;
1183    let mut has_default = context.default_fields;
1184
1185    for attribute in attributes {
1186        if !attribute.path().is_ident("serde") {
1187            continue;
1188        }
1189
1190        attribute.parse_nested_meta(|meta| {
1191            if meta.path.is_ident("rename") {
1192                parse_rename_meta(meta, &mut rename_serialize, &mut rename_deserialize)?;
1193                return Ok(());
1194            }
1195            if meta.path.is_ident("alias") {
1196                aliases.push(parse_string_value(meta)?);
1197                return Ok(());
1198            }
1199            if meta.path.is_ident("flatten") {
1200                flatten = true;
1201                return Ok(());
1202            }
1203            if meta.path.is_ident("default") {
1204                has_default = true;
1205                consume_unused_meta(meta)?;
1206                return Ok(());
1207            }
1208            if meta.path.is_ident("skip") || meta.path.is_ident("skip_deserializing") {
1209                skip_metadata = true;
1210                return Ok(());
1211            }
1212            consume_unused_meta(meta)?;
1213            Ok(())
1214        })?;
1215    }
1216
1217    let has_explicit_rename = rename_serialize.is_some() || rename_deserialize.is_some();
1218
1219    let canonical_name = rename_serialize
1220        .or_else(|| {
1221            context
1222                .rename_serialize
1223                .map(|rule| rule.apply_to_field(&base_name))
1224        })
1225        .unwrap_or_else(|| base_name.clone());
1226    let deserialize_name = rename_deserialize
1227        .or_else(|| {
1228            context
1229                .rename_deserialize
1230                .map(|rule| rule.apply_to_field(&base_name))
1231        })
1232        .unwrap_or_else(|| base_name.clone());
1233
1234    if deserialize_name != canonical_name {
1235        aliases.push(deserialize_name);
1236    }
1237
1238    if flatten && (!aliases.is_empty() || has_explicit_rename) {
1239        return Err(syn::Error::new_spanned(
1240            field_ident,
1241            "flattened fields cannot use serde rename or alias attributes",
1242        ));
1243    }
1244
1245    aliases.retain(|alias| alias != &canonical_name);
1246    aliases.sort();
1247    aliases.dedup();
1248
1249    Ok(SerdeFieldAttrs {
1250        canonical_name,
1251        aliases,
1252        flatten,
1253        skip_metadata,
1254        has_default,
1255    })
1256}
1257
1258fn parse_serde_variant_attrs(
1259    attributes: &[Attribute],
1260    variant_ident: &syn::Ident,
1261    container_attrs: &SerdeContainerAttrs,
1262) -> syn::Result<SerdeVariantAttrs> {
1263    let base_name = unraw(variant_ident);
1264    let mut rename_serialize = None;
1265    let mut rename_deserialize = None;
1266    let mut aliases = Vec::new();
1267    let mut skip_metadata = false;
1268
1269    for attribute in attributes {
1270        if !attribute.path().is_ident("serde") {
1271            continue;
1272        }
1273
1274        attribute.parse_nested_meta(|meta| {
1275            if meta.path.is_ident("rename") {
1276                parse_rename_meta(meta, &mut rename_serialize, &mut rename_deserialize)?;
1277                return Ok(());
1278            }
1279            if meta.path.is_ident("alias") {
1280                aliases.push(parse_string_value(meta)?);
1281                return Ok(());
1282            }
1283            if meta.path.is_ident("skip")
1284                || meta.path.is_ident("skip_deserializing")
1285                || meta.path.is_ident("other")
1286            {
1287                skip_metadata = true;
1288                consume_unused_meta(meta)?;
1289                return Ok(());
1290            }
1291            consume_unused_meta(meta)?;
1292            Ok(())
1293        })?;
1294    }
1295
1296    let canonical_name = rename_serialize
1297        .or_else(|| {
1298            container_attrs
1299                .rename_all_serialize
1300                .map(|rule| rule.apply_to_variant(&base_name))
1301        })
1302        .unwrap_or_else(|| base_name.clone());
1303    let deserialize_name = rename_deserialize
1304        .or_else(|| {
1305            container_attrs
1306                .rename_all_deserialize
1307                .map(|rule| rule.apply_to_variant(&base_name))
1308        })
1309        .unwrap_or_else(|| base_name.clone());
1310
1311    if deserialize_name != canonical_name {
1312        aliases.push(deserialize_name);
1313    }
1314
1315    aliases.retain(|alias| alias != &canonical_name);
1316    aliases.sort();
1317    aliases.dedup();
1318
1319    Ok(SerdeVariantAttrs {
1320        canonical_name,
1321        aliases,
1322        skip_metadata,
1323    })
1324}
1325
1326fn ensure_struct_container_attrs(container_attrs: &SerdeContainerAttrs) -> syn::Result<()> {
1327    if container_attrs.rename_all_fields_serialize.is_some()
1328        || container_attrs.rename_all_fields_deserialize.is_some()
1329    {
1330        return Err(syn::Error::new(
1331            proc_macro2::Span::call_site(),
1332            "serde(rename_all_fields = ...) is only supported on enums",
1333        ));
1334    }
1335    if container_attrs.tag.is_some()
1336        || container_attrs.content.is_some()
1337        || container_attrs.untagged
1338    {
1339        return Err(syn::Error::new(
1340            proc_macro2::Span::call_site(),
1341            "serde enum tagging attributes are not supported on structs",
1342        ));
1343    }
1344    Ok(())
1345}
1346
1347fn enum_representation(container_attrs: &SerdeContainerAttrs) -> syn::Result<EnumRepresentation> {
1348    if container_attrs.untagged && container_attrs.tag.is_some() {
1349        return Err(syn::Error::new(
1350            proc_macro2::Span::call_site(),
1351            "serde(untagged) cannot be combined with serde(tag = ...)",
1352        ));
1353    }
1354    if container_attrs.untagged && container_attrs.content.is_some() {
1355        return Err(syn::Error::new(
1356            proc_macro2::Span::call_site(),
1357            "serde(untagged) cannot be combined with serde(content = ...)",
1358        ));
1359    }
1360    if container_attrs.content.is_some() && container_attrs.tag.is_none() {
1361        return Err(syn::Error::new(
1362            proc_macro2::Span::call_site(),
1363            "serde(content = ...) requires serde(tag = ...)",
1364        ));
1365    }
1366
1367    if container_attrs.untagged {
1368        return Ok(EnumRepresentation::Untagged);
1369    }
1370
1371    match (&container_attrs.tag, &container_attrs.content) {
1372        (Some(tag), Some(content)) => Ok(EnumRepresentation::Adjacent {
1373            tag: tag.clone(),
1374            content: content.clone(),
1375        }),
1376        (Some(tag), None) => Ok(EnumRepresentation::Internal { tag: tag.clone() }),
1377        (None, None) => Ok(EnumRepresentation::External),
1378        (None, Some(_)) => unreachable!("validated above"),
1379    }
1380}
1381
1382fn non_external_variant_field_conflicts(
1383    data_enum: &DataEnum,
1384    container_attrs: &SerdeContainerAttrs,
1385) -> syn::Result<NonExternalFieldConflicts> {
1386    let representation = enum_representation(container_attrs)?;
1387    if matches!(representation, EnumRepresentation::External) {
1388        return Ok(NonExternalFieldConflicts::default());
1389    }
1390
1391    let context = SerdeFieldContext::for_enum_variant_fields(container_attrs);
1392    let mut counts = HashMap::<String, usize>::new();
1393    let mut canonical_names = HashSet::new();
1394    let mut alias_owners = HashMap::<String, HashSet<String>>::new();
1395    let mut env_owners = HashMap::<String, HashSet<String>>::new();
1396
1397    for variant in &data_enum.variants {
1398        let variant_attrs =
1399            parse_serde_variant_attrs(&variant.attrs, &variant.ident, container_attrs)?;
1400        if variant_attrs.skip_metadata {
1401            continue;
1402        }
1403
1404        let Fields::Named(fields) = &variant.fields else {
1405            continue;
1406        };
1407
1408        let mut seen = HashSet::new();
1409        for field in &fields.named {
1410            let Some(field_ident) = &field.ident else {
1411                continue;
1412            };
1413            let serde_attrs = parse_serde_field_attrs(&field.attrs, field_ident, context)?;
1414            if serde_attrs.skip_metadata || serde_attrs.flatten {
1415                continue;
1416            }
1417            let tier_attrs = parse_tier_attrs(&field.attrs)?;
1418            let canonical_name = serde_attrs.canonical_name.clone();
1419            if seen.insert(canonical_name.clone()) {
1420                canonical_names.insert(canonical_name.clone());
1421                *counts.entry(canonical_name.clone()).or_default() += 1;
1422            }
1423            for alias in serde_attrs.aliases {
1424                alias_owners
1425                    .entry(alias)
1426                    .or_default()
1427                    .insert(canonical_name.clone());
1428            }
1429            if let Some(env) = tier_attrs.env {
1430                env_owners
1431                    .entry(env)
1432                    .or_default()
1433                    .insert(canonical_name.clone());
1434            }
1435        }
1436    }
1437
1438    let skipped_fields = counts
1439        .into_iter()
1440        .filter_map(|(path, count)| (count > 1).then_some(path))
1441        .collect::<HashSet<_>>();
1442
1443    let skipped_aliases = alias_owners
1444        .into_iter()
1445        .filter_map(|(alias, owners)| {
1446            (owners.len() > 1 || canonical_names.contains(&alias)).then_some(alias)
1447        })
1448        .collect::<HashSet<_>>();
1449
1450    let skipped_envs = env_owners
1451        .into_iter()
1452        .filter_map(|(env, owners)| (owners.len() > 1).then_some(env))
1453        .collect::<HashSet<_>>();
1454
1455    Ok(NonExternalFieldConflicts {
1456        skipped_fields,
1457        skipped_aliases,
1458        skipped_envs,
1459    })
1460}
1461
1462fn has_field_naming_attrs(attributes: &[Attribute]) -> syn::Result<bool> {
1463    let mut has_naming = false;
1464    for attribute in attributes {
1465        if !attribute.path().is_ident("serde") {
1466            continue;
1467        }
1468
1469        attribute.parse_nested_meta(|meta| {
1470            if meta.path.is_ident("rename")
1471                || meta.path.is_ident("alias")
1472                || meta.path.is_ident("flatten")
1473                || meta.path.is_ident("default")
1474            {
1475                has_naming = true;
1476            }
1477            consume_unused_meta(meta)?;
1478            Ok(())
1479        })?;
1480    }
1481
1482    Ok(has_naming)
1483}
1484
1485fn validate_merge_strategy(attrs: &TierAttrs, ty: &Type) -> syn::Result<()> {
1486    if attrs.merge.as_deref() == Some("append") && !supports_append_strategy(ty) {
1487        return Err(syn::Error::new_spanned(
1488            ty,
1489            "tier(merge = \"append\") requires a Vec<T> or array-like field",
1490        ));
1491    }
1492    Ok(())
1493}
1494
1495fn validate_validation_attrs(attrs: &TierAttrs, field_ident: &syn::Ident) -> syn::Result<()> {
1496    if let (Some(min), Some(max)) = (&attrs.min, &attrs.max)
1497        && min.value > max.value
1498    {
1499        return Err(syn::Error::new_spanned(
1500            field_ident,
1501            "tier(min = ...) cannot be greater than tier(max = ...)",
1502        ));
1503    }
1504
1505    if let (Some(min_length), Some(max_length)) = (attrs.min_length, attrs.max_length)
1506        && min_length > max_length
1507    {
1508        return Err(syn::Error::new_spanned(
1509            field_ident,
1510            "tier(min_length = ...) cannot be greater than tier(max_length = ...)",
1511        ));
1512    }
1513
1514    if let (Some(min_items), Some(max_items)) = (attrs.min_items, attrs.max_items)
1515        && min_items > max_items
1516    {
1517        return Err(syn::Error::new_spanned(
1518            field_ident,
1519            "tier(min_items = ...) cannot be greater than tier(max_items = ...)",
1520        ));
1521    }
1522
1523    if let (Some(min_properties), Some(max_properties)) =
1524        (attrs.min_properties, attrs.max_properties)
1525        && min_properties > max_properties
1526    {
1527        return Err(syn::Error::new_spanned(
1528            field_ident,
1529            "tier(min_properties = ...) cannot be greater than tier(max_properties = ...)",
1530        ));
1531    }
1532
1533    if let Some(multiple_of) = &attrs.multiple_of
1534        && !(multiple_of.value.is_finite() && multiple_of.value > 0.0)
1535    {
1536        return Err(syn::Error::new_spanned(
1537            field_ident,
1538            "tier(multiple_of = ...) must be greater than 0",
1539        ));
1540    }
1541
1542    if attrs.pattern.as_deref() == Some("") {
1543        return Err(syn::Error::new_spanned(
1544            field_ident,
1545            "tier(pattern = ...) cannot be empty",
1546        ));
1547    }
1548
1549    if attrs.one_of.is_empty()
1550        && (attrs.hostname
1551            || attrs.url
1552            || attrs.email
1553            || attrs.ip_addr
1554            || attrs.socket_addr
1555            || attrs.absolute_path)
1556    {
1557        return Ok(());
1558    }
1559
1560    if !attrs.one_of.is_empty() && (attrs.min.is_some() || attrs.max.is_some()) {
1561        return Err(syn::Error::new_spanned(
1562            field_ident,
1563            "tier(one_of(...)) cannot be combined with tier(min = ...) or tier(max = ...)",
1564        ));
1565    }
1566
1567    Ok(())
1568}
1569
1570fn container_check_tokens(attrs: &TierContainerAttrs) -> Vec<proc_macro2::TokenStream> {
1571    attrs
1572        .checks
1573        .iter()
1574        .map(|check| match check {
1575            ContainerValidationCheck::AtLeastOneOf(paths) => {
1576                let paths = container_paths_tokens(paths);
1577                quote! {
1578                    metadata.push_check(::tier::ValidationCheck::AtLeastOneOf {
1579                        paths: #paths,
1580                    });
1581                }
1582            }
1583            ContainerValidationCheck::ExactlyOneOf(paths) => {
1584                let paths = container_paths_tokens(paths);
1585                quote! {
1586                    metadata.push_check(::tier::ValidationCheck::ExactlyOneOf {
1587                        paths: #paths,
1588                    });
1589                }
1590            }
1591            ContainerValidationCheck::MutuallyExclusive(paths) => {
1592                let paths = container_paths_tokens(paths);
1593                quote! {
1594                    metadata.push_check(::tier::ValidationCheck::MutuallyExclusive {
1595                        paths: #paths,
1596                    });
1597                }
1598            }
1599            ContainerValidationCheck::RequiredWith { path, requires } => {
1600                let path = container_path_tokens(path);
1601                let requires = container_paths_tokens(requires);
1602                quote! {
1603                    metadata.push_check(::tier::ValidationCheck::RequiredWith {
1604                        path: #path,
1605                        requires: #requires,
1606                    });
1607                }
1608            }
1609            ContainerValidationCheck::RequiredIf {
1610                path,
1611                equals,
1612                requires,
1613            } => {
1614                let path = container_path_tokens(path);
1615                let requires = container_paths_tokens(requires);
1616                quote! {
1617                    metadata.push_check(::tier::ValidationCheck::RequiredIf {
1618                        path: #path,
1619                        equals: ::tier::ValidationValue::from(#equals),
1620                        requires: #requires,
1621                    });
1622                }
1623            }
1624        })
1625        .collect()
1626}
1627
1628fn container_path_tokens(path: &ContainerPathSpec) -> proc_macro2::TokenStream {
1629    match path {
1630        ContainerPathSpec::String(path) => {
1631            let path = LitStr::new(path, proc_macro2::Span::call_site());
1632            quote! { ::std::string::String::from(#path) }
1633        }
1634        ContainerPathSpec::Expr(path) => quote! { ::std::string::String::from(#path) },
1635    }
1636}
1637
1638fn container_paths_tokens(paths: &ContainerPathListSpec) -> proc_macro2::TokenStream {
1639    match paths {
1640        ContainerPathListSpec::Strings(paths) => {
1641            let paths = paths
1642                .iter()
1643                .map(|path| LitStr::new(path, proc_macro2::Span::call_site()))
1644                .collect::<Vec<_>>();
1645            quote! { ::std::vec![#(::std::string::String::from(#paths)),*] }
1646        }
1647        ContainerPathListSpec::Exprs(paths) => {
1648            quote! { ::std::vec![#(::std::string::String::from(#paths)),*] }
1649        }
1650    }
1651}
1652
1653fn supports_append_strategy(ty: &Type) -> bool {
1654    let Some(inner) = metadata_inner_type(ty) else {
1655        return matches!(ty, Type::Array(_))
1656            || matches!(last_type_ident(ty).as_deref(), Some("Vec"));
1657    };
1658    supports_append_strategy(inner)
1659}
1660
1661fn parse_rename_all_meta(
1662    meta: syn::meta::ParseNestedMeta<'_>,
1663    serialize: &mut Option<RenameRule>,
1664    deserialize: &mut Option<RenameRule>,
1665) -> syn::Result<()> {
1666    if meta.input.peek(syn::Token![=]) {
1667        let literal: LitStr = meta.value()?.parse()?;
1668        let rule = RenameRule::parse(&literal.value(), literal.span())?;
1669        *serialize = Some(rule);
1670        *deserialize = Some(rule);
1671        return Ok(());
1672    }
1673
1674    meta.parse_nested_meta(|nested| {
1675        if nested.path.is_ident("serialize") {
1676            let literal: LitStr = nested.value()?.parse()?;
1677            *serialize = Some(RenameRule::parse(&literal.value(), literal.span())?);
1678            return Ok(());
1679        }
1680        if nested.path.is_ident("deserialize") {
1681            let literal: LitStr = nested.value()?.parse()?;
1682            *deserialize = Some(RenameRule::parse(&literal.value(), literal.span())?);
1683            return Ok(());
1684        }
1685        Err(nested.error("unsupported serde rename_all option"))
1686    })
1687}
1688
1689fn parse_rename_meta(
1690    meta: syn::meta::ParseNestedMeta<'_>,
1691    serialize: &mut Option<String>,
1692    deserialize: &mut Option<String>,
1693) -> syn::Result<()> {
1694    if meta.input.peek(syn::Token![=]) {
1695        let value = parse_string_value(meta)?;
1696        *serialize = Some(value.clone());
1697        *deserialize = Some(value);
1698        return Ok(());
1699    }
1700
1701    meta.parse_nested_meta(|nested| {
1702        if nested.path.is_ident("serialize") {
1703            *serialize = Some(parse_string_value(nested)?);
1704            return Ok(());
1705        }
1706        if nested.path.is_ident("deserialize") {
1707            *deserialize = Some(parse_string_value(nested)?);
1708            return Ok(());
1709        }
1710        Err(nested.error("unsupported serde rename option"))
1711    })
1712}
1713
1714fn parse_string_value(meta: syn::meta::ParseNestedMeta<'_>) -> syn::Result<String> {
1715    let literal: LitStr = meta.value()?.parse()?;
1716    Ok(literal.value())
1717}
1718
1719fn parse_usize_value(meta: syn::meta::ParseNestedMeta<'_>) -> syn::Result<usize> {
1720    let literal: syn::LitInt = meta.value()?.parse()?;
1721    literal.base10_parse()
1722}
1723
1724fn parse_string_list_call(meta: syn::meta::ParseNestedMeta<'_>) -> syn::Result<Vec<String>> {
1725    let content;
1726    syn::parenthesized!(content in meta.input);
1727    let values = Punctuated::<LitStr, syn::Token![,]>::parse_terminated(&content)?;
1728    if values.is_empty() {
1729        return Err(meta.error("expected at least one string literal"));
1730    }
1731    Ok(values.into_iter().map(|value| value.value()).collect())
1732}
1733
1734fn parse_expr_list_call(meta: syn::meta::ParseNestedMeta<'_>) -> syn::Result<Vec<Expr>> {
1735    let content;
1736    syn::parenthesized!(content in meta.input);
1737    let values = Punctuated::<Expr, syn::Token![,]>::parse_terminated(&content)?;
1738    if values.is_empty() {
1739        return Err(meta.error("expected at least one expression"));
1740    }
1741    Ok(values.into_iter().collect())
1742}
1743
1744fn parse_source_kind_list(
1745    meta: syn::meta::ParseNestedMeta<'_>,
1746) -> syn::Result<Vec<TierSourceKind>> {
1747    let span = meta.path.span();
1748    parse_string_list_call(meta)?
1749        .into_iter()
1750        .map(|value| TierSourceKind::parse(&value, span))
1751        .collect()
1752}
1753
1754fn parse_literal_expr_list(meta: syn::meta::ParseNestedMeta<'_>) -> syn::Result<Vec<Expr>> {
1755    let content;
1756    syn::parenthesized!(content in meta.input);
1757    let values = Punctuated::<Expr, syn::Token![,]>::parse_terminated(&content)?;
1758    if values.is_empty() {
1759        return Err(meta.error("expected at least one literal value"));
1760    }
1761    let values = values.into_iter().collect::<Vec<_>>();
1762    for value in &values {
1763        validate_value_expr(value, value.span())?;
1764    }
1765    Ok(values)
1766}
1767
1768fn parse_numeric_literal(meta: syn::meta::ParseNestedMeta<'_>) -> syn::Result<NumericLiteral> {
1769    let expr: Expr = meta.value()?.parse()?;
1770    parse_numeric_expr(expr, meta.path.span())
1771}
1772
1773fn parse_numeric_expr(expr: Expr, span: proc_macro2::Span) -> syn::Result<NumericLiteral> {
1774    match expr {
1775        Expr::Lit(expr_lit) => match expr_lit.lit {
1776            Lit::Int(literal) => Ok(NumericLiteral {
1777                tokens: quote! { #literal },
1778                value: literal.base10_parse::<f64>()?,
1779            }),
1780            Lit::Float(literal) => Ok(NumericLiteral {
1781                tokens: quote! { #literal },
1782                value: literal.base10_parse::<f64>()?,
1783            }),
1784            _ => Err(syn::Error::new(
1785                span,
1786                "expected an integer or float literal",
1787            )),
1788        },
1789        Expr::Unary(expr_unary) if matches!(expr_unary.op, syn::UnOp::Neg(_)) => {
1790            match *expr_unary.expr {
1791                Expr::Lit(expr_lit) => match expr_lit.lit {
1792                    Lit::Int(literal) => Ok(NumericLiteral {
1793                        tokens: quote! { -#literal },
1794                        value: -literal.base10_parse::<f64>()?,
1795                    }),
1796                    Lit::Float(literal) => Ok(NumericLiteral {
1797                        tokens: quote! { -#literal },
1798                        value: -literal.base10_parse::<f64>()?,
1799                    }),
1800                    _ => Err(syn::Error::new(
1801                        span,
1802                        "expected an integer or float literal",
1803                    )),
1804                },
1805                _ => Err(syn::Error::new(
1806                    span,
1807                    "expected an integer or float literal",
1808                )),
1809            }
1810        }
1811        _ => Err(syn::Error::new(
1812            span,
1813            "expected an integer or float literal",
1814        )),
1815    }
1816}
1817
1818fn parse_value_expr(meta: syn::meta::ParseNestedMeta<'_>) -> syn::Result<Expr> {
1819    let expr: Expr = meta.value()?.parse()?;
1820    validate_value_expr(&expr, meta.path.span())?;
1821    Ok(expr)
1822}
1823
1824fn validate_value_expr(expr: &Expr, span: proc_macro2::Span) -> syn::Result<()> {
1825    match expr {
1826        Expr::Lit(expr_lit) => match &expr_lit.lit {
1827            Lit::Str(_) | Lit::Bool(_) | Lit::Int(_) | Lit::Float(_) => Ok(()),
1828            _ => Err(syn::Error::new(
1829                span,
1830                "expected a string, bool, integer, or float literal",
1831            )),
1832        },
1833        Expr::Unary(expr_unary) if matches!(expr_unary.op, syn::UnOp::Neg(_)) => match &*expr_unary
1834            .expr
1835        {
1836            Expr::Lit(expr_lit) if matches!(expr_lit.lit, Lit::Int(_) | Lit::Float(_)) => Ok(()),
1837            _ => Err(syn::Error::new(
1838                span,
1839                "expected a string, bool, integer, or float literal",
1840            )),
1841        },
1842        _ => Err(syn::Error::new(
1843            span,
1844            "expected a string, bool, integer, or float literal",
1845        )),
1846    }
1847}
1848
1849fn parse_required_with_container_check(
1850    meta: syn::meta::ParseNestedMeta<'_>,
1851) -> syn::Result<ContainerValidationCheck> {
1852    let mut path = None::<ContainerPathSpec>;
1853    let mut requires = None::<ContainerPathListSpec>;
1854    meta.parse_nested_meta(|nested| {
1855        if nested.path.is_ident("path") {
1856            if path.is_some() {
1857                return Err(nested.error(
1858                    "required_with supports either `path = ...` or `path_expr = ...`, not both",
1859                ));
1860            }
1861            path = Some(ContainerPathSpec::String(parse_string_value(nested)?));
1862            return Ok(());
1863        }
1864        if nested.path.is_ident("path_expr") {
1865            if path.is_some() {
1866                return Err(nested.error(
1867                    "required_with supports either `path = ...` or `path_expr = ...`, not both",
1868                ));
1869            }
1870            path = Some(ContainerPathSpec::Expr(parse_expr_value(nested)?));
1871            return Ok(());
1872        }
1873        if nested.path.is_ident("requires") {
1874            if requires.is_some() {
1875                return Err(nested.error(
1876                    "required_with supports either `requires(...)` or `requires_expr(...)`, not both",
1877                ));
1878            }
1879            requires = Some(ContainerPathListSpec::Strings(parse_string_list_call(nested)?));
1880            return Ok(());
1881        }
1882        if nested.path.is_ident("requires_expr") {
1883            if requires.is_some() {
1884                return Err(nested.error(
1885                    "required_with supports either `requires(...)` or `requires_expr(...)`, not both",
1886                ));
1887            }
1888            requires = Some(ContainerPathListSpec::Exprs(parse_expr_list_call(nested)?));
1889            return Ok(());
1890        }
1891        Err(nested.error("unsupported required_with option"))
1892    })?;
1893
1894    let Some(path) = path else {
1895        return Err(meta.error("required_with requires `path = \"...\"` or `path_expr = ...`"));
1896    };
1897    let Some(requires) = requires else {
1898        return Err(
1899            meta.error("required_with requires `requires(\"...\")` or `requires_expr(...)`")
1900        );
1901    };
1902
1903    Ok(ContainerValidationCheck::RequiredWith { path, requires })
1904}
1905
1906fn parse_required_if_container_check(
1907    meta: syn::meta::ParseNestedMeta<'_>,
1908) -> syn::Result<ContainerValidationCheck> {
1909    let mut path = None::<ContainerPathSpec>;
1910    let mut equals = None;
1911    let mut requires = None::<ContainerPathListSpec>;
1912    meta.parse_nested_meta(|nested| {
1913        if nested.path.is_ident("path") {
1914            if path.is_some() {
1915                return Err(nested.error(
1916                    "required_if supports either `path = ...` or `path_expr = ...`, not both",
1917                ));
1918            }
1919            path = Some(ContainerPathSpec::String(parse_string_value(nested)?));
1920            return Ok(());
1921        }
1922        if nested.path.is_ident("path_expr") {
1923            if path.is_some() {
1924                return Err(nested.error(
1925                    "required_if supports either `path = ...` or `path_expr = ...`, not both",
1926                ));
1927            }
1928            path = Some(ContainerPathSpec::Expr(parse_expr_value(nested)?));
1929            return Ok(());
1930        }
1931        if nested.path.is_ident("equals") {
1932            equals = Some(parse_value_expr(nested)?);
1933            return Ok(());
1934        }
1935        if nested.path.is_ident("requires") {
1936            if requires.is_some() {
1937                return Err(nested.error(
1938                    "required_if supports either `requires(...)` or `requires_expr(...)`, not both",
1939                ));
1940            }
1941            requires = Some(ContainerPathListSpec::Strings(parse_string_list_call(
1942                nested,
1943            )?));
1944            return Ok(());
1945        }
1946        if nested.path.is_ident("requires_expr") {
1947            if requires.is_some() {
1948                return Err(nested.error(
1949                    "required_if supports either `requires(...)` or `requires_expr(...)`, not both",
1950                ));
1951            }
1952            requires = Some(ContainerPathListSpec::Exprs(parse_expr_list_call(nested)?));
1953            return Ok(());
1954        }
1955        Err(nested.error("unsupported required_if option"))
1956    })?;
1957
1958    let Some(path) = path else {
1959        return Err(meta.error("required_if requires `path = \"...\"` or `path_expr = ...`"));
1960    };
1961    let Some(equals) = equals else {
1962        return Err(meta.error("required_if requires `equals = ...`"));
1963    };
1964    let Some(requires) = requires else {
1965        return Err(meta.error("required_if requires `requires(\"...\")` or `requires_expr(...)`"));
1966    };
1967
1968    Ok(ContainerValidationCheck::RequiredIf {
1969        path,
1970        equals,
1971        requires,
1972    })
1973}
1974
1975fn doc_comment(attributes: &[Attribute]) -> Option<String> {
1976    let mut lines = Vec::new();
1977    for attribute in attributes {
1978        if !attribute.path().is_ident("doc") {
1979            continue;
1980        }
1981        let Meta::NameValue(name_value) = &attribute.meta else {
1982            continue;
1983        };
1984        let Expr::Lit(expr_lit) = &name_value.value else {
1985            continue;
1986        };
1987        let Lit::Str(literal) = &expr_lit.lit else {
1988            continue;
1989        };
1990        let line = literal.value().trim().to_owned();
1991        if !line.is_empty() {
1992            lines.push(line);
1993        }
1994    }
1995
1996    (!lines.is_empty()).then(|| lines.join("\n"))
1997}
1998
1999fn direct_field_metadata_tokens(
2000    accumulator: &proc_macro2::Ident,
2001    field_name: &LitStr,
2002    aliases: &[LitStr],
2003    serde_attrs: &SerdeFieldAttrs,
2004    attrs: &TierAttrs,
2005    secret_type: bool,
2006) -> syn::Result<proc_macro2::TokenStream> {
2007    let mut builder = quote! {
2008        ::tier::FieldMetadata::new(#field_name)
2009    };
2010
2011    for alias in aliases {
2012        builder = quote! { #builder.alias(#alias) };
2013    }
2014    if attrs.secret || secret_type {
2015        builder = quote! { #builder.secret() };
2016    }
2017    if let Some(env) = &attrs.env {
2018        let env = LitStr::new(env, field_name.span());
2019        builder = quote! { #builder.env(#env) };
2020    }
2021    if let Some(doc) = &attrs.doc {
2022        let doc = LitStr::new(doc, field_name.span());
2023        builder = quote! { #builder.doc(#doc) };
2024    }
2025    if let Some(example) = &attrs.example {
2026        let example = LitStr::new(example, field_name.span());
2027        builder = quote! { #builder.example(#example) };
2028    }
2029    if let Some(deprecated) = &attrs.deprecated {
2030        let deprecated = LitStr::new(deprecated, field_name.span());
2031        builder = quote! { #builder.deprecated(#deprecated) };
2032    }
2033    if serde_attrs.has_default {
2034        builder = quote! { #builder.defaulted() };
2035    }
2036    if let Some(merge) = &attrs.merge {
2037        let merge_strategy = match merge.as_str() {
2038            "merge" => quote! { ::tier::MergeStrategy::Merge },
2039            "replace" => quote! { ::tier::MergeStrategy::Replace },
2040            "append" => quote! { ::tier::MergeStrategy::Append },
2041            _ => {
2042                return Err(syn::Error::new(
2043                    field_name.span(),
2044                    "unsupported tier merge strategy, expected merge|replace|append",
2045                ));
2046            }
2047        };
2048        builder = quote! { #builder.merge_strategy(#merge_strategy) };
2049    }
2050    if !attrs.sources.is_empty() {
2051        let sources = attrs
2052            .sources
2053            .iter()
2054            .map(|source| source.tokens())
2055            .collect::<Vec<_>>();
2056        builder = quote! { #builder.allow_sources([#(#sources),*]) };
2057    }
2058    if attrs.non_empty {
2059        builder = quote! { #builder.non_empty() };
2060    }
2061    if let Some(min) = &attrs.min {
2062        let min = &min.tokens;
2063        builder = quote! { #builder.min(#min) };
2064    }
2065    if let Some(max) = &attrs.max {
2066        let max = &max.tokens;
2067        builder = quote! { #builder.max(#max) };
2068    }
2069    if let Some(min_length) = attrs.min_length {
2070        builder = quote! { #builder.min_length(#min_length) };
2071    }
2072    if let Some(max_length) = attrs.max_length {
2073        builder = quote! { #builder.max_length(#max_length) };
2074    }
2075    if let Some(min_items) = attrs.min_items {
2076        builder = quote! { #builder.min_items(#min_items) };
2077    }
2078    if let Some(max_items) = attrs.max_items {
2079        builder = quote! { #builder.max_items(#max_items) };
2080    }
2081    if let Some(min_properties) = attrs.min_properties {
2082        builder = quote! { #builder.min_properties(#min_properties) };
2083    }
2084    if let Some(max_properties) = attrs.max_properties {
2085        builder = quote! { #builder.max_properties(#max_properties) };
2086    }
2087    if let Some(multiple_of) = &attrs.multiple_of {
2088        let multiple_of = &multiple_of.tokens;
2089        builder = quote! { #builder.multiple_of(#multiple_of) };
2090    }
2091    if let Some(pattern) = &attrs.pattern {
2092        let pattern = LitStr::new(pattern, field_name.span());
2093        builder = quote! { #builder.pattern(#pattern) };
2094    }
2095    if attrs.unique_items {
2096        builder = quote! { #builder.unique_items() };
2097    }
2098    if !attrs.one_of.is_empty() {
2099        let one_of = &attrs.one_of;
2100        builder = quote! { #builder.one_of([#(#one_of),*]) };
2101    }
2102    if attrs.hostname {
2103        builder = quote! { #builder.hostname() };
2104    }
2105    if attrs.url {
2106        builder = quote! { #builder.url() };
2107    }
2108    if attrs.email {
2109        builder = quote! { #builder.email() };
2110    }
2111    if attrs.ip_addr {
2112        builder = quote! { #builder.ip_addr() };
2113    }
2114    if attrs.socket_addr {
2115        builder = quote! { #builder.socket_addr() };
2116    }
2117    if attrs.absolute_path {
2118        builder = quote! { #builder.absolute_path() };
2119    }
2120    if let Some(env_decode) = &attrs.env_decode {
2121        let env_decode = match env_decode.as_str() {
2122            "csv" => quote! { ::tier::EnvDecoder::Csv },
2123            "path_list" => quote! { ::tier::EnvDecoder::PathList },
2124            "key_value_map" => quote! { ::tier::EnvDecoder::KeyValueMap },
2125            "whitespace" => quote! { ::tier::EnvDecoder::Whitespace },
2126            _ => {
2127                return Err(syn::Error::new(
2128                    field_name.span(),
2129                    "unsupported tier env decoder, expected csv|path_list|key_value_map|whitespace",
2130                ));
2131            }
2132        };
2133        builder = quote! { #builder.env_decoder(#env_decode) };
2134    }
2135
2136    Ok(quote! {
2137        #accumulator.push(#builder);
2138    })
2139}
2140
2141fn is_secret_type(ty: &Type) -> bool {
2142    matches!(last_type_ident(ty).as_deref(), Some("Secret"))
2143}
2144
2145fn metadata_target_type(ty: &Type) -> &Type {
2146    let Some(inner) = metadata_inner_type(ty) else {
2147        return ty;
2148    };
2149    metadata_target_type(inner)
2150}
2151
2152fn metadata_inner_type(ty: &Type) -> Option<&Type> {
2153    let Type::Path(type_path) = ty else {
2154        return None;
2155    };
2156    let segment = type_path.path.segments.last()?;
2157    match segment.ident.to_string().as_str() {
2158        "Option" | "Box" | "Arc" => match &segment.arguments {
2159            PathArguments::AngleBracketed(arguments) => {
2160                arguments.args.iter().find_map(|argument| {
2161                    if let GenericArgument::Type(ty) = argument {
2162                        Some(ty)
2163                    } else {
2164                        None
2165                    }
2166                })
2167            }
2168            _ => None,
2169        },
2170        _ => None,
2171    }
2172}
2173
2174fn option_inner_type(ty: &Type) -> Option<&Type> {
2175    wrapper_inner_type(ty, "Option")
2176}
2177
2178fn patch_inner_type(ty: &Type) -> Option<&Type> {
2179    wrapper_inner_type(ty, "Patch")
2180}
2181
2182fn wrapper_inner_type<'a>(ty: &'a Type, wrapper: &str) -> Option<&'a Type> {
2183    let Type::Path(type_path) = ty else {
2184        return None;
2185    };
2186    let segment = type_path.path.segments.last()?;
2187    if segment.ident != wrapper {
2188        return None;
2189    }
2190    match &segment.arguments {
2191        PathArguments::AngleBracketed(arguments) => arguments.args.iter().find_map(|argument| {
2192            if let GenericArgument::Type(ty) = argument {
2193                Some(ty)
2194            } else {
2195                None
2196            }
2197        }),
2198        _ => None,
2199    }
2200}
2201
2202fn last_type_ident(ty: &Type) -> Option<String> {
2203    let Type::Path(type_path) = ty else {
2204        return None;
2205    };
2206    type_path
2207        .path
2208        .segments
2209        .last()
2210        .map(|segment| segment.ident.to_string())
2211}
2212
2213fn unraw(ident: &syn::Ident) -> String {
2214    ident.to_string().trim_start_matches("r#").to_owned()
2215}
2216
2217fn consume_unused_meta(meta: syn::meta::ParseNestedMeta<'_>) -> syn::Result<()> {
2218    if meta.input.peek(syn::Token![=]) {
2219        let _: Expr = meta.value()?.parse()?;
2220        return Ok(());
2221    }
2222
2223    if meta.input.peek(syn::token::Paren) {
2224        meta.parse_nested_meta(|nested| {
2225            consume_unused_meta(nested)?;
2226            Ok(())
2227        })?;
2228    }
2229
2230    Ok(())
2231}