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