Skip to main content

cfg_derive/
lib.rs

1//! Auto derive FromConfig.
2#![cfg_attr(docsrs, feature(doc_cfg))]
3#![warn(
4    anonymous_parameters,
5    missing_copy_implementations,
6    missing_debug_implementations,
7    missing_docs,
8    nonstandard_style,
9    rust_2018_idioms,
10    single_use_lifetimes,
11    trivial_casts,
12    trivial_numeric_casts,
13    unreachable_pub,
14    unused_extern_crates,
15    unused_qualifications,
16    variant_size_differences
17)]
18use quote::{__private::TokenStream, quote};
19use std::collections::BTreeMap;
20use syn::spanned::Spanned;
21use syn::*;
22
23#[allow(missing_docs)]
24#[proc_macro_derive(FromConfig, attributes(config, validate))]
25pub fn derive_config(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
26    let input: DeriveInput = parse_macro_input!(input as DeriveInput);
27    let name = input.ident.clone();
28    let body = match input.data {
29        Data::Struct(data) => derive_config_struct(&name, input.attrs, data),
30        _ => Err(Error::new_spanned(name, "Only support struct")),
31    };
32    let body = match body {
33        Ok(tokens) => tokens,
34        Err(err) => err.to_compile_error(),
35    };
36    proc_macro::TokenStream::from(body)
37}
38
39fn derive_config_struct(
40    name: &Ident,
41    attrs: Vec<Attribute>,
42    data: DataStruct,
43) -> Result<TokenStream> {
44    // Resolve cfg-rs crate path without relying on proc_macro_crate.
45    // Default to ::cfg_rs, allow override via #[config(crate = "your_crate_name")]
46    let mut cfg_crate_path = quote!(::cfg_rs);
47
48    let prefix = match derive_config_prefix(attrs, &mut cfg_crate_path) {
49        Some(p) => quote! {
50            #[automatically_derived]
51            impl #cfg_crate_path::FromConfigWithPrefix for #name {
52                fn prefix() -> &'static str {
53                    #p
54                }
55            }
56        },
57        _ => quote! {},
58    };
59
60    let fields = derive_config_fields(data)?;
61    let fs: Vec<Ident> = fields.iter().map(|f| f.name.clone()).collect();
62    #[cfg(feature = "regex")]
63    let regex_map = {
64        use quote::ToTokens;
65        let mut map: BTreeMap<String, Ident> = BTreeMap::new();
66        let mut idx = 0usize;
67        for field in &fields {
68            for rule in &field.validates {
69                if let ValidateRule::Regex { pattern, .. } = rule {
70                    let key = pattern.to_token_stream().to_string();
71                    if !map.contains_key(&key) {
72                        let ident = quote::format_ident!("__CFG_REGEX_{}", idx);
73                        idx += 1;
74                        map.insert(key, ident);
75                    }
76                }
77            }
78        }
79        map
80    };
81    #[cfg(not(feature = "regex"))]
82    let regex_map: BTreeMap<String, Ident> = BTreeMap::new();
83
84    let regex_cache_decl = if regex_map.is_empty() {
85        quote! {}
86    } else {
87        let decls = regex_map.values().map(|ident| {
88            quote! {
89                static #ident: ::std::sync::OnceLock<
90                    ::core::result::Result<::regex::Regex, ::std::string::String>
91                > = ::std::sync::OnceLock::new();
92            }
93        });
94        quote! { #(#decls)* }
95    };
96
97    let parse_fields: Vec<TokenStream> = fields
98        .iter()
99        .map(|f| build_parse_and_validate(f, &cfg_crate_path, &regex_map))
100        .collect();
101
102    Ok(quote! {
103        #[automatically_derived]
104        impl #cfg_crate_path::FromConfig for #name {
105            fn from_config(
106                context: &mut #cfg_crate_path::ConfigContext<'_>,
107                value: ::core::option::Option<#cfg_crate_path::ConfigValue<'_>>,
108            ) -> ::core::result::Result<Self, #cfg_crate_path::ConfigError> {
109                #regex_cache_decl
110                #(#parse_fields)*
111                ::core::result::Result::Ok(Self {
112                    #(#fs,)*
113                })
114            }
115        }
116
117        #prefix
118    })
119}
120
121fn derive_config_prefix(attrs: Vec<Attribute>, crate_path: &mut TokenStream) -> Option<String> {
122    let mut prefix = None;
123    for attr in attrs {
124        if attr.path().is_ident("config") {
125            attr.parse_nested_meta(|meta| {
126                if meta.path.is_ident("prefix") {
127                    let value = meta.value()?;
128                    let s: LitStr = value.parse()?;
129                    prefix = Some(s.value());
130                    Ok(())
131                } else if meta.path.is_ident("crate") {
132                    let value = meta.value()?;
133                    let s: LitStr = value.parse()?;
134                    let ident = Ident::new(&s.value(), s.span());
135                    *crate_path = quote!(#ident);
136                    Ok(())
137                } else {
138                    Err(meta.error("Only support prefix"))
139                }
140            })
141            .unwrap();
142        }
143        if prefix.is_some() {
144            break;
145        }
146    }
147    prefix
148}
149
150struct FieldInfo {
151    name: Ident,
152    def: Option<String>,
153    ren: String,
154    desc: Option<String>,
155    ty: Type,
156    validates: Vec<ValidateRule>,
157}
158
159fn derive_config_fields(data: DataStruct) -> Result<Vec<FieldInfo>> {
160    if let Fields::Named(fields) = data.fields {
161        let mut fs = vec![];
162        for field in fields.named {
163            fs.push(derive_config_field(field)?);
164        }
165        return Ok(fs);
166    }
167    Err(Error::new_spanned(data.fields, "Only support named body"))
168}
169
170fn derive_config_field(field: Field) -> Result<FieldInfo> {
171    let name = field.ident.expect("Not possible");
172    let mut f = FieldInfo {
173        ren: name.to_string(),
174        name,
175        def: None,
176        desc: None,
177        ty: field.ty.clone(),
178        validates: vec![],
179    };
180    derive_config_field_attr(&mut f, field.attrs)?;
181    Ok(f)
182}
183
184fn derive_config_field_attr(f: &mut FieldInfo, attrs: Vec<Attribute>) -> Result<()> {
185    for attr in attrs {
186        if attr.path().is_ident("config") {
187            attr.parse_nested_meta(|meta| {
188                if meta.path.is_ident("default") {
189                    f.def = Some(parse_lit(meta.value()?.parse::<Lit>()?));
190                } else if meta.path.is_ident("name") {
191                    f.ren = parse_lit(meta.value()?.parse::<Lit>()?);
192                } else if meta.path.is_ident("desc") {
193                    f.desc = Some(parse_lit(meta.value()?.parse::<Lit>()?));
194                } else {
195                    return Err(meta.error("Only support default/name/desc"));
196                }
197                Ok(())
198            })?;
199        } else if attr.path().is_ident("validate") {
200            parse_validate_attr(f, attr)?;
201        }
202    }
203    Ok(())
204}
205
206enum ValidateRule {
207    Range {
208        min: Option<Expr>,
209        max: Option<Expr>,
210        message: Option<LitStr>,
211    },
212    NotEmpty {
213        message: Option<LitStr>,
214    },
215    Length {
216        min: Option<Expr>,
217        max: Option<Expr>,
218        message: Option<LitStr>,
219    },
220    #[cfg(feature = "regex")]
221    Regex {
222        pattern: Expr,
223        message: Option<LitStr>,
224    },
225    Custom {
226        path: Path,
227        message: Option<LitStr>,
228    },
229}
230
231fn parse_validate_attr(f: &mut FieldInfo, attr: Attribute) -> Result<()> {
232    let mut rules: Vec<ValidateRule> = Vec::new();
233    let mut message_seen = false;
234    attr.parse_nested_meta(|meta| {
235        let item = meta.path.get_ident().map(|i| i.to_string());
236        let ret = match item.as_deref() {
237            Some("range") => {
238                let mut min: Option<Expr> = None;
239                let mut max: Option<Expr> = None;
240                meta.parse_nested_meta(|inner| {
241                    if inner.path.is_ident("min") {
242                        let value = inner.value()?;
243                        min = Some(value.parse::<Expr>()?);
244                        Ok(())
245                    } else if inner.path.is_ident("max") {
246                        let value = inner.value()?;
247                        max = Some(value.parse::<Expr>()?);
248                        Ok(())
249                    } else {
250                        Err(inner.error("Only support min/max"))
251                    }
252                })?;
253                rules.push(ValidateRule::Range {
254                    min,
255                    max,
256                    message: None,
257                });
258                Ok(())
259            }
260            Some("length") => {
261                let mut min: Option<Expr> = None;
262                let mut max: Option<Expr> = None;
263                meta.parse_nested_meta(|inner| {
264                    if inner.path.is_ident("min") {
265                        let value = inner.value()?;
266                        min = Some(value.parse::<Expr>()?);
267                        Ok(())
268                    } else if inner.path.is_ident("max") {
269                        let value = inner.value()?;
270                        max = Some(value.parse::<Expr>()?);
271                        Ok(())
272                    } else {
273                        Err(inner.error("Only support min/max"))
274                    }
275                })?;
276                rules.push(ValidateRule::Length {
277                    min,
278                    max,
279                    message: None,
280                });
281                Ok(())
282            }
283            Some("not_empty") => {
284                rules.push(ValidateRule::NotEmpty { message: None });
285                Ok(())
286            }
287            #[cfg(feature = "regex")]
288            Some("regex") => {
289                let value = meta.value()?;
290                let s: Expr = value.parse()?;
291                rules.push(ValidateRule::Regex {
292                    pattern: s,
293                    message: None,
294                });
295                Ok(())
296            }
297            Some("custom") => {
298                let value = meta.value()?;
299                let path = if let Ok(p) = value.parse::<Path>() {
300                    p
301                } else {
302                    let s: LitStr = value.parse()?;
303                    parse_str::<Path>(&s.value()).map_err(|err| {
304                        Error::new(
305                            s.span(),
306                            format!("custom validator must be a valid path: {}", err),
307                        )
308                    })?
309                };
310                rules.push(ValidateRule::Custom {
311                    path,
312                    message: None,
313                });
314                Ok(())
315            }
316            Some("message") => {
317                let message = if let Ok(value) = meta.value() {
318                    value.parse::<LitStr>()?
319                } else {
320                    meta.input.parse::<LitStr>()?
321                };
322                if rules.is_empty() {
323                    return Err(meta.error("validate message must follow a rule"));
324                }
325                if message_seen {
326                    return Err(meta.error("Only one message allowed per validate attribute"));
327                }
328                message_seen = true;
329                if let Some(last) = rules.pop() {
330                    if validate_rule_has_message(&last) {
331                        return Err(meta.error("validate message already set for this rule"));
332                    }
333                    rules.push(apply_validate_message(last, Some(message)));
334                }
335                Ok(())
336            }
337            _ => Err(meta.error("Only support range/length/not_empty/regex/custom/message")),
338        };
339
340        ret
341    })?;
342
343    if rules.is_empty() {
344        return Err(Error::new(
345            attr.span(),
346            "validate attribute must contain a rule",
347        ));
348    }
349    f.validates.extend(rules);
350    Ok(())
351}
352
353fn validate_rule_has_message(rule: &ValidateRule) -> bool {
354    match rule {
355        ValidateRule::Range { message, .. }
356        | ValidateRule::NotEmpty { message }
357        | ValidateRule::Length { message, .. }
358        | ValidateRule::Custom { message, .. } => message.is_some(),
359        #[cfg(feature = "regex")]
360        ValidateRule::Regex { message, .. } => message.is_some(),
361    }
362}
363
364fn apply_validate_message(rule: ValidateRule, message: Option<LitStr>) -> ValidateRule {
365    match rule {
366        ValidateRule::Range { min, max, .. } => ValidateRule::Range { min, max, message },
367        ValidateRule::NotEmpty { .. } => ValidateRule::NotEmpty { message },
368        ValidateRule::Length { min, max, .. } => ValidateRule::Length { min, max, message },
369        #[cfg(feature = "regex")]
370        ValidateRule::Regex { pattern, .. } => ValidateRule::Regex { pattern, message },
371        ValidateRule::Custom { path, .. } => ValidateRule::Custom { path, message },
372    }
373}
374
375fn build_parse_and_validate(
376    field: &FieldInfo,
377    crate_path: &TokenStream,
378    regex_map: &BTreeMap<String, Ident>,
379) -> TokenStream {
380    let name = &field.name;
381    let ty = &field.ty;
382    let key = field.ren.as_str();
383    let def = match &field.def {
384        Some(d) => quote! {,Some(#d.into())},
385        None => quote! {,None},
386    };
387    let validate = build_validate_block(field, crate_path, regex_map);
388    if field.validates.is_empty() {
389        quote! {
390            let #name: #ty = context.parse_config(#key #def)?;
391        }
392    } else {
393        quote! {
394            let #name: #ty = context.parse_config(#key #def)?;
395            #validate
396        }
397    }
398}
399
400fn build_validate_block(
401    field: &FieldInfo,
402    crate_path: &TokenStream,
403    regex_map: &BTreeMap<String, Ident>,
404) -> TokenStream {
405    if field.validates.is_empty() {
406        return quote! {};
407    }
408
409    let name = &field.name;
410    let key = field.ren.as_str();
411    let is_option = option_inner(&field.ty).is_some();
412
413    let field_key_init = quote! {
414        let field_key = || {
415            let current_key = context.current_key();
416            if current_key.is_empty() {
417                #key.to_string()
418            } else {
419                format!("{}.{}", current_key, #key)
420            }
421        };
422    };
423    let field_key_expr = quote! { &field_key };
424
425    if is_option {
426        let value_expr = quote! { value };
427        let checks: Vec<TokenStream> = field
428            .validates
429            .iter()
430            .map(|rule| {
431                build_validate_rule(rule, crate_path, &field_key_expr, &value_expr, regex_map)
432            })
433            .collect();
434        quote! {
435            #field_key_init
436            if let ::core::option::Option::Some(value) = #name.as_ref() {
437                #(#checks)*
438            }
439        }
440    } else {
441        let value_expr = quote! { &#name };
442        let checks: Vec<TokenStream> = field
443            .validates
444            .iter()
445            .map(|rule| {
446                build_validate_rule(rule, crate_path, &field_key_expr, &value_expr, regex_map)
447            })
448            .collect();
449        quote! {
450            #field_key_init
451            #(#checks)*
452        }
453    }
454}
455
456fn build_validate_rule(
457    rule: &ValidateRule,
458    crate_path: &TokenStream,
459    field_key: &TokenStream,
460    value: &TokenStream,
461    _regex_map: &BTreeMap<String, Ident>,
462) -> TokenStream {
463    match rule {
464        ValidateRule::Range { min, max, message } => {
465            let min_expr = min
466                .as_ref()
467                .map(|v| quote! { ::core::option::Option::Some(&#v) });
468            let max_expr = max
469                .as_ref()
470                .map(|v| quote! { ::core::option::Option::Some(&#v) });
471            let min_ref = min_expr.unwrap_or_else(|| quote! { ::core::option::Option::None });
472            let max_ref = max_expr.unwrap_or_else(|| quote! { ::core::option::Option::None });
473            let call = quote! {
474                #crate_path::validate::validate_range(
475                    #field_key,
476                    #value,
477                    #min_ref,
478                    #max_ref,
479                )
480            };
481            wrap_validate_call(call, crate_path, field_key, message)
482        }
483        ValidateRule::Length { min, max, message } => {
484            let min_expr = min
485                .as_ref()
486                .map(|v| quote! { ::core::option::Option::Some(#v) });
487            let max_expr = max
488                .as_ref()
489                .map(|v| quote! { ::core::option::Option::Some(#v) });
490            let min_ref = min_expr.unwrap_or_else(|| quote! { ::core::option::Option::None });
491            let max_ref = max_expr.unwrap_or_else(|| quote! { ::core::option::Option::None });
492            let call = quote! {
493                #crate_path::validate::validate_length(
494                    #field_key,
495                    #value,
496                    #min_ref,
497                    #max_ref,
498                )
499            };
500            wrap_validate_call(call, crate_path, field_key, message)
501        }
502        ValidateRule::NotEmpty { message } => {
503            let call = quote! {
504                #crate_path::validate::validate_not_empty(
505                    #field_key,
506                    #value,
507                )
508            };
509            wrap_validate_call(call, crate_path, field_key, message)
510        }
511        #[cfg(feature = "regex")]
512        ValidateRule::Regex { pattern, message } => {
513            use quote::ToTokens;
514            let key = pattern.to_token_stream().to_string();
515            let regex_ident = _regex_map.get(&key).expect("missing regex cache entry");
516            let call = quote! {
517                {
518                    let regex_result = #regex_ident.get_or_init(|| {
519                        ::regex::Regex::new(#pattern)
520                            .map_err(|err| format!("invalid regex: {}", err))
521                    });
522
523                    let regex = match regex_result {
524                        ::core::result::Result::Ok(re) => re,
525                        ::core::result::Result::Err(message) => {
526                            return ::core::result::Result::Err(
527                                #crate_path::ConfigError::ConfigParseError(
528                                    (#field_key)(),
529                                    message.clone(),
530                                ),
531                            );
532                        }
533                    };
534
535                    #crate_path::validate::validate_regex(
536                        #field_key,
537                        regex,
538                        #value.as_ref(),
539                    )
540                }
541            };
542            wrap_validate_call(call, crate_path, field_key, message)
543        }
544        ValidateRule::Custom { path, message } => {
545            let call = quote! { #crate_path::validate::validate_custom(#field_key, #value, #path) };
546            wrap_validate_call(call, crate_path, field_key, message)
547        }
548    }
549}
550
551fn wrap_validate_call(
552    call: TokenStream,
553    crate_path: &TokenStream,
554    field_key: &TokenStream,
555    message: &Option<LitStr>,
556) -> TokenStream {
557    if let Some(message) = message {
558        quote! {
559            match #call {
560                ::core::result::Result::Ok(()) => (),
561                ::core::result::Result::Err(_) => {
562                    return ::core::result::Result::Err(
563                        #crate_path::ConfigError::ConfigParseError(
564                            (#field_key)(),
565                            #message.to_string(),
566                        ),
567                    );
568                }
569            }
570        }
571    } else {
572        quote! {
573            #call?;
574        }
575    }
576}
577
578fn option_inner(ty: &Type) -> Option<&Type> {
579    let Type::Path(type_path) = ty else {
580        return None;
581    };
582    let segment = type_path.path.segments.last()?;
583    if segment.ident != "Option" {
584        return None;
585    }
586    let PathArguments::AngleBracketed(args) = &segment.arguments else {
587        return None;
588    };
589    for arg in &args.args {
590        if let GenericArgument::Type(inner) = arg {
591            return Some(inner);
592        }
593    }
594    None
595}
596
597fn parse_lit(lit: Lit) -> String {
598    match lit {
599        Lit::Str(s) => s.value(),
600        Lit::ByteStr(s) => match String::from_utf8(s.value()) {
601            Ok(v) => v,
602            Err(e) => panic!("Invalid UTF-8 sequence: {}", e),
603        },
604        Lit::Byte(b) => (b.value() as char).to_string(),
605        Lit::Int(i) => i.base10_digits().to_owned(),
606        Lit::Float(f) => f.base10_digits().to_owned(),
607        Lit::Bool(b) => b.value.to_string(),
608        Lit::Char(c) => c.value().to_string(),
609        Lit::Verbatim(_) => panic!("cfg-rs not support Verbatim"),
610        _ => panic!("cfg-rs not support new types"),
611    }
612}