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