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
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}