Skip to main content

confroid_derive/
lib.rs

1//! Derive macro for [`confroid`](https://docs.rs/confroid).
2//!
3//! `#[derive(Config)]` generates a `confroid::FromEnv` implementation for a
4//! named struct. See the `confroid` crate for usage.
5
6use darling::util::Flag;
7use darling::{FromDeriveInput, FromField, ast};
8use proc_macro::TokenStream;
9use quote::quote;
10use syn::{DeriveInput, parse_macro_input};
11
12/// An arbitrary expression captured from an attribute value.
13///
14/// darling's built-in `syn::Expr` handling rejects bare literals such as
15/// `default = 8080`, so we capture the expression ourselves — literals wrapped
16/// as literal expressions, everything else cloned verbatim.
17#[derive(Clone)]
18struct MetaExpr(#[allow(dead_code)] syn::Expr);
19
20impl darling::FromMeta for MetaExpr {
21    fn from_expr(expr: &syn::Expr) -> darling::Result<Self> {
22        Ok(MetaExpr(expr.clone()))
23    }
24
25    fn from_value(value: &syn::Lit) -> darling::Result<Self> {
26        Ok(MetaExpr(lit_to_expr(value)))
27    }
28}
29
30/// The `default` attribute: either bare (`default` -> `Default::default()`) or
31/// an explicit expression (`default = expr`).
32///
33/// This is a dedicated type rather than `Override<MetaExpr>` because darling's
34/// `Override` rejects non-literal expression values before delegating, which
35/// would reject a path default such as `default = MY_CONST`.
36#[derive(Clone)]
37enum DefaultKind {
38    Inherit,
39    Explicit(syn::Expr),
40}
41
42impl darling::FromMeta for DefaultKind {
43    fn from_word() -> darling::Result<Self> {
44        Ok(DefaultKind::Inherit)
45    }
46
47    fn from_expr(expr: &syn::Expr) -> darling::Result<Self> {
48        Ok(DefaultKind::Explicit(expr.clone()))
49    }
50
51    fn from_value(value: &syn::Lit) -> darling::Result<Self> {
52        Ok(DefaultKind::Explicit(lit_to_expr(value)))
53    }
54}
55
56fn lit_to_expr(lit: &syn::Lit) -> syn::Expr {
57    syn::Expr::Lit(syn::ExprLit {
58        attrs: Vec::new(),
59        lit: lit.clone(),
60    })
61}
62
63#[derive(FromField)]
64#[darling(attributes(confroid), forward_attrs(doc))]
65#[allow(dead_code)]
66struct FieldOpts {
67    ident: Option<syn::Ident>,
68    ty: syn::Type,
69    /// Override the derived environment variable name / prefix.
70    name: Option<String>,
71    /// `default` (bare) -> `Default::default()`, `default = expr` -> `expr`.
72    default: Option<DefaultKind>,
73    /// Example value, used only for documentation generation.
74    example: Option<MetaExpr>,
75    /// Parse a single delimited variable into a `Vec` instead of indexed keys.
76    auto_vec: Flag,
77    /// Delimiter for `auto_vec` (defaults to `,`).
78    auto_vec_delimiter: Option<String>,
79    /// Forwarded `#[doc]` attributes, used for documentation generation.
80    attrs: Vec<syn::Attribute>,
81}
82
83#[derive(FromDeriveInput)]
84#[darling(attributes(confroid), supports(struct_named))]
85struct InputOpts {
86    ident: syn::Ident,
87    generics: syn::Generics,
88    data: ast::Data<darling::util::Ignored, FieldOpts>,
89}
90
91/// Derive `confroid::FromEnv` for a named struct.
92#[proc_macro_derive(Config, attributes(confroid))]
93pub fn derive_config(input: TokenStream) -> TokenStream {
94    let di = parse_macro_input!(input as DeriveInput);
95    let opts = match InputOpts::from_derive_input(&di) {
96        Ok(opts) => opts,
97        Err(err) => return err.write_errors().into(),
98    };
99    match expand(&opts) {
100        Ok(tokens) => tokens.into(),
101        Err(err) => err.write_errors().into(),
102    }
103}
104
105fn expand(opts: &InputOpts) -> darling::Result<proc_macro2::TokenStream> {
106    let ident = &opts.ident;
107    let (impl_generics, ty_generics, where_clause) = opts.generics.split_for_impl();
108
109    let fields = opts
110        .data
111        .as_ref()
112        .take_struct()
113        .expect("supports(struct_named) guarantees a struct")
114        .fields;
115
116    let mut errors = darling::Error::accumulator();
117    let mut field_inits = Vec::with_capacity(fields.len());
118    let mut field_docs = Vec::with_capacity(fields.len());
119
120    for field in fields {
121        let fname = field
122            .ident
123            .as_ref()
124            .expect("named struct fields always have an ident");
125        let fname_str = fname.to_string();
126        let var_seg = field
127            .name
128            .clone()
129            .unwrap_or_else(|| screaming_snake(&fname_str));
130        let fty = &field.ty;
131
132        // The core read expression evaluates to `confroid::Result<#fty>`.
133        let read_expr = if field.auto_vec.is_present() {
134            match vec_inner(fty) {
135                Some(elem_ty) => {
136                    let delim = field
137                        .auto_vec_delimiter
138                        .clone()
139                        .unwrap_or_else(|| ",".to_string());
140                    auto_vec_read(&var_seg, &fname_str, elem_ty, &delim)
141                }
142                None => {
143                    errors.push(
144                        darling::Error::custom("`auto_vec` requires a `Vec<T>` field")
145                            .with_span(fty),
146                    );
147                    continue;
148                }
149            }
150        } else {
151            quote! {
152                {
153                    let __ctx = ctx.field(#var_seg, #fname_str);
154                    <#fty as ::confroid::FromEnv>::from_env(&__ctx, env)
155                }
156            }
157        };
158
159        let init = match &field.default {
160            None => quote! { #fname: (#read_expr)? },
161            Some(DefaultKind::Inherit) => quote! {
162                #fname: match #read_expr {
163                    ::core::result::Result::Ok(__v) => __v,
164                    ::core::result::Result::Err(::confroid::ConfroidError::EnvVarNotFound { .. }) =>
165                        <#fty as ::core::default::Default>::default(),
166                    ::core::result::Result::Err(__e) => return ::core::result::Result::Err(__e),
167                }
168            },
169            Some(DefaultKind::Explicit(expr)) => {
170                let default_val = default_value(expr);
171                quote! {
172                    #fname: match #read_expr {
173                        ::core::result::Result::Ok(__v) => __v,
174                        ::core::result::Result::Err(::confroid::ConfroidError::EnvVarNotFound { .. }) =>
175                            #default_val,
176                        ::core::result::Result::Err(__e) => return ::core::result::Result::Err(__e),
177                    }
178                }
179            }
180        };
181
182        field_inits.push(init);
183
184        // Documentation metadata for this field.
185        let doc_tokens = match extract_doc(&field.attrs) {
186            Some(doc) => quote! { ::core::option::Option::Some(#doc) },
187            None => quote! { ::core::option::Option::None },
188        };
189        // Default/example annotations are only rendered for displayable scalar
190        // leaves; collections (and a bare default on an `Option`) have no
191        // single displayable value.
192        let is_collection = type_is_named(fty, "Vec") || type_is_named(fty, "HashMap");
193        let is_option = type_is_named(fty, "Option");
194        let none = quote! { ::core::option::Option::None };
195        let default_doc = match &field.default {
196            Some(DefaultKind::Inherit) if !is_collection && !is_option => quote! {
197                ::core::option::Option::Some(
198                    ::std::format!("{}", <#fty as ::core::default::Default>::default())
199                )
200            },
201            Some(DefaultKind::Explicit(expr)) if !is_collection => quote! {
202                ::core::option::Option::Some(::std::format!("{}", #expr))
203            },
204            _ => none.clone(),
205        };
206        let example_doc = match &field.example {
207            Some(MetaExpr(expr)) if !is_collection => quote! {
208                ::core::option::Option::Some(::std::format!("{}", #expr))
209            },
210            _ => none.clone(),
211        };
212        field_docs.push(quote! {
213            ::confroid::FieldDoc {
214                var_seg: #var_seg,
215                name: #fname_str,
216                doc: #doc_tokens,
217                default: #default_doc,
218                example: #example_doc,
219                children: <#fty as ::confroid::Documented>::doc_fields(),
220            }
221        });
222    }
223
224    errors.finish()?;
225
226    let documented_impl = if cfg!(feature = "docs") {
227        let ident_str = ident.to_string();
228        quote! {
229            #[automatically_derived]
230            impl #impl_generics ::confroid::Documented for #ident #ty_generics #where_clause {
231                fn doc_name() -> &'static str {
232                    #ident_str
233                }
234
235                fn doc_fields() -> ::core::option::Option<::std::vec::Vec<::confroid::FieldDoc>> {
236                    ::core::option::Option::Some(::std::vec![
237                        #(#field_docs),*
238                    ])
239                }
240            }
241        }
242    } else {
243        quote! {}
244    };
245
246    Ok(quote! {
247        #[automatically_derived]
248        impl #impl_generics ::confroid::FromEnv for #ident #ty_generics #where_clause {
249            fn from_env(
250                ctx: &::confroid::Ctx,
251                env: &::confroid::Env,
252            ) -> ::confroid::Result<Self> {
253                ::core::result::Result::Ok(Self {
254                    #(#field_inits),*
255                })
256            }
257
258            fn is_present(ctx: &::confroid::Ctx, env: &::confroid::Env) -> bool {
259                !env.immediate_children(&ctx.var).is_empty()
260            }
261        }
262
263        #documented_impl
264    })
265}
266
267/// Delimited-value parsing for an `auto_vec` field.
268fn auto_vec_read(
269    var_seg: &str,
270    fname_str: &str,
271    elem_ty: &syn::Type,
272    delim: &str,
273) -> proc_macro2::TokenStream {
274    quote! {
275        {
276            let __ctx = ctx.field(#var_seg, #fname_str);
277            match env.get(&__ctx.var) {
278                ::core::option::Option::None => ::core::result::Result::Err(
279                    ::confroid::ConfroidError::EnvVarNotFound {
280                        var_name: __ctx.var.clone(),
281                        field: __ctx.path.clone(),
282                    }
283                ),
284                ::core::option::Option::Some(__raw) if __raw.is_empty() =>
285                    ::core::result::Result::Ok(::std::vec::Vec::new()),
286                ::core::option::Option::Some(__raw) => {
287                    let mut __out: ::std::vec::Vec<#elem_ty> = ::std::vec::Vec::new();
288                    let mut __result = ::core::result::Result::Ok(());
289                    for __part in __raw.split(#delim) {
290                        match __part.parse::<#elem_ty>() {
291                            ::core::result::Result::Ok(__v) => __out.push(__v),
292                            ::core::result::Result::Err(__e) => {
293                                __result = ::core::result::Result::Err(
294                                    ::confroid::ConfroidError::EnvVarInvalid {
295                                        var_name: __ctx.var.clone(),
296                                        field: __ctx.path.clone(),
297                                        value: __part.to_string(),
298                                        parser_error: __e.to_string(),
299                                    }
300                                );
301                                break;
302                            }
303                        }
304                    }
305                    __result.map(|()| __out)
306                }
307            }
308        }
309    }
310}
311
312/// Tokens for an explicit `default = expr`.
313///
314/// Numeric literals are emitted verbatim so integer/float literal inference
315/// coerces them to the field type. Everything else is wrapped in `Into::into`
316/// so that, for example, a `&str` default lands in a `String` field.
317fn default_value(expr: &syn::Expr) -> proc_macro2::TokenStream {
318    if is_numeric_literal(expr) {
319        quote! { #expr }
320    } else {
321        quote! { ::core::convert::Into::into(#expr) }
322    }
323}
324
325fn is_numeric_literal(expr: &syn::Expr) -> bool {
326    match expr {
327        syn::Expr::Lit(syn::ExprLit { lit, .. }) => {
328            matches!(lit, syn::Lit::Int(_) | syn::Lit::Float(_))
329        }
330        syn::Expr::Unary(syn::ExprUnary {
331            op: syn::UnOp::Neg(_),
332            expr,
333            ..
334        }) => is_numeric_literal(expr),
335        syn::Expr::Group(g) => is_numeric_literal(&g.expr),
336        syn::Expr::Paren(p) => is_numeric_literal(&p.expr),
337        _ => false,
338    }
339}
340
341/// Whether `ty`'s last path segment is the given identifier (e.g. `Vec`).
342fn type_is_named(ty: &syn::Type, name: &str) -> bool {
343    matches!(ty, syn::Type::Path(tp) if tp.path.segments.last().is_some_and(|s| s.ident == name))
344}
345
346/// The element type `T` of a `Vec<T>`, if `ty` is a vector.
347fn vec_inner(ty: &syn::Type) -> Option<&syn::Type> {
348    let syn::Type::Path(tp) = ty else {
349        return None;
350    };
351    let seg = tp.path.segments.last()?;
352    if seg.ident != "Vec" {
353        return None;
354    }
355    let syn::PathArguments::AngleBracketed(args) = &seg.arguments else {
356        return None;
357    };
358    args.args.iter().find_map(|arg| match arg {
359        syn::GenericArgument::Type(inner) => Some(inner),
360        _ => None,
361    })
362}
363
364/// Collect and clean the text of `#[doc]` attributes into a single string.
365fn extract_doc(attrs: &[syn::Attribute]) -> Option<String> {
366    let mut lines = Vec::new();
367    for attr in attrs {
368        if !attr.path().is_ident("doc") {
369            continue;
370        }
371        if let syn::Meta::NameValue(nv) = &attr.meta
372            && let syn::Expr::Lit(syn::ExprLit {
373                lit: syn::Lit::Str(s),
374                ..
375            }) = &nv.value
376        {
377            lines.push(s.value().trim().to_string());
378        }
379    }
380    if lines.is_empty() {
381        None
382    } else {
383        Some(lines.join(" "))
384    }
385}
386
387/// Convert an identifier to `SCREAMING_SNAKE_CASE`.
388fn screaming_snake(s: &str) -> String {
389    let mut out = String::new();
390    let mut prev_is_word = false;
391    for ch in s.chars() {
392        if ch == '_' {
393            out.push('_');
394            prev_is_word = false;
395            continue;
396        }
397        if ch.is_uppercase() && prev_is_word {
398            out.push('_');
399        }
400        out.extend(ch.to_uppercase());
401        prev_is_word = ch.is_lowercase() || ch.is_ascii_digit();
402    }
403    out
404}