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
23fn expand_tier_config(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
24    let tier_attrs = parse_tier_container_attrs(&input.attrs)?;
25    let ident = input.ident;
26    let generics = input.generics;
27    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
28    let container_attrs = parse_serde_container_attrs(&input.attrs)?;
29    let field_tokens = match input.data {
30        Data::Struct(data_struct) => expand_struct_metadata(data_struct, &container_attrs)?,
31        Data::Enum(data_enum) => expand_enum_metadata(data_enum, &container_attrs)?,
32        Data::Union(union) => {
33            return Err(syn::Error::new_spanned(
34                union.union_token,
35                "TierConfig cannot be derived for unions",
36            ));
37        }
38    };
39    let check_tokens = container_check_tokens(&tier_attrs);
40
41    Ok(quote! {
42        impl #impl_generics ::tier::TierMetadata for #ident #ty_generics #where_clause {
43            fn metadata() -> ::tier::ConfigMetadata {
44                let mut metadata = ::tier::ConfigMetadata::new();
45                #(#field_tokens)*
46                #(#check_tokens)*
47                metadata
48            }
49        }
50    })
51}
52
53fn expand_struct_metadata(
54    data_struct: DataStruct,
55    container_attrs: &SerdeContainerAttrs,
56) -> syn::Result<Vec<proc_macro2::TokenStream>> {
57    ensure_struct_container_attrs(container_attrs)?;
58
59    match data_struct.fields {
60        Fields::Named(fields) => expand_named_fields_metadata(
61            fields,
62            SerdeFieldContext::for_struct(container_attrs),
63            &format_ident!("metadata"),
64            None,
65        ),
66        Fields::Unnamed(fields) => {
67            expand_newtype_struct_metadata(fields, &format_ident!("metadata"))
68        }
69        Fields::Unit => Ok(Vec::new()),
70    }
71}
72
73fn expand_enum_metadata(
74    data_enum: DataEnum,
75    container_attrs: &SerdeContainerAttrs,
76) -> syn::Result<Vec<proc_macro2::TokenStream>> {
77    let representation = enum_representation(container_attrs)?;
78    let conflicts = non_external_variant_field_conflicts(&data_enum, container_attrs)?;
79    let mut tokens = vec![quote! {
80        metadata.push(
81            ::tier::FieldMetadata::new("").merge_strategy(::tier::MergeStrategy::Replace)
82        );
83    }];
84    if let Some(tag) = representation.tag_field() {
85        let tag_lit = LitStr::new(tag, proc_macro2::Span::call_site());
86        tokens.push(quote! {
87            metadata.push(::tier::FieldMetadata::new(#tag_lit));
88        });
89    }
90
91    for variant in data_enum.variants {
92        let variant_ident = variant.ident.clone();
93        let variant_attrs =
94            parse_serde_variant_attrs(&variant.attrs, &variant_ident, container_attrs)?;
95        if variant_attrs.skip_metadata {
96            continue;
97        }
98
99        match variant.fields {
100            Fields::Named(fields) => {
101                let field_tokens = expand_named_fields_metadata(
102                    fields,
103                    SerdeFieldContext::for_enum_variant_fields(container_attrs),
104                    &format_ident!("variant_metadata"),
105                    Some(&conflicts),
106                )?;
107                push_variant_tokens(
108                    &mut tokens,
109                    field_tokens,
110                    &variant_attrs,
111                    &representation,
112                    variant_ident.span(),
113                );
114            }
115            Fields::Unnamed(fields) => {
116                let field_tokens = expand_newtype_variant_metadata(
117                    fields,
118                    &representation,
119                    variant_ident.span(),
120                    &format_ident!("variant_metadata"),
121                )?;
122                push_variant_tokens(
123                    &mut tokens,
124                    field_tokens,
125                    &variant_attrs,
126                    &representation,
127                    variant_ident.span(),
128                );
129            }
130            Fields::Unit => {}
131        }
132    }
133
134    Ok(tokens)
135}
136
137fn push_variant_tokens(
138    tokens: &mut Vec<proc_macro2::TokenStream>,
139    variant_tokens: Vec<proc_macro2::TokenStream>,
140    variant_attrs: &SerdeVariantAttrs,
141    representation: &EnumRepresentation,
142    span: proc_macro2::Span,
143) {
144    let variant_name_lit = LitStr::new(&variant_attrs.canonical_name, span);
145    let variant_alias_lits = variant_attrs
146        .aliases
147        .iter()
148        .map(|alias| LitStr::new(alias, span))
149        .collect::<Vec<_>>();
150
151    match representation {
152        EnumRepresentation::External => {
153            tokens.push(quote! {
154                {
155                    let mut variant_metadata = ::tier::ConfigMetadata::new();
156                    #(#variant_tokens)*
157                    metadata.extend(::tier::metadata::prefixed_metadata(
158                        #variant_name_lit,
159                        ::std::vec![#(::std::string::String::from(#variant_alias_lits)),*],
160                        variant_metadata,
161                    ));
162                }
163            });
164        }
165        EnumRepresentation::Adjacent { content, .. } => {
166            let content_lit = LitStr::new(content, span);
167            tokens.push(quote! {
168                {
169                    let mut variant_metadata = ::tier::ConfigMetadata::new();
170                    #(#variant_tokens)*
171                    metadata.extend(::tier::metadata::prefixed_metadata(
172                        #content_lit,
173                        ::std::vec![],
174                        variant_metadata,
175                    ));
176                }
177            });
178        }
179        EnumRepresentation::Internal { .. } | EnumRepresentation::Untagged => {
180            tokens.push(quote! {
181                {
182                    let mut variant_metadata = ::tier::ConfigMetadata::new();
183                    #(#variant_tokens)*
184                    metadata.extend(variant_metadata);
185                }
186            });
187        }
188    }
189}
190
191fn expand_named_fields_metadata(
192    fields: FieldsNamed,
193    context: SerdeFieldContext,
194    accumulator: &proc_macro2::Ident,
195    conflicts: Option<&NonExternalFieldConflicts>,
196) -> syn::Result<Vec<proc_macro2::TokenStream>> {
197    let mut field_tokens = Vec::new();
198
199    for field in fields.named {
200        field_tokens.extend(expand_named_field_metadata(
201            field,
202            context,
203            accumulator,
204            conflicts,
205        )?);
206    }
207
208    Ok(field_tokens)
209}
210
211fn expand_named_field_metadata(
212    field: Field,
213    context: SerdeFieldContext,
214    accumulator: &proc_macro2::Ident,
215    conflicts: Option<&NonExternalFieldConflicts>,
216) -> syn::Result<Vec<proc_macro2::TokenStream>> {
217    let field_ident = field.ident.expect("named field");
218    let mut serde_attrs = parse_serde_field_attrs(&field.attrs, &field_ident, context)?;
219    let mut attrs = parse_tier_attrs(&field.attrs)?;
220    if attrs.doc.is_none() {
221        attrs.doc = doc_comment(&field.attrs);
222    }
223
224    if serde_attrs.skip_metadata {
225        if attrs.has_any() {
226            return Err(syn::Error::new_spanned(
227                field_ident,
228                "skipped fields cannot use tier metadata attributes",
229            ));
230        }
231        return Ok(Vec::new());
232    }
233
234    if serde_attrs.flatten && attrs.has_any() {
235        return Err(syn::Error::new_spanned(
236            field_ident,
237            "flattened fields cannot use tier metadata attributes",
238        ));
239    }
240
241    if let Some(conflicts) = conflicts {
242        if conflicts
243            .skipped_fields
244            .contains(&serde_attrs.canonical_name)
245        {
246            return Ok(Vec::new());
247        }
248        serde_attrs
249            .aliases
250            .retain(|alias| !conflicts.skipped_aliases.contains(alias));
251        if attrs
252            .env
253            .as_ref()
254            .is_some_and(|env| conflicts.skipped_envs.contains(env))
255        {
256            attrs.env = None;
257        }
258    }
259
260    validate_merge_strategy(&attrs, &field.ty)?;
261    validate_validation_attrs(&attrs, &field_ident)?;
262
263    let field_type = field.ty;
264    let metadata_ty = metadata_target_type(&field_type);
265    let canonical_name_lit = LitStr::new(&serde_attrs.canonical_name, field_ident.span());
266    let alias_lits = serde_attrs
267        .aliases
268        .iter()
269        .map(|alias| LitStr::new(alias, field_ident.span()))
270        .collect::<Vec<_>>();
271
272    if serde_attrs.flatten {
273        return Ok(vec![quote! {
274            #accumulator.extend(<#metadata_ty as ::tier::TierMetadata>::metadata());
275        }]);
276    }
277
278    Ok(vec![
279        quote! {
280            #accumulator.extend(::tier::metadata::prefixed_metadata(
281                #canonical_name_lit,
282                ::std::vec![#(::std::string::String::from(#alias_lits)),*],
283                <#metadata_ty as ::tier::TierMetadata>::metadata(),
284            ));
285        },
286        direct_field_metadata_tokens(
287            accumulator,
288            &canonical_name_lit,
289            &alias_lits,
290            &serde_attrs,
291            &attrs,
292            is_secret_type(metadata_ty),
293        )?,
294    ])
295}
296
297fn expand_newtype_struct_metadata(
298    fields: FieldsUnnamed,
299    accumulator: &proc_macro2::Ident,
300) -> syn::Result<Vec<proc_macro2::TokenStream>> {
301    if fields.unnamed.len() != 1 {
302        return Err(syn::Error::new_spanned(
303            fields,
304            "TierConfig only supports tuple structs with exactly one field",
305        ));
306    }
307
308    let field = fields.unnamed.into_iter().next().expect("single field");
309    if parse_tier_attrs(&field.attrs)?.has_any() || has_field_naming_attrs(&field.attrs)? {
310        return Err(syn::Error::new_spanned(
311            field,
312            "tuple struct wrappers cannot use field-level tier or serde naming attributes",
313        ));
314    }
315
316    let metadata_ty = metadata_target_type(&field.ty);
317    Ok(vec![quote! {
318        #accumulator.extend(<#metadata_ty as ::tier::TierMetadata>::metadata());
319    }])
320}
321
322fn expand_newtype_variant_metadata(
323    fields: FieldsUnnamed,
324    representation: &EnumRepresentation,
325    span: proc_macro2::Span,
326    accumulator: &proc_macro2::Ident,
327) -> syn::Result<Vec<proc_macro2::TokenStream>> {
328    if fields.unnamed.len() != 1 {
329        return Err(syn::Error::new(
330            span,
331            "TierConfig only supports enum tuple variants with exactly one field",
332        ));
333    }
334
335    if matches!(representation, EnumRepresentation::Internal { .. }) {
336        return Err(syn::Error::new(
337            span,
338            "internally tagged enums with tuple variants are not supported by TierConfig metadata",
339        ));
340    }
341
342    let field = fields.unnamed.into_iter().next().expect("single field");
343    if parse_tier_attrs(&field.attrs)?.has_any() || has_field_naming_attrs(&field.attrs)? {
344        return Err(syn::Error::new_spanned(
345            field,
346            "tuple enum variants cannot use field-level tier or serde naming attributes",
347        ));
348    }
349
350    let metadata_ty = metadata_target_type(&field.ty);
351    Ok(vec![quote! {
352        #accumulator.extend(<#metadata_ty as ::tier::TierMetadata>::metadata());
353    }])
354}
355
356#[derive(Debug, Default)]
357struct TierAttrs {
358    secret: bool,
359    env: Option<String>,
360    doc: Option<String>,
361    example: Option<String>,
362    deprecated: Option<String>,
363    merge: Option<String>,
364    non_empty: bool,
365    min: Option<NumericLiteral>,
366    max: Option<NumericLiteral>,
367    min_length: Option<usize>,
368    max_length: Option<usize>,
369    one_of: Vec<Expr>,
370    hostname: bool,
371    ip_addr: bool,
372    socket_addr: bool,
373    absolute_path: bool,
374}
375
376impl TierAttrs {
377    fn has_any(&self) -> bool {
378        self.secret
379            || self.env.is_some()
380            || self.doc.is_some()
381            || self.example.is_some()
382            || self.deprecated.is_some()
383            || self.merge.is_some()
384            || self.non_empty
385            || self.min.is_some()
386            || self.max.is_some()
387            || self.min_length.is_some()
388            || self.max_length.is_some()
389            || !self.one_of.is_empty()
390            || self.hostname
391            || self.ip_addr
392            || self.socket_addr
393            || self.absolute_path
394    }
395}
396
397#[derive(Debug, Default)]
398struct TierContainerAttrs {
399    checks: Vec<ContainerValidationCheck>,
400}
401
402#[derive(Debug, Clone)]
403struct NumericLiteral {
404    tokens: proc_macro2::TokenStream,
405    value: f64,
406}
407
408#[derive(Debug, Clone)]
409enum ContainerValidationCheck {
410    AtLeastOneOf(Vec<String>),
411    ExactlyOneOf(Vec<String>),
412    MutuallyExclusive(Vec<String>),
413    RequiredWith {
414        path: String,
415        requires: Vec<String>,
416    },
417    RequiredIf {
418        path: String,
419        equals: Expr,
420        requires: Vec<String>,
421    },
422}
423
424#[derive(Debug, Default)]
425struct SerdeContainerAttrs {
426    rename_all_serialize: Option<RenameRule>,
427    rename_all_deserialize: Option<RenameRule>,
428    rename_all_fields_serialize: Option<RenameRule>,
429    rename_all_fields_deserialize: Option<RenameRule>,
430    default_fields: bool,
431    tag: Option<String>,
432    content: Option<String>,
433    untagged: bool,
434}
435
436#[derive(Debug, Clone, Copy, Default)]
437struct SerdeFieldContext {
438    rename_serialize: Option<RenameRule>,
439    rename_deserialize: Option<RenameRule>,
440    default_fields: bool,
441}
442
443impl SerdeFieldContext {
444    fn for_struct(container_attrs: &SerdeContainerAttrs) -> Self {
445        Self {
446            rename_serialize: container_attrs.rename_all_serialize,
447            rename_deserialize: container_attrs.rename_all_deserialize,
448            default_fields: container_attrs.default_fields,
449        }
450    }
451
452    fn for_enum_variant_fields(container_attrs: &SerdeContainerAttrs) -> Self {
453        Self {
454            rename_serialize: container_attrs.rename_all_fields_serialize,
455            rename_deserialize: container_attrs.rename_all_fields_deserialize,
456            default_fields: false,
457        }
458    }
459}
460
461#[derive(Debug, Default)]
462struct SerdeFieldAttrs {
463    canonical_name: String,
464    aliases: Vec<String>,
465    flatten: bool,
466    skip_metadata: bool,
467    has_default: bool,
468}
469
470#[derive(Debug, Default)]
471struct SerdeVariantAttrs {
472    canonical_name: String,
473    aliases: Vec<String>,
474    skip_metadata: bool,
475}
476
477#[derive(Debug, Default)]
478struct NonExternalFieldConflicts {
479    skipped_fields: HashSet<String>,
480    skipped_aliases: HashSet<String>,
481    skipped_envs: HashSet<String>,
482}
483
484#[derive(Debug, Clone)]
485enum EnumRepresentation {
486    External,
487    Internal { tag: String },
488    Adjacent { tag: String, content: String },
489    Untagged,
490}
491
492impl EnumRepresentation {
493    fn tag_field(&self) -> Option<&str> {
494        match self {
495            Self::Internal { tag } => Some(tag.as_str()),
496            Self::Adjacent { tag, .. } => Some(tag.as_str()),
497            Self::External | Self::Untagged => None,
498        }
499    }
500}
501
502#[derive(Debug, Clone, Copy, PartialEq, Eq)]
503enum RenameRule {
504    Lower,
505    Upper,
506    Pascal,
507    Camel,
508    Snake,
509    ScreamingSnake,
510    Kebab,
511    ScreamingKebab,
512}
513
514impl RenameRule {
515    fn parse(value: &str, span: proc_macro2::Span) -> syn::Result<Self> {
516        match value {
517            "lowercase" => Ok(Self::Lower),
518            "UPPERCASE" => Ok(Self::Upper),
519            "PascalCase" => Ok(Self::Pascal),
520            "camelCase" => Ok(Self::Camel),
521            "snake_case" => Ok(Self::Snake),
522            "SCREAMING_SNAKE_CASE" => Ok(Self::ScreamingSnake),
523            "kebab-case" => Ok(Self::Kebab),
524            "SCREAMING-KEBAB-CASE" => Ok(Self::ScreamingKebab),
525            _ => Err(syn::Error::new(
526                span,
527                "unsupported serde rename rule for TierConfig",
528            )),
529        }
530    }
531
532    fn apply_to_field(self, value: &str) -> String {
533        match self {
534            Self::Lower | Self::Snake => value.to_owned(),
535            Self::Upper | Self::ScreamingSnake => value.to_ascii_uppercase(),
536            Self::Pascal => {
537                let mut output = String::new();
538                let mut capitalize = true;
539                for ch in value.chars() {
540                    if ch == '_' {
541                        capitalize = true;
542                    } else if capitalize {
543                        output.push(ch.to_ascii_uppercase());
544                        capitalize = false;
545                    } else {
546                        output.push(ch);
547                    }
548                }
549                output
550            }
551            Self::Camel => {
552                let pascal = Self::Pascal.apply_to_field(value);
553                lowercase_first_char(&pascal)
554            }
555            Self::Kebab => value.replace('_', "-"),
556            Self::ScreamingKebab => value.replace('_', "-").to_ascii_uppercase(),
557        }
558    }
559
560    fn apply_to_variant(self, value: &str) -> String {
561        match self {
562            Self::Lower => value.to_ascii_lowercase(),
563            Self::Upper => value.to_ascii_uppercase(),
564            Self::Pascal => value.to_owned(),
565            Self::Camel => lowercase_first_char(value),
566            Self::Snake => {
567                let mut output = String::new();
568                for (index, ch) in value.char_indices() {
569                    if index > 0 && ch.is_uppercase() {
570                        output.push('_');
571                    }
572                    output.push(ch.to_ascii_lowercase());
573                }
574                output
575            }
576            Self::ScreamingSnake => Self::Snake.apply_to_variant(value).to_ascii_uppercase(),
577            Self::Kebab => Self::Snake.apply_to_variant(value).replace('_', "-"),
578            Self::ScreamingKebab => Self::Kebab.apply_to_variant(value).to_ascii_uppercase(),
579        }
580    }
581}
582
583fn lowercase_first_char(value: &str) -> String {
584    let mut chars = value.chars();
585    let Some(first) = chars.next() else {
586        return String::new();
587    };
588
589    let mut output = first.to_ascii_lowercase().to_string();
590    output.push_str(chars.as_str());
591    output
592}
593
594fn parse_tier_attrs(attributes: &[Attribute]) -> syn::Result<TierAttrs> {
595    let mut attrs = TierAttrs::default();
596    for attribute in attributes {
597        if !attribute.path().is_ident("tier") {
598            continue;
599        }
600        attribute.parse_nested_meta(|meta| {
601            if meta.path.is_ident("secret") {
602                attrs.secret = true;
603                return Ok(());
604            }
605            if meta.path.is_ident("env") {
606                attrs.env = Some(parse_string_value(meta)?);
607                return Ok(());
608            }
609            if meta.path.is_ident("doc") {
610                attrs.doc = Some(parse_string_value(meta)?);
611                return Ok(());
612            }
613            if meta.path.is_ident("example") {
614                attrs.example = Some(parse_string_value(meta)?);
615                return Ok(());
616            }
617            if meta.path.is_ident("deprecated") {
618                attrs.deprecated = Some(if meta.input.peek(syn::Token![=]) {
619                    parse_string_value(meta)?
620                } else {
621                    "this field is deprecated".to_owned()
622                });
623                return Ok(());
624            }
625            if meta.path.is_ident("merge") {
626                attrs.merge = Some(parse_string_value(meta)?);
627                return Ok(());
628            }
629            if meta.path.is_ident("non_empty") {
630                attrs.non_empty = true;
631                consume_unused_meta(meta)?;
632                return Ok(());
633            }
634            if meta.path.is_ident("min") {
635                attrs.min = Some(parse_numeric_literal(meta)?);
636                return Ok(());
637            }
638            if meta.path.is_ident("max") {
639                attrs.max = Some(parse_numeric_literal(meta)?);
640                return Ok(());
641            }
642            if meta.path.is_ident("min_length") {
643                attrs.min_length = Some(parse_usize_value(meta)?);
644                return Ok(());
645            }
646            if meta.path.is_ident("max_length") {
647                attrs.max_length = Some(parse_usize_value(meta)?);
648                return Ok(());
649            }
650            if meta.path.is_ident("one_of") {
651                attrs.one_of = parse_literal_expr_list(meta)?;
652                return Ok(());
653            }
654            if meta.path.is_ident("hostname") {
655                attrs.hostname = true;
656                consume_unused_meta(meta)?;
657                return Ok(());
658            }
659            if meta.path.is_ident("ip_addr") {
660                attrs.ip_addr = true;
661                consume_unused_meta(meta)?;
662                return Ok(());
663            }
664            if meta.path.is_ident("socket_addr") {
665                attrs.socket_addr = true;
666                consume_unused_meta(meta)?;
667                return Ok(());
668            }
669            if meta.path.is_ident("absolute_path") {
670                attrs.absolute_path = true;
671                consume_unused_meta(meta)?;
672                return Ok(());
673            }
674            Err(meta.error("unsupported tier attribute"))
675        })?;
676    }
677    Ok(attrs)
678}
679
680fn parse_tier_container_attrs(attributes: &[Attribute]) -> syn::Result<TierContainerAttrs> {
681    let mut attrs = TierContainerAttrs::default();
682
683    for attribute in attributes {
684        if !attribute.path().is_ident("tier") {
685            continue;
686        }
687
688        attribute.parse_nested_meta(|meta| {
689            if meta.path.is_ident("at_least_one_of") {
690                attrs.checks.push(ContainerValidationCheck::AtLeastOneOf(
691                    parse_string_list_call(meta)?,
692                ));
693                return Ok(());
694            }
695            if meta.path.is_ident("exactly_one_of") {
696                attrs.checks.push(ContainerValidationCheck::ExactlyOneOf(
697                    parse_string_list_call(meta)?,
698                ));
699                return Ok(());
700            }
701            if meta.path.is_ident("mutually_exclusive") {
702                attrs
703                    .checks
704                    .push(ContainerValidationCheck::MutuallyExclusive(
705                        parse_string_list_call(meta)?,
706                    ));
707                return Ok(());
708            }
709            if meta.path.is_ident("required_with") {
710                attrs
711                    .checks
712                    .push(parse_required_with_container_check(meta)?);
713                return Ok(());
714            }
715            if meta.path.is_ident("required_if") {
716                attrs.checks.push(parse_required_if_container_check(meta)?);
717                return Ok(());
718            }
719            Err(meta.error("unsupported tier container attribute"))
720        })?;
721    }
722
723    Ok(attrs)
724}
725
726fn parse_serde_container_attrs(attributes: &[Attribute]) -> syn::Result<SerdeContainerAttrs> {
727    let mut attrs = SerdeContainerAttrs::default();
728    for attribute in attributes {
729        if !attribute.path().is_ident("serde") {
730            continue;
731        }
732
733        attribute.parse_nested_meta(|meta| {
734            if meta.path.is_ident("rename_all") {
735                parse_rename_all_meta(
736                    meta,
737                    &mut attrs.rename_all_serialize,
738                    &mut attrs.rename_all_deserialize,
739                )?;
740                return Ok(());
741            }
742            if meta.path.is_ident("rename_all_fields") {
743                parse_rename_all_meta(
744                    meta,
745                    &mut attrs.rename_all_fields_serialize,
746                    &mut attrs.rename_all_fields_deserialize,
747                )?;
748                return Ok(());
749            }
750            if meta.path.is_ident("default") {
751                attrs.default_fields = true;
752                consume_unused_meta(meta)?;
753                return Ok(());
754            }
755            if meta.path.is_ident("tag") {
756                attrs.tag = Some(parse_string_value(meta)?);
757                return Ok(());
758            }
759            if meta.path.is_ident("content") {
760                attrs.content = Some(parse_string_value(meta)?);
761                return Ok(());
762            }
763            if meta.path.is_ident("untagged") {
764                attrs.untagged = true;
765                consume_unused_meta(meta)?;
766                return Ok(());
767            }
768            consume_unused_meta(meta)?;
769            Ok(())
770        })?;
771    }
772
773    Ok(attrs)
774}
775
776fn parse_serde_field_attrs(
777    attributes: &[Attribute],
778    field_ident: &syn::Ident,
779    context: SerdeFieldContext,
780) -> syn::Result<SerdeFieldAttrs> {
781    let base_name = unraw(field_ident);
782    let mut rename_serialize = None;
783    let mut rename_deserialize = None;
784    let mut aliases = Vec::new();
785    let mut flatten = false;
786    let mut skip_metadata = false;
787    let mut has_default = context.default_fields;
788
789    for attribute in attributes {
790        if !attribute.path().is_ident("serde") {
791            continue;
792        }
793
794        attribute.parse_nested_meta(|meta| {
795            if meta.path.is_ident("rename") {
796                parse_rename_meta(meta, &mut rename_serialize, &mut rename_deserialize)?;
797                return Ok(());
798            }
799            if meta.path.is_ident("alias") {
800                aliases.push(parse_string_value(meta)?);
801                return Ok(());
802            }
803            if meta.path.is_ident("flatten") {
804                flatten = true;
805                return Ok(());
806            }
807            if meta.path.is_ident("default") {
808                has_default = true;
809                consume_unused_meta(meta)?;
810                return Ok(());
811            }
812            if meta.path.is_ident("skip") || meta.path.is_ident("skip_deserializing") {
813                skip_metadata = true;
814                return Ok(());
815            }
816            consume_unused_meta(meta)?;
817            Ok(())
818        })?;
819    }
820
821    let has_explicit_rename = rename_serialize.is_some() || rename_deserialize.is_some();
822
823    let canonical_name = rename_serialize
824        .or_else(|| {
825            context
826                .rename_serialize
827                .map(|rule| rule.apply_to_field(&base_name))
828        })
829        .unwrap_or_else(|| base_name.clone());
830    let deserialize_name = rename_deserialize
831        .or_else(|| {
832            context
833                .rename_deserialize
834                .map(|rule| rule.apply_to_field(&base_name))
835        })
836        .unwrap_or_else(|| base_name.clone());
837
838    if deserialize_name != canonical_name {
839        aliases.push(deserialize_name);
840    }
841
842    if flatten && (!aliases.is_empty() || has_explicit_rename) {
843        return Err(syn::Error::new_spanned(
844            field_ident,
845            "flattened fields cannot use serde rename or alias attributes",
846        ));
847    }
848
849    aliases.retain(|alias| alias != &canonical_name);
850    aliases.sort();
851    aliases.dedup();
852
853    Ok(SerdeFieldAttrs {
854        canonical_name,
855        aliases,
856        flatten,
857        skip_metadata,
858        has_default,
859    })
860}
861
862fn parse_serde_variant_attrs(
863    attributes: &[Attribute],
864    variant_ident: &syn::Ident,
865    container_attrs: &SerdeContainerAttrs,
866) -> syn::Result<SerdeVariantAttrs> {
867    let base_name = unraw(variant_ident);
868    let mut rename_serialize = None;
869    let mut rename_deserialize = None;
870    let mut aliases = Vec::new();
871    let mut skip_metadata = false;
872
873    for attribute in attributes {
874        if !attribute.path().is_ident("serde") {
875            continue;
876        }
877
878        attribute.parse_nested_meta(|meta| {
879            if meta.path.is_ident("rename") {
880                parse_rename_meta(meta, &mut rename_serialize, &mut rename_deserialize)?;
881                return Ok(());
882            }
883            if meta.path.is_ident("alias") {
884                aliases.push(parse_string_value(meta)?);
885                return Ok(());
886            }
887            if meta.path.is_ident("skip")
888                || meta.path.is_ident("skip_deserializing")
889                || meta.path.is_ident("other")
890            {
891                skip_metadata = true;
892                consume_unused_meta(meta)?;
893                return Ok(());
894            }
895            consume_unused_meta(meta)?;
896            Ok(())
897        })?;
898    }
899
900    let canonical_name = rename_serialize
901        .or_else(|| {
902            container_attrs
903                .rename_all_serialize
904                .map(|rule| rule.apply_to_variant(&base_name))
905        })
906        .unwrap_or_else(|| base_name.clone());
907    let deserialize_name = rename_deserialize
908        .or_else(|| {
909            container_attrs
910                .rename_all_deserialize
911                .map(|rule| rule.apply_to_variant(&base_name))
912        })
913        .unwrap_or_else(|| base_name.clone());
914
915    if deserialize_name != canonical_name {
916        aliases.push(deserialize_name);
917    }
918
919    aliases.retain(|alias| alias != &canonical_name);
920    aliases.sort();
921    aliases.dedup();
922
923    Ok(SerdeVariantAttrs {
924        canonical_name,
925        aliases,
926        skip_metadata,
927    })
928}
929
930fn ensure_struct_container_attrs(container_attrs: &SerdeContainerAttrs) -> syn::Result<()> {
931    if container_attrs.rename_all_fields_serialize.is_some()
932        || container_attrs.rename_all_fields_deserialize.is_some()
933    {
934        return Err(syn::Error::new(
935            proc_macro2::Span::call_site(),
936            "serde(rename_all_fields = ...) is only supported on enums",
937        ));
938    }
939    if container_attrs.tag.is_some()
940        || container_attrs.content.is_some()
941        || container_attrs.untagged
942    {
943        return Err(syn::Error::new(
944            proc_macro2::Span::call_site(),
945            "serde enum tagging attributes are not supported on structs",
946        ));
947    }
948    Ok(())
949}
950
951fn enum_representation(container_attrs: &SerdeContainerAttrs) -> syn::Result<EnumRepresentation> {
952    if container_attrs.untagged && container_attrs.tag.is_some() {
953        return Err(syn::Error::new(
954            proc_macro2::Span::call_site(),
955            "serde(untagged) cannot be combined with serde(tag = ...)",
956        ));
957    }
958    if container_attrs.untagged && container_attrs.content.is_some() {
959        return Err(syn::Error::new(
960            proc_macro2::Span::call_site(),
961            "serde(untagged) cannot be combined with serde(content = ...)",
962        ));
963    }
964    if container_attrs.content.is_some() && container_attrs.tag.is_none() {
965        return Err(syn::Error::new(
966            proc_macro2::Span::call_site(),
967            "serde(content = ...) requires serde(tag = ...)",
968        ));
969    }
970
971    if container_attrs.untagged {
972        return Ok(EnumRepresentation::Untagged);
973    }
974
975    match (&container_attrs.tag, &container_attrs.content) {
976        (Some(tag), Some(content)) => Ok(EnumRepresentation::Adjacent {
977            tag: tag.clone(),
978            content: content.clone(),
979        }),
980        (Some(tag), None) => Ok(EnumRepresentation::Internal { tag: tag.clone() }),
981        (None, None) => Ok(EnumRepresentation::External),
982        (None, Some(_)) => unreachable!("validated above"),
983    }
984}
985
986fn non_external_variant_field_conflicts(
987    data_enum: &DataEnum,
988    container_attrs: &SerdeContainerAttrs,
989) -> syn::Result<NonExternalFieldConflicts> {
990    let representation = enum_representation(container_attrs)?;
991    if matches!(representation, EnumRepresentation::External) {
992        return Ok(NonExternalFieldConflicts::default());
993    }
994
995    let context = SerdeFieldContext::for_enum_variant_fields(container_attrs);
996    let mut counts = HashMap::<String, usize>::new();
997    let mut canonical_names = HashSet::new();
998    let mut alias_owners = HashMap::<String, HashSet<String>>::new();
999    let mut env_owners = HashMap::<String, HashSet<String>>::new();
1000
1001    for variant in &data_enum.variants {
1002        let variant_attrs =
1003            parse_serde_variant_attrs(&variant.attrs, &variant.ident, container_attrs)?;
1004        if variant_attrs.skip_metadata {
1005            continue;
1006        }
1007
1008        let Fields::Named(fields) = &variant.fields else {
1009            continue;
1010        };
1011
1012        let mut seen = HashSet::new();
1013        for field in &fields.named {
1014            let Some(field_ident) = &field.ident else {
1015                continue;
1016            };
1017            let serde_attrs = parse_serde_field_attrs(&field.attrs, field_ident, context)?;
1018            if serde_attrs.skip_metadata || serde_attrs.flatten {
1019                continue;
1020            }
1021            let tier_attrs = parse_tier_attrs(&field.attrs)?;
1022            let canonical_name = serde_attrs.canonical_name.clone();
1023            if seen.insert(canonical_name.clone()) {
1024                canonical_names.insert(canonical_name.clone());
1025                *counts.entry(canonical_name.clone()).or_default() += 1;
1026            }
1027            for alias in serde_attrs.aliases {
1028                alias_owners
1029                    .entry(alias)
1030                    .or_default()
1031                    .insert(canonical_name.clone());
1032            }
1033            if let Some(env) = tier_attrs.env {
1034                env_owners
1035                    .entry(env)
1036                    .or_default()
1037                    .insert(canonical_name.clone());
1038            }
1039        }
1040    }
1041
1042    let skipped_fields = counts
1043        .into_iter()
1044        .filter_map(|(path, count)| (count > 1).then_some(path))
1045        .collect::<HashSet<_>>();
1046
1047    let skipped_aliases = alias_owners
1048        .into_iter()
1049        .filter_map(|(alias, owners)| {
1050            (owners.len() > 1 || canonical_names.contains(&alias)).then_some(alias)
1051        })
1052        .collect::<HashSet<_>>();
1053
1054    let skipped_envs = env_owners
1055        .into_iter()
1056        .filter_map(|(env, owners)| (owners.len() > 1).then_some(env))
1057        .collect::<HashSet<_>>();
1058
1059    Ok(NonExternalFieldConflicts {
1060        skipped_fields,
1061        skipped_aliases,
1062        skipped_envs,
1063    })
1064}
1065
1066fn has_field_naming_attrs(attributes: &[Attribute]) -> syn::Result<bool> {
1067    let mut has_naming = false;
1068    for attribute in attributes {
1069        if !attribute.path().is_ident("serde") {
1070            continue;
1071        }
1072
1073        attribute.parse_nested_meta(|meta| {
1074            if meta.path.is_ident("rename")
1075                || meta.path.is_ident("alias")
1076                || meta.path.is_ident("flatten")
1077                || meta.path.is_ident("default")
1078            {
1079                has_naming = true;
1080            }
1081            consume_unused_meta(meta)?;
1082            Ok(())
1083        })?;
1084    }
1085
1086    Ok(has_naming)
1087}
1088
1089fn validate_merge_strategy(attrs: &TierAttrs, ty: &Type) -> syn::Result<()> {
1090    if attrs.merge.as_deref() == Some("append") && !supports_append_strategy(ty) {
1091        return Err(syn::Error::new_spanned(
1092            ty,
1093            "tier(merge = \"append\") requires a Vec<T> or array-like field",
1094        ));
1095    }
1096    Ok(())
1097}
1098
1099fn validate_validation_attrs(attrs: &TierAttrs, field_ident: &syn::Ident) -> syn::Result<()> {
1100    if let (Some(min), Some(max)) = (&attrs.min, &attrs.max)
1101        && min.value > max.value
1102    {
1103        return Err(syn::Error::new_spanned(
1104            field_ident,
1105            "tier(min = ...) cannot be greater than tier(max = ...)",
1106        ));
1107    }
1108
1109    if let (Some(min_length), Some(max_length)) = (attrs.min_length, attrs.max_length)
1110        && min_length > max_length
1111    {
1112        return Err(syn::Error::new_spanned(
1113            field_ident,
1114            "tier(min_length = ...) cannot be greater than tier(max_length = ...)",
1115        ));
1116    }
1117
1118    if attrs.one_of.is_empty()
1119        && (attrs.hostname || attrs.ip_addr || attrs.socket_addr || attrs.absolute_path)
1120    {
1121        return Ok(());
1122    }
1123
1124    if !attrs.one_of.is_empty() && (attrs.min.is_some() || attrs.max.is_some()) {
1125        return Err(syn::Error::new_spanned(
1126            field_ident,
1127            "tier(one_of(...)) cannot be combined with tier(min = ...) or tier(max = ...)",
1128        ));
1129    }
1130
1131    Ok(())
1132}
1133
1134fn container_check_tokens(attrs: &TierContainerAttrs) -> Vec<proc_macro2::TokenStream> {
1135    attrs
1136        .checks
1137        .iter()
1138        .map(|check| match check {
1139            ContainerValidationCheck::AtLeastOneOf(paths) => {
1140                let path_lits = paths
1141                    .iter()
1142                    .map(|path| LitStr::new(path, proc_macro2::Span::call_site()))
1143                    .collect::<Vec<_>>();
1144                quote! {
1145                    metadata.push_check(::tier::ValidationCheck::AtLeastOneOf {
1146                        paths: ::std::vec![#(::std::string::String::from(#path_lits)),*],
1147                    });
1148                }
1149            }
1150            ContainerValidationCheck::ExactlyOneOf(paths) => {
1151                let path_lits = paths
1152                    .iter()
1153                    .map(|path| LitStr::new(path, proc_macro2::Span::call_site()))
1154                    .collect::<Vec<_>>();
1155                quote! {
1156                    metadata.push_check(::tier::ValidationCheck::ExactlyOneOf {
1157                        paths: ::std::vec![#(::std::string::String::from(#path_lits)),*],
1158                    });
1159                }
1160            }
1161            ContainerValidationCheck::MutuallyExclusive(paths) => {
1162                let path_lits = paths
1163                    .iter()
1164                    .map(|path| LitStr::new(path, proc_macro2::Span::call_site()))
1165                    .collect::<Vec<_>>();
1166                quote! {
1167                    metadata.push_check(::tier::ValidationCheck::MutuallyExclusive {
1168                        paths: ::std::vec![#(::std::string::String::from(#path_lits)),*],
1169                    });
1170                }
1171            }
1172            ContainerValidationCheck::RequiredWith { path, requires } => {
1173                let path = LitStr::new(path, proc_macro2::Span::call_site());
1174                let requires = requires
1175                    .iter()
1176                    .map(|item| LitStr::new(item, proc_macro2::Span::call_site()))
1177                    .collect::<Vec<_>>();
1178                quote! {
1179                    metadata.push_check(::tier::ValidationCheck::RequiredWith {
1180                        path: ::std::string::String::from(#path),
1181                        requires: ::std::vec![#(::std::string::String::from(#requires)),*],
1182                    });
1183                }
1184            }
1185            ContainerValidationCheck::RequiredIf {
1186                path,
1187                equals,
1188                requires,
1189            } => {
1190                let path = LitStr::new(path, proc_macro2::Span::call_site());
1191                let requires = requires
1192                    .iter()
1193                    .map(|item| LitStr::new(item, proc_macro2::Span::call_site()))
1194                    .collect::<Vec<_>>();
1195                quote! {
1196                    metadata.push_check(::tier::ValidationCheck::RequiredIf {
1197                        path: ::std::string::String::from(#path),
1198                        equals: ::tier::ValidationValue::from(#equals),
1199                        requires: ::std::vec![#(::std::string::String::from(#requires)),*],
1200                    });
1201                }
1202            }
1203        })
1204        .collect()
1205}
1206
1207fn supports_append_strategy(ty: &Type) -> bool {
1208    let Some(inner) = metadata_inner_type(ty) else {
1209        return matches!(ty, Type::Array(_))
1210            || matches!(last_type_ident(ty).as_deref(), Some("Vec"));
1211    };
1212    supports_append_strategy(inner)
1213}
1214
1215fn parse_rename_all_meta(
1216    meta: syn::meta::ParseNestedMeta<'_>,
1217    serialize: &mut Option<RenameRule>,
1218    deserialize: &mut Option<RenameRule>,
1219) -> syn::Result<()> {
1220    if meta.input.peek(syn::Token![=]) {
1221        let literal: LitStr = meta.value()?.parse()?;
1222        let rule = RenameRule::parse(&literal.value(), literal.span())?;
1223        *serialize = Some(rule);
1224        *deserialize = Some(rule);
1225        return Ok(());
1226    }
1227
1228    meta.parse_nested_meta(|nested| {
1229        if nested.path.is_ident("serialize") {
1230            let literal: LitStr = nested.value()?.parse()?;
1231            *serialize = Some(RenameRule::parse(&literal.value(), literal.span())?);
1232            return Ok(());
1233        }
1234        if nested.path.is_ident("deserialize") {
1235            let literal: LitStr = nested.value()?.parse()?;
1236            *deserialize = Some(RenameRule::parse(&literal.value(), literal.span())?);
1237            return Ok(());
1238        }
1239        Err(nested.error("unsupported serde rename_all option"))
1240    })
1241}
1242
1243fn parse_rename_meta(
1244    meta: syn::meta::ParseNestedMeta<'_>,
1245    serialize: &mut Option<String>,
1246    deserialize: &mut Option<String>,
1247) -> syn::Result<()> {
1248    if meta.input.peek(syn::Token![=]) {
1249        let value = parse_string_value(meta)?;
1250        *serialize = Some(value.clone());
1251        *deserialize = Some(value);
1252        return Ok(());
1253    }
1254
1255    meta.parse_nested_meta(|nested| {
1256        if nested.path.is_ident("serialize") {
1257            *serialize = Some(parse_string_value(nested)?);
1258            return Ok(());
1259        }
1260        if nested.path.is_ident("deserialize") {
1261            *deserialize = Some(parse_string_value(nested)?);
1262            return Ok(());
1263        }
1264        Err(nested.error("unsupported serde rename option"))
1265    })
1266}
1267
1268fn parse_string_value(meta: syn::meta::ParseNestedMeta<'_>) -> syn::Result<String> {
1269    let literal: LitStr = meta.value()?.parse()?;
1270    Ok(literal.value())
1271}
1272
1273fn parse_usize_value(meta: syn::meta::ParseNestedMeta<'_>) -> syn::Result<usize> {
1274    let literal: syn::LitInt = meta.value()?.parse()?;
1275    literal.base10_parse()
1276}
1277
1278fn parse_string_list_call(meta: syn::meta::ParseNestedMeta<'_>) -> syn::Result<Vec<String>> {
1279    let content;
1280    syn::parenthesized!(content in meta.input);
1281    let values = Punctuated::<LitStr, syn::Token![,]>::parse_terminated(&content)?;
1282    if values.is_empty() {
1283        return Err(meta.error("expected at least one string literal"));
1284    }
1285    Ok(values.into_iter().map(|value| value.value()).collect())
1286}
1287
1288fn parse_literal_expr_list(meta: syn::meta::ParseNestedMeta<'_>) -> syn::Result<Vec<Expr>> {
1289    let content;
1290    syn::parenthesized!(content in meta.input);
1291    let values = Punctuated::<Expr, syn::Token![,]>::parse_terminated(&content)?;
1292    if values.is_empty() {
1293        return Err(meta.error("expected at least one literal value"));
1294    }
1295    let values = values.into_iter().collect::<Vec<_>>();
1296    for value in &values {
1297        validate_value_expr(value, value.span())?;
1298    }
1299    Ok(values)
1300}
1301
1302fn parse_numeric_literal(meta: syn::meta::ParseNestedMeta<'_>) -> syn::Result<NumericLiteral> {
1303    let expr: Expr = meta.value()?.parse()?;
1304    parse_numeric_expr(expr, meta.path.span())
1305}
1306
1307fn parse_numeric_expr(expr: Expr, span: proc_macro2::Span) -> syn::Result<NumericLiteral> {
1308    match expr {
1309        Expr::Lit(expr_lit) => match expr_lit.lit {
1310            Lit::Int(literal) => Ok(NumericLiteral {
1311                tokens: quote! { #literal },
1312                value: literal.base10_parse::<f64>()?,
1313            }),
1314            Lit::Float(literal) => Ok(NumericLiteral {
1315                tokens: quote! { #literal },
1316                value: literal.base10_parse::<f64>()?,
1317            }),
1318            _ => Err(syn::Error::new(
1319                span,
1320                "expected an integer or float literal",
1321            )),
1322        },
1323        Expr::Unary(expr_unary) if matches!(expr_unary.op, syn::UnOp::Neg(_)) => {
1324            match *expr_unary.expr {
1325                Expr::Lit(expr_lit) => match expr_lit.lit {
1326                    Lit::Int(literal) => Ok(NumericLiteral {
1327                        tokens: quote! { -#literal },
1328                        value: -literal.base10_parse::<f64>()?,
1329                    }),
1330                    Lit::Float(literal) => Ok(NumericLiteral {
1331                        tokens: quote! { -#literal },
1332                        value: -literal.base10_parse::<f64>()?,
1333                    }),
1334                    _ => Err(syn::Error::new(
1335                        span,
1336                        "expected an integer or float literal",
1337                    )),
1338                },
1339                _ => Err(syn::Error::new(
1340                    span,
1341                    "expected an integer or float literal",
1342                )),
1343            }
1344        }
1345        _ => Err(syn::Error::new(
1346            span,
1347            "expected an integer or float literal",
1348        )),
1349    }
1350}
1351
1352fn parse_value_expr(meta: syn::meta::ParseNestedMeta<'_>) -> syn::Result<Expr> {
1353    let expr: Expr = meta.value()?.parse()?;
1354    validate_value_expr(&expr, meta.path.span())?;
1355    Ok(expr)
1356}
1357
1358fn validate_value_expr(expr: &Expr, span: proc_macro2::Span) -> syn::Result<()> {
1359    match expr {
1360        Expr::Lit(expr_lit) => match &expr_lit.lit {
1361            Lit::Str(_) | Lit::Bool(_) | Lit::Int(_) | Lit::Float(_) => Ok(()),
1362            _ => Err(syn::Error::new(
1363                span,
1364                "expected a string, bool, integer, or float literal",
1365            )),
1366        },
1367        Expr::Unary(expr_unary) if matches!(expr_unary.op, syn::UnOp::Neg(_)) => match &*expr_unary
1368            .expr
1369        {
1370            Expr::Lit(expr_lit) if matches!(expr_lit.lit, Lit::Int(_) | Lit::Float(_)) => Ok(()),
1371            _ => Err(syn::Error::new(
1372                span,
1373                "expected a string, bool, integer, or float literal",
1374            )),
1375        },
1376        _ => Err(syn::Error::new(
1377            span,
1378            "expected a string, bool, integer, or float literal",
1379        )),
1380    }
1381}
1382
1383fn parse_required_with_container_check(
1384    meta: syn::meta::ParseNestedMeta<'_>,
1385) -> syn::Result<ContainerValidationCheck> {
1386    let mut path = None;
1387    let mut requires = Vec::new();
1388    meta.parse_nested_meta(|nested| {
1389        if nested.path.is_ident("path") {
1390            path = Some(parse_string_value(nested)?);
1391            return Ok(());
1392        }
1393        if nested.path.is_ident("requires") {
1394            requires = parse_string_list_call(nested)?;
1395            return Ok(());
1396        }
1397        Err(nested.error("unsupported required_with option"))
1398    })?;
1399
1400    let Some(path) = path else {
1401        return Err(meta.error("required_with requires `path = \"...\"`"));
1402    };
1403    if requires.is_empty() {
1404        return Err(meta.error("required_with requires `requires(\"...\")`"));
1405    }
1406
1407    Ok(ContainerValidationCheck::RequiredWith { path, requires })
1408}
1409
1410fn parse_required_if_container_check(
1411    meta: syn::meta::ParseNestedMeta<'_>,
1412) -> syn::Result<ContainerValidationCheck> {
1413    let mut path = None;
1414    let mut equals = None;
1415    let mut requires = Vec::new();
1416    meta.parse_nested_meta(|nested| {
1417        if nested.path.is_ident("path") {
1418            path = Some(parse_string_value(nested)?);
1419            return Ok(());
1420        }
1421        if nested.path.is_ident("equals") {
1422            equals = Some(parse_value_expr(nested)?);
1423            return Ok(());
1424        }
1425        if nested.path.is_ident("requires") {
1426            requires = parse_string_list_call(nested)?;
1427            return Ok(());
1428        }
1429        Err(nested.error("unsupported required_if option"))
1430    })?;
1431
1432    let Some(path) = path else {
1433        return Err(meta.error("required_if requires `path = \"...\"`"));
1434    };
1435    let Some(equals) = equals else {
1436        return Err(meta.error("required_if requires `equals = ...`"));
1437    };
1438    if requires.is_empty() {
1439        return Err(meta.error("required_if requires `requires(\"...\")`"));
1440    }
1441
1442    Ok(ContainerValidationCheck::RequiredIf {
1443        path,
1444        equals,
1445        requires,
1446    })
1447}
1448
1449fn doc_comment(attributes: &[Attribute]) -> Option<String> {
1450    let mut lines = Vec::new();
1451    for attribute in attributes {
1452        if !attribute.path().is_ident("doc") {
1453            continue;
1454        }
1455        let Meta::NameValue(name_value) = &attribute.meta else {
1456            continue;
1457        };
1458        let Expr::Lit(expr_lit) = &name_value.value else {
1459            continue;
1460        };
1461        let Lit::Str(literal) = &expr_lit.lit else {
1462            continue;
1463        };
1464        let line = literal.value().trim().to_owned();
1465        if !line.is_empty() {
1466            lines.push(line);
1467        }
1468    }
1469
1470    (!lines.is_empty()).then(|| lines.join("\n"))
1471}
1472
1473fn direct_field_metadata_tokens(
1474    accumulator: &proc_macro2::Ident,
1475    field_name: &LitStr,
1476    aliases: &[LitStr],
1477    serde_attrs: &SerdeFieldAttrs,
1478    attrs: &TierAttrs,
1479    secret_type: bool,
1480) -> syn::Result<proc_macro2::TokenStream> {
1481    let mut builder = quote! {
1482        ::tier::FieldMetadata::new(#field_name)
1483    };
1484
1485    for alias in aliases {
1486        builder = quote! { #builder.alias(#alias) };
1487    }
1488    if attrs.secret || secret_type {
1489        builder = quote! { #builder.secret() };
1490    }
1491    if let Some(env) = &attrs.env {
1492        let env = LitStr::new(env, field_name.span());
1493        builder = quote! { #builder.env(#env) };
1494    }
1495    if let Some(doc) = &attrs.doc {
1496        let doc = LitStr::new(doc, field_name.span());
1497        builder = quote! { #builder.doc(#doc) };
1498    }
1499    if let Some(example) = &attrs.example {
1500        let example = LitStr::new(example, field_name.span());
1501        builder = quote! { #builder.example(#example) };
1502    }
1503    if let Some(deprecated) = &attrs.deprecated {
1504        let deprecated = LitStr::new(deprecated, field_name.span());
1505        builder = quote! { #builder.deprecated(#deprecated) };
1506    }
1507    if serde_attrs.has_default {
1508        builder = quote! { #builder.defaulted() };
1509    }
1510    if let Some(merge) = &attrs.merge {
1511        let merge_strategy = match merge.as_str() {
1512            "merge" => quote! { ::tier::MergeStrategy::Merge },
1513            "replace" => quote! { ::tier::MergeStrategy::Replace },
1514            "append" => quote! { ::tier::MergeStrategy::Append },
1515            _ => {
1516                return Err(syn::Error::new(
1517                    field_name.span(),
1518                    "unsupported tier merge strategy, expected merge|replace|append",
1519                ));
1520            }
1521        };
1522        builder = quote! { #builder.merge_strategy(#merge_strategy) };
1523    }
1524    if attrs.non_empty {
1525        builder = quote! { #builder.non_empty() };
1526    }
1527    if let Some(min) = &attrs.min {
1528        let min = &min.tokens;
1529        builder = quote! { #builder.min(#min) };
1530    }
1531    if let Some(max) = &attrs.max {
1532        let max = &max.tokens;
1533        builder = quote! { #builder.max(#max) };
1534    }
1535    if let Some(min_length) = attrs.min_length {
1536        builder = quote! { #builder.min_length(#min_length) };
1537    }
1538    if let Some(max_length) = attrs.max_length {
1539        builder = quote! { #builder.max_length(#max_length) };
1540    }
1541    if !attrs.one_of.is_empty() {
1542        let one_of = &attrs.one_of;
1543        builder = quote! { #builder.one_of([#(#one_of),*]) };
1544    }
1545    if attrs.hostname {
1546        builder = quote! { #builder.hostname() };
1547    }
1548    if attrs.ip_addr {
1549        builder = quote! { #builder.ip_addr() };
1550    }
1551    if attrs.socket_addr {
1552        builder = quote! { #builder.socket_addr() };
1553    }
1554    if attrs.absolute_path {
1555        builder = quote! { #builder.absolute_path() };
1556    }
1557
1558    Ok(quote! {
1559        #accumulator.push(#builder);
1560    })
1561}
1562
1563fn is_secret_type(ty: &Type) -> bool {
1564    matches!(last_type_ident(ty).as_deref(), Some("Secret"))
1565}
1566
1567fn metadata_target_type(ty: &Type) -> &Type {
1568    let Some(inner) = metadata_inner_type(ty) else {
1569        return ty;
1570    };
1571    metadata_target_type(inner)
1572}
1573
1574fn metadata_inner_type(ty: &Type) -> Option<&Type> {
1575    let Type::Path(type_path) = ty else {
1576        return None;
1577    };
1578    let segment = type_path.path.segments.last()?;
1579    match segment.ident.to_string().as_str() {
1580        "Option" | "Box" | "Arc" => match &segment.arguments {
1581            PathArguments::AngleBracketed(arguments) => {
1582                arguments.args.iter().find_map(|argument| {
1583                    if let GenericArgument::Type(ty) = argument {
1584                        Some(ty)
1585                    } else {
1586                        None
1587                    }
1588                })
1589            }
1590            _ => None,
1591        },
1592        _ => None,
1593    }
1594}
1595
1596fn last_type_ident(ty: &Type) -> Option<String> {
1597    let Type::Path(type_path) = ty else {
1598        return None;
1599    };
1600    type_path
1601        .path
1602        .segments
1603        .last()
1604        .map(|segment| segment.ident.to_string())
1605}
1606
1607fn unraw(ident: &syn::Ident) -> String {
1608    ident.to_string().trim_start_matches("r#").to_owned()
1609}
1610
1611fn consume_unused_meta(meta: syn::meta::ParseNestedMeta<'_>) -> syn::Result<()> {
1612    if meta.input.peek(syn::Token![=]) {
1613        let _: Expr = meta.value()?.parse()?;
1614        return Ok(());
1615    }
1616
1617    if meta.input.peek(syn::token::Paren) {
1618        meta.parse_nested_meta(|nested| {
1619            consume_unused_meta(nested)?;
1620            Ok(())
1621        })?;
1622    }
1623
1624    Ok(())
1625}