Skip to main content

conflaguration_derive/
lib.rs

1use proc_macro::TokenStream;
2use proc_macro2::TokenStream as TokenStream2;
3use quote::quote;
4use syn::Data;
5use syn::DeriveInput;
6use syn::Fields;
7use syn::Lit;
8use syn::Meta;
9use syn::Token;
10use syn::parse::Parse;
11use syn::parse::ParseStream;
12use syn::punctuated::Punctuated;
13use syn::spanned::Spanned;
14
15#[proc_macro_derive(Settings, attributes(settings, setting))]
16pub fn derive_settings(input: TokenStream) -> TokenStream {
17    match derive_settings_impl(input.into()) {
18        Ok(tokens) => tokens.into(),
19        Err(err) => err.to_compile_error().into(),
20    }
21}
22
23#[proc_macro_derive(Validate, attributes(settings, setting))]
24pub fn derive_validate(input: TokenStream) -> TokenStream {
25    match derive_validate_impl(input.into()) {
26        Ok(tokens) => tokens.into(),
27        Err(err) => err.to_compile_error().into(),
28    }
29}
30
31#[proc_macro_derive(ConfigDisplay, attributes(settings, setting))]
32pub fn derive_config_display(input: TokenStream) -> TokenStream {
33    match derive_config_display_impl(input.into()) {
34        Ok(tokens) => tokens.into(),
35        Err(err) => err.to_compile_error().into(),
36    }
37}
38
39struct StructAttrs {
40    prefix: Option<String>,
41    resolve_with: Option<syn::Path>,
42}
43
44struct FieldAttrs {
45    envs: Vec<String>,
46    envs_override: bool,
47    default: Option<Lit>,
48    default_str: Option<String>,
49    use_default: bool,
50    resolve_with: Option<syn::Path>,
51    nested: bool,
52    skip: bool,
53    sensitive: bool,
54    override_prefix: Option<Option<String>>,
55}
56
57struct BracketedStrings {
58    values: Vec<String>,
59}
60
61impl Parse for BracketedStrings {
62    fn parse(input: ParseStream) -> syn::Result<Self> {
63        let content;
64        syn::bracketed!(content in input);
65        let lits: Punctuated<syn::LitStr, Token![,]> = content.parse_terminated(|input| input.parse::<syn::LitStr>(), Token![,])?;
66        Ok(Self {
67            values: lits.iter().map(syn::LitStr::value).collect(),
68        })
69    }
70}
71
72fn parse_struct_attrs(input: &DeriveInput) -> syn::Result<StructAttrs> {
73    let mut prefix = None;
74    let mut resolve_with = None;
75    for attr in &input.attrs {
76        if !attr.path().is_ident("settings") {
77            continue;
78        }
79        attr.parse_nested_meta(|meta| {
80            if meta.path.is_ident("prefix") {
81                let value = meta.value()?;
82                let lit: syn::LitStr = value.parse()?;
83                prefix = Some(lit.value());
84                return Ok(());
85            }
86            if meta.path.is_ident("resolve_with") {
87                let value = meta.value()?;
88                let lit: syn::LitStr = value.parse()?;
89                let path: syn::Path = lit.parse()?;
90                resolve_with = Some(path);
91                return Ok(());
92            }
93            Err(meta.error("unknown settings attribute"))
94        })?;
95    }
96    Ok(StructAttrs { prefix, resolve_with })
97}
98
99fn parse_env_list(meta: &syn::meta::ParseNestedMeta<'_>) -> syn::Result<Vec<String>> {
100    let value = meta.value()?;
101    if value.peek(syn::token::Bracket) {
102        let parsed: BracketedStrings = value.parse()?;
103        Ok(parsed.values)
104    } else {
105        let lit: syn::LitStr = value.parse()?;
106        Ok(vec![lit.value()])
107    }
108}
109
110fn parse_field_attrs(field: &syn::Field) -> syn::Result<FieldAttrs> {
111    let mut attrs = FieldAttrs {
112        envs: Vec::new(),
113        envs_override: false,
114        default: None,
115        default_str: None,
116        use_default: false,
117        resolve_with: None,
118        nested: false,
119        skip: false,
120        sensitive: false,
121        override_prefix: None,
122    };
123
124    for attr in &field.attrs {
125        if !attr.path().is_ident("setting") {
126            continue;
127        }
128
129        if let Meta::List(_) = &attr.meta {
130            attr.parse_nested_meta(|meta| {
131                if meta.path.is_ident("envs") {
132                    attrs.envs = parse_env_list(&meta)?;
133                    return Ok(());
134                }
135                if meta.path.is_ident("r#override") || meta.path.is_ident("override") {
136                    attrs.envs_override = true;
137                    return Ok(());
138                }
139                if meta.path.is_ident("default") {
140                    if meta.input.peek(Token![=]) {
141                        let value = meta.value()?;
142                        let lit: Lit = value.parse()?;
143                        attrs.default = Some(lit);
144                    } else {
145                        attrs.use_default = true;
146                    }
147                    return Ok(());
148                }
149                if meta.path.is_ident("default_str") {
150                    let value = meta.value()?;
151                    let lit: syn::LitStr = value.parse()?;
152                    attrs.default_str = Some(lit.value());
153                    return Ok(());
154                }
155                if meta.path.is_ident("resolve_with") {
156                    let value = meta.value()?;
157                    let lit: syn::LitStr = value.parse()?;
158                    let path: syn::Path = lit.parse()?;
159                    attrs.resolve_with = Some(path);
160                    return Ok(());
161                }
162                if meta.path.is_ident("nested") {
163                    attrs.nested = true;
164                    return Ok(());
165                }
166                if meta.path.is_ident("skip") {
167                    attrs.skip = true;
168                    return Ok(());
169                }
170                if meta.path.is_ident("sensitive") {
171                    attrs.sensitive = true;
172                    return Ok(());
173                }
174                if meta.path.is_ident("override_prefix") {
175                    if meta.input.peek(Token![=]) {
176                        let value = meta.value()?;
177                        let lit: syn::LitStr = value.parse()?;
178                        attrs.override_prefix = Some(Some(lit.value()));
179                    } else {
180                        attrs.override_prefix = Some(None);
181                    }
182                    return Ok(());
183                }
184                Err(meta.error("unknown setting attribute"))
185            })?;
186        }
187    }
188
189    let span = field.ident.as_ref().map_or_else(|| field.span(), |ident| ident.span());
190
191    let has_any_default = attrs.default.is_some() || attrs.default_str.is_some() || attrs.use_default;
192    if (attrs.default.is_some() as u8 + attrs.default_str.is_some() as u8 + attrs.use_default as u8) > 1 {
193        return Err(syn::Error::new(span, "only one of default, default = value, or default_str allowed"));
194    }
195    if attrs.skip && (has_any_default || attrs.resolve_with.is_some() || !attrs.envs.is_empty() || attrs.envs_override || attrs.nested || attrs.sensitive) {
196        return Err(syn::Error::new(span, "skip cannot be combined with other setting attributes"));
197    }
198    if attrs.nested && (has_any_default || attrs.resolve_with.is_some() || !attrs.envs.is_empty() || attrs.envs_override || attrs.sensitive) {
199        return Err(syn::Error::new(span, "nested cannot be combined with default, default_str, resolve_with, envs, override, or sensitive"));
200    }
201    if attrs.override_prefix.is_some() && !attrs.nested {
202        return Err(syn::Error::new(span, "override_prefix requires nested"));
203    }
204
205    Ok(attrs)
206}
207
208fn field_name_to_env_key(name: &str) -> String {
209    name.to_uppercase()
210}
211
212fn build_key_list(prefix: &Option<String>, field_name: &str, attrs: &FieldAttrs) -> Vec<String> {
213    let mut keys = Vec::new();
214
215    let names = if attrs.envs.is_empty() { vec![field_name_to_env_key(field_name)] } else { attrs.envs.clone() };
216
217    for name in &names {
218        let key = if attrs.envs_override {
219            name.clone()
220        } else {
221            match prefix {
222                Some(pfx) => format!("{pfx}_{name}"),
223                None => name.clone(),
224            }
225        };
226        if !keys.contains(&key) {
227            keys.push(key);
228        }
229    }
230
231    keys
232}
233
234fn gen_resolve_with_call(keys_expr: TokenStream2, func: &syn::Path, attrs: &FieldAttrs) -> TokenStream2 {
235    if let Some(lit) = &attrs.default {
236        return quote! {
237            ::conflaguration::resolve_with_or(#keys_expr, #func, #lit)?
238        };
239    }
240
241    if attrs.use_default {
242        return quote! {
243            ::conflaguration::resolve_with_or(#keys_expr, #func, ::core::default::Default::default())?
244        };
245    }
246
247    if let Some(default_str) = &attrs.default_str {
248        return quote! {
249            ::conflaguration::resolve_with_or_str(#keys_expr, #func, #default_str)?
250        };
251    }
252
253    quote! {
254        ::conflaguration::resolve_with(#keys_expr, #func)?
255    }
256}
257
258fn gen_resolve_call(keys_expr: TokenStream2, attrs: &FieldAttrs) -> TokenStream2 {
259    if let Some(func) = &attrs.resolve_with {
260        return gen_resolve_with_call(keys_expr, func, attrs);
261    }
262
263    if let Some(lit) = &attrs.default {
264        if matches!(lit, Lit::Str(_)) {
265            let lit_str = match lit {
266                Lit::Str(strlit) => strlit.value(),
267                _ => unreachable!(),
268            };
269            return quote! {
270                ::conflaguration::resolve_or_parse(#keys_expr, #lit_str)?
271            };
272        }
273        return quote! {
274            ::conflaguration::resolve_or(#keys_expr, #lit)?
275        };
276    }
277
278    if attrs.use_default {
279        return quote! {
280            ::conflaguration::resolve_or_else(#keys_expr, || ::core::default::Default::default())?
281        };
282    }
283
284    if let Some(default_str) = &attrs.default_str {
285        return quote! {
286            ::conflaguration::resolve_or_parse(#keys_expr, #default_str)?
287        };
288    }
289
290    quote! {
291        ::conflaguration::resolve(#keys_expr)?
292    }
293}
294
295enum PrefixMode<'a> {
296    Static(&'a Option<String>),
297    Dynamic,
298}
299
300fn nested_prefix(field_type: &syn::Type, attrs: &FieldAttrs, prefix_mode: &PrefixMode<'_>) -> Option<TokenStream2> {
301    match &attrs.override_prefix {
302        Some(Some(explicit)) => Some(quote! { #explicit.to_owned() }),
303        Some(None) => {
304            let pfx = match prefix_mode {
305                PrefixMode::Static(Some(pfx)) => quote! { #pfx },
306                PrefixMode::Dynamic => quote! { __prefix },
307                PrefixMode::Static(None) => return None,
308            };
309            Some(quote! {
310                match <#field_type as ::conflaguration::Settings>::PREFIX {
311                    Some(__inner) => ::std::format!("{}_{}", #pfx, __inner),
312                    None => (#pfx).to_owned(),
313                }
314            })
315        }
316        None => None,
317    }
318}
319
320fn gen_nested_construct(field_type: &syn::Type, prefix: Option<TokenStream2>) -> TokenStream2 {
321    match prefix {
322        Some(pfx) => quote! {
323            { let __nested = #pfx; <#field_type as ::conflaguration::Settings>::from_env_with_prefix(&__nested)? }
324        },
325        None => quote! { <#field_type as ::conflaguration::Settings>::from_env()? },
326    }
327}
328
329fn gen_nested_override(field_name: &syn::Ident, prefix: Option<TokenStream2>) -> TokenStream2 {
330    match prefix {
331        Some(pfx) => quote! {
332            { let __nested = #pfx; ::conflaguration::Settings::override_from_env_with_prefix(&mut self.#field_name, &__nested)?; }
333        },
334        None => quote! { ::conflaguration::Settings::override_from_env(&mut self.#field_name)?; },
335    }
336}
337
338fn dynamic_key_tokens(field_name_str: &str, attrs: &FieldAttrs) -> (TokenStream2, TokenStream2) {
339    let names = if attrs.envs.is_empty() { vec![field_name_to_env_key(field_name_str)] } else { attrs.envs.clone() };
340    let names_ref = &names;
341    let keys_setup = if attrs.envs_override {
342        quote! { let __keys: Vec<String> = vec![#(#names_ref.to_string()),*]; }
343    } else {
344        quote! { let __keys: Vec<String> = vec![#(::std::format!("{}_{}", __prefix, #names_ref)),*]; }
345    };
346    let refs_setup = quote! { let __key_refs: Vec<&str> = __keys.iter().map(|s| s.as_str()).collect(); };
347    (keys_setup, refs_setup)
348}
349
350fn gen_override_guard(field_name: &syn::Ident, keys_ref: TokenStream2, resolve_with: Option<&syn::Path>) -> TokenStream2 {
351    let assign = match resolve_with {
352        Some(func) => quote! {
353            self.#field_name = ::conflaguration::resolve_with(#keys_ref, #func)?;
354        },
355        None => quote! {
356            self.#field_name = ::conflaguration::resolve(#keys_ref)?;
357        },
358    };
359    quote! {
360        if (#keys_ref).iter().any(|__k| ::std::env::var(__k).is_ok()) {
361            #assign
362        }
363    }
364}
365
366fn gen_construct_resolve(field_name_str: &str, attrs: &FieldAttrs, prefix_mode: &PrefixMode<'_>) -> TokenStream2 {
367    match prefix_mode {
368        PrefixMode::Static(prefix) => {
369            let keys = build_key_list(prefix, field_name_str, attrs);
370            let keys_ref = &keys;
371            gen_resolve_call(quote! { &[#(#keys_ref),*] }, attrs)
372        }
373        PrefixMode::Dynamic => {
374            let (keys_setup, refs_setup) = dynamic_key_tokens(field_name_str, attrs);
375            let resolve = gen_resolve_call(quote! { &__key_refs }, attrs);
376            quote! { { #keys_setup #refs_setup #resolve } }
377        }
378    }
379}
380
381fn gen_override_resolve(field_name: &syn::Ident, field_name_str: &str, attrs: &FieldAttrs, prefix_mode: &PrefixMode<'_>) -> TokenStream2 {
382    match prefix_mode {
383        PrefixMode::Static(prefix) => {
384            let keys = build_key_list(prefix, field_name_str, attrs);
385            let keys_ref = &keys;
386            let keys_expr = quote! { &[#(#keys_ref),*] };
387            let guard = gen_override_guard(field_name, quote! { __keys }, attrs.resolve_with.as_ref());
388            quote! { { let __keys: &[&str] = #keys_expr; #guard } }
389        }
390        PrefixMode::Dynamic => {
391            let (keys_setup, refs_setup) = dynamic_key_tokens(field_name_str, attrs);
392            let guard = gen_override_guard(field_name, quote! { &__key_refs }, attrs.resolve_with.as_ref());
393            quote! { { #keys_setup #refs_setup #guard } }
394        }
395    }
396}
397
398fn gen_field_construct(field: &syn::Field, prefix_mode: &PrefixMode<'_>, struct_attrs: &StructAttrs) -> syn::Result<TokenStream2> {
399    let field_name = field
400        .ident
401        .as_ref()
402        .ok_or_else(|| syn::Error::new(field.span(), "tuple struct fields not supported"))?;
403    let mut attrs = parse_field_attrs(field)?;
404
405    if attrs.resolve_with.is_none() && attrs.default.is_none() && !attrs.use_default {
406        attrs.resolve_with.clone_from(&struct_attrs.resolve_with);
407    }
408
409    if attrs.skip {
410        return Ok(quote! { ::core::default::Default::default() });
411    }
412    if attrs.nested {
413        let prefix = nested_prefix(&field.ty, &attrs, prefix_mode);
414        return Ok(gen_nested_construct(&field.ty, prefix));
415    }
416    Ok(gen_construct_resolve(&field_name.to_string(), &attrs, prefix_mode))
417}
418
419fn gen_field_override(field: &syn::Field, prefix_mode: &PrefixMode<'_>, struct_attrs: &StructAttrs) -> syn::Result<TokenStream2> {
420    let field_name = field
421        .ident
422        .as_ref()
423        .ok_or_else(|| syn::Error::new(field.span(), "tuple struct fields not supported"))?;
424    let mut attrs = parse_field_attrs(field)?;
425
426    if attrs.resolve_with.is_none() && attrs.default.is_none() && !attrs.use_default {
427        attrs.resolve_with.clone_from(&struct_attrs.resolve_with);
428    }
429
430    if attrs.skip {
431        return Ok(quote! {});
432    }
433    if attrs.nested {
434        let prefix = nested_prefix(&field.ty, &attrs, prefix_mode);
435        return Ok(gen_nested_override(field_name, prefix));
436    }
437    Ok(gen_override_resolve(field_name, &field_name.to_string(), &attrs, prefix_mode))
438}
439
440fn derive_settings_impl(input: TokenStream2) -> syn::Result<TokenStream2> {
441    let input: DeriveInput = syn::parse2(input)?;
442    let struct_attrs = parse_struct_attrs(&input)?;
443
444    let fields = match &input.data {
445        Data::Struct(data) => match &data.fields {
446            Fields::Named(named) => &named.named,
447            _ => return Err(syn::Error::new(input.ident.span(), "only named struct fields supported")),
448        },
449        _ => return Err(syn::Error::new(input.ident.span(), "Settings can only be derived on structs")),
450    };
451
452    let static_prefix = PrefixMode::Static(&struct_attrs.prefix);
453    let dynamic_prefix = PrefixMode::Dynamic;
454
455    let mut static_exprs = Vec::new();
456    let mut dynamic_exprs = Vec::new();
457    let mut override_static_stmts = Vec::new();
458    let mut override_dynamic_stmts = Vec::new();
459    for field in fields {
460        let field_name = field
461            .ident
462            .as_ref()
463            .ok_or_else(|| syn::Error::new(field.span(), "tuple struct fields not supported"))?;
464        let static_expr = gen_field_construct(field, &static_prefix, &struct_attrs)?;
465        let dynamic_expr = gen_field_construct(field, &dynamic_prefix, &struct_attrs)?;
466        let override_static = gen_field_override(field, &static_prefix, &struct_attrs)?;
467        let override_dynamic = gen_field_override(field, &dynamic_prefix, &struct_attrs)?;
468        static_exprs.push(quote! { #field_name: #static_expr });
469        dynamic_exprs.push(quote! { #field_name: #dynamic_expr });
470        override_static_stmts.push(override_static);
471        override_dynamic_stmts.push(override_dynamic);
472    }
473
474    let struct_name = &input.ident;
475    let (impl_generics, type_generics, where_clause) = input.generics.split_for_impl();
476
477    let prefix_const = match &struct_attrs.prefix {
478        Some(pfx) => quote! { const PREFIX: Option<&'static str> = Some(#pfx); },
479        None => quote! { const PREFIX: Option<&'static str> = None; },
480    };
481
482    Ok(quote! {
483        impl #impl_generics ::conflaguration::Settings for #struct_name #type_generics #where_clause {
484            #prefix_const
485
486            fn from_env() -> ::conflaguration::Result<Self> {
487                Ok(Self {
488                    #(#static_exprs),*
489                })
490            }
491
492            fn from_env_with_prefix(__prefix: &str) -> ::conflaguration::Result<Self> {
493                Ok(Self {
494                    #(#dynamic_exprs),*
495                })
496            }
497
498            fn override_from_env(&mut self) -> ::conflaguration::Result<()> {
499                #(#override_static_stmts)*
500                Ok(())
501            }
502
503            fn override_from_env_with_prefix(&mut self, __prefix: &str) -> ::conflaguration::Result<()> {
504                #(#override_dynamic_stmts)*
505                Ok(())
506            }
507        }
508    })
509}
510
511fn derive_validate_impl(input: TokenStream2) -> syn::Result<TokenStream2> {
512    let input: DeriveInput = syn::parse2(input)?;
513
514    let fields = match &input.data {
515        Data::Struct(data) => match &data.fields {
516            Fields::Named(named) => &named.named,
517            _ => return Err(syn::Error::new(input.ident.span(), "only named struct fields supported")),
518        },
519        _ => return Err(syn::Error::new(input.ident.span(), "Validate can only be derived on structs")),
520    };
521
522    let mut validate_calls = Vec::new();
523    for field in fields {
524        let field_name = field
525            .ident
526            .as_ref()
527            .ok_or_else(|| syn::Error::new(field.span(), "tuple struct fields not supported"))?;
528        let field_name_str = field_name.to_string();
529        let attrs = parse_field_attrs(field)?;
530
531        if attrs.nested {
532            validate_calls.push(quote! {
533                if let Err(__err) = ::conflaguration::Validate::validate(&self.#field_name) {
534                    match __err {
535                        ::conflaguration::Error::Validation { errors: __inner } => {
536                            for mut __ve in __inner {
537                                __ve.prepend_path(#field_name_str);
538                                __errors.push(__ve);
539                            }
540                        }
541                        __other => return Err(__other),
542                    }
543                }
544            });
545        }
546    }
547
548    let struct_name = &input.ident;
549    let (impl_generics, type_generics, where_clause) = input.generics.split_for_impl();
550
551    if validate_calls.is_empty() {
552        return Ok(quote! {
553            impl #impl_generics ::conflaguration::Validate for #struct_name #type_generics #where_clause {
554                fn validate(&self) -> ::conflaguration::Result<()> {
555                    Ok(())
556                }
557            }
558        });
559    }
560
561    Ok(quote! {
562        impl #impl_generics ::conflaguration::Validate for #struct_name #type_generics #where_clause {
563            fn validate(&self) -> ::conflaguration::Result<()> {
564                let mut __errors: Vec<::conflaguration::ValidationMessage> = vec![];
565                #(#validate_calls)*
566                if __errors.is_empty() {
567                    Ok(())
568                } else {
569                    Err(::conflaguration::Error::Validation { errors: __errors })
570                }
571            }
572        }
573    })
574}
575
576fn gen_display_skip(field_name_str: &str, field_name: &syn::Ident) -> TokenStream2 {
577    quote! { ::std::writeln!(__f, "{}{} = {:?} (skipped)", __indent, #field_name_str, self.#field_name)?; }
578}
579
580fn gen_display_nested_static(field_name_str: &str, field_name: &syn::Ident) -> TokenStream2 {
581    quote! {
582        ::std::writeln!(__f, "{}{}:", __indent, #field_name_str)?;
583        ::conflaguration::ConfigDisplay::fmt_config(&self.#field_name, __f, __depth + 1)?;
584    }
585}
586
587fn gen_display_nested_dynamic(field_name_str: &str, field_name: &syn::Ident, field_type: &syn::Type, attrs: &FieldAttrs) -> TokenStream2 {
588    match &attrs.override_prefix {
589        Some(Some(explicit)) => quote! {
590            ::std::writeln!(__f, "{}{}:", __indent, #field_name_str)?;
591            ::conflaguration::ConfigDisplay::fmt_config_with_prefix(&self.#field_name, __f, __depth + 1, #explicit)?;
592        },
593        Some(None) => quote! {
594            ::std::writeln!(__f, "{}{}:", __indent, #field_name_str)?;
595            {
596                let __nested_pfx = match <#field_type as ::conflaguration::Settings>::PREFIX {
597                    Some(__inner) => ::std::format!("{}_{}", __prefix, __inner),
598                    None => __prefix.to_string(),
599                };
600                ::conflaguration::ConfigDisplay::fmt_config_with_prefix(&self.#field_name, __f, __depth + 1, &__nested_pfx)?;
601            }
602        },
603        None => quote! {
604            ::std::writeln!(__f, "{}{}:", __indent, #field_name_str)?;
605            ::conflaguration::ConfigDisplay::fmt_config(&self.#field_name, __f, __depth + 1)?;
606        },
607    }
608}
609
610fn gen_display_value(field_name_str: &str, field_name: &syn::Ident, attrs: &FieldAttrs, keys_display_expr: TokenStream2) -> TokenStream2 {
611    if attrs.sensitive {
612        quote! { ::std::writeln!(__f, "{}{} = *** ({})", __indent, #field_name_str, #keys_display_expr)?; }
613    } else {
614        quote! { ::std::writeln!(__f, "{}{} = {:?} ({})", __indent, #field_name_str, self.#field_name, #keys_display_expr)?; }
615    }
616}
617
618fn derive_config_display_impl(input: TokenStream2) -> syn::Result<TokenStream2> {
619    let input: DeriveInput = syn::parse2(input)?;
620    let struct_attrs = parse_struct_attrs(&input)?;
621
622    let fields = match &input.data {
623        Data::Struct(data) => match &data.fields {
624            Fields::Named(named) => &named.named,
625            _ => return Err(syn::Error::new(input.ident.span(), "only named struct fields supported")),
626        },
627        _ => return Err(syn::Error::new(input.ident.span(), "ConfigDisplay can only be derived on structs")),
628    };
629
630    let mut static_lines = Vec::new();
631    let mut dynamic_lines = Vec::new();
632
633    for field in fields {
634        let field_name = field
635            .ident
636            .as_ref()
637            .ok_or_else(|| syn::Error::new(field.span(), "tuple struct fields not supported"))?;
638        let field_name_str = field_name.to_string();
639        let attrs = parse_field_attrs(field)?;
640
641        let static_keys = build_key_list(&struct_attrs.prefix, &field_name_str, &attrs);
642        let static_keys_display = static_keys.join(", ");
643        static_lines.push(if attrs.skip {
644            gen_display_skip(&field_name_str, field_name)
645        } else if attrs.nested {
646            gen_display_nested_static(&field_name_str, field_name)
647        } else {
648            gen_display_value(&field_name_str, field_name, &attrs, quote! { #static_keys_display })
649        });
650
651        let names = if attrs.envs.is_empty() {
652            vec![field_name_to_env_key(&field_name_str)]
653        } else {
654            attrs.envs.clone()
655        };
656        let names_ref = &names;
657        let dynamic_keys_expr = if attrs.envs_override {
658            let joined = names.join(", ");
659            quote! { #joined }
660        } else {
661            quote! {
662                {
663                    let __keys: Vec<String> = vec![#(::std::format!("{}_{}", __prefix, #names_ref)),*];
664                    __keys.join(", ")
665                }
666            }
667        };
668        dynamic_lines.push(if attrs.skip {
669            gen_display_skip(&field_name_str, field_name)
670        } else if attrs.nested {
671            gen_display_nested_dynamic(&field_name_str, field_name, &field.ty, &attrs)
672        } else {
673            gen_display_value(&field_name_str, field_name, &attrs, dynamic_keys_expr)
674        });
675    }
676
677    let struct_name = &input.ident;
678    let (impl_generics, type_generics, where_clause) = input.generics.split_for_impl();
679
680    Ok(quote! {
681        impl #impl_generics ::conflaguration::ConfigDisplay for #struct_name #type_generics #where_clause {
682            fn fmt_config(&self, __f: &mut ::std::fmt::Formatter<'_>, __depth: usize) -> ::std::fmt::Result {
683                let __indent = "  ".repeat(__depth);
684                #(#static_lines)*
685                Ok(())
686            }
687
688            fn fmt_config_with_prefix(&self, __f: &mut ::std::fmt::Formatter<'_>, __depth: usize, __prefix: &str) -> ::std::fmt::Result {
689                let __indent = "  ".repeat(__depth);
690                #(#dynamic_lines)*
691                Ok(())
692            }
693        }
694
695        impl #impl_generics ::std::fmt::Display for #struct_name #type_generics #where_clause {
696            fn fmt(&self, __f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
697                ::conflaguration::ConfigDisplay::fmt_config(self, __f, 0)
698            }
699        }
700    })
701}
702
703#[cfg(test)]
704mod tests {
705    use super::*;
706
707    #[test]
708    fn settings_rejects_enum() {
709        let input: TokenStream2 = quote! { enum Foo { A, B } };
710        let result = derive_settings_impl(input);
711        let err = result.unwrap_err();
712        assert!(err.to_string().contains("structs"));
713    }
714
715    #[test]
716    fn settings_rejects_tuple_struct() {
717        let input: TokenStream2 = quote! { struct Foo(u16); };
718        let result = derive_settings_impl(input);
719        let err = result.unwrap_err();
720        assert!(err.to_string().contains("named"));
721    }
722
723    #[test]
724    fn validate_rejects_enum() {
725        let input: TokenStream2 = quote! { enum Bar { X } };
726        let result = derive_validate_impl(input);
727        let err = result.unwrap_err();
728        assert!(err.to_string().contains("structs"));
729    }
730
731    #[test]
732    fn validate_rejects_tuple_struct() {
733        let input: TokenStream2 = quote! { struct Bar(String); };
734        let result = derive_validate_impl(input);
735        let err = result.unwrap_err();
736        assert!(err.to_string().contains("named"));
737    }
738
739    #[test]
740    fn config_display_rejects_enum() {
741        let input: TokenStream2 = quote! { enum Baz { Y } };
742        let result = derive_config_display_impl(input);
743        let err = result.unwrap_err();
744        assert!(err.to_string().contains("structs"));
745    }
746
747    #[test]
748    fn unknown_settings_attribute_errors() {
749        let input: TokenStream2 = quote! {
750            #[settings(bogus = "nope")]
751            struct Bad {
752                field: u16,
753            }
754        };
755        let result = derive_settings_impl(input);
756        assert!(result.is_err());
757    }
758
759    #[test]
760    fn unknown_setting_field_attribute_errors() {
761        let input: TokenStream2 = quote! {
762            struct Bad {
763                #[setting(bogus)]
764                field: u16,
765            }
766        };
767        let result = derive_settings_impl(input);
768        assert!(result.is_err());
769    }
770}