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