Skip to main content

easy_config_macros/
lib.rs

1use proc_macro::TokenStream;
2use quote::quote;
3use syn::{
4    Data, DeriveInput, Expr, Fields, GenericArgument, Lit, Meta, PathArguments, Type,
5    parse_macro_input, punctuated::Punctuated, token,
6};
7
8#[proc_macro_derive(EasyConfig, attributes(attr, merge))]
9pub fn easy_config_derive(input: TokenStream) -> TokenStream {
10    let input = parse_macro_input!(input as DeriveInput);
11    let struct_name = &input.ident;
12
13    let fields = match input.data {
14        Data::Struct(data) => match data.fields {
15            Fields::Named(fields) => fields.named,
16            _ => panic!("Only structs with named fields are supported"),
17        },
18        _ => panic!("Only structs are supported"),
19    };
20
21    let mut config_key_inits = Vec::new();
22    let mut from_props_fields = Vec::new();
23    let mut getter_methods = Vec::new();
24
25    for f in fields.iter() {
26        let field_name = f.ident.as_ref().unwrap();
27        let field_ty = &f.ty;
28
29        if f.attrs.iter().any(|attr| attr.path().is_ident("merge")) {
30            config_key_inits.push(quote! {
31                <#field_ty as FromConfigDef>::config_def()?.config_keys().values().cloned().collect::<Vec<_>>()
32            });
33            from_props_fields.push(quote! {
34                #field_name: <#field_ty as FromConfigDef>::from_props(props)?
35            });
36        } else {
37            let mut attrs = ParsedAttributes::default();
38            for attr in &f.attrs {
39                if attr.path().is_ident("attr") {
40                    let parsed = attr
41                        .parse_args_with(Punctuated::<Meta, token::Comma>::parse_terminated)
42                        .expect("Failed to parse config attributes");
43                    attrs.populate_from(parsed);
44                }
45            }
46
47            if attrs.getter {
48                getter_methods.push(quote! {
49                    pub fn #field_name(&self) -> &#field_ty { &self.#field_name }
50                });
51            }
52
53            let field_name_str = field_name.to_string();
54            let lookup_key = attrs.name.map_or(quote! { #field_name_str }, |e| {
55                if let Expr::Lit(expr_lit) = &e
56                    && let Lit::Str(_) = &expr_lit.lit
57                {
58                    return quote! { #e };
59                }
60                quote! { &*(#e) }
61            });
62            let docs = attrs
63                .documentation
64                .map(|d| quote! { Some(Into::<String>::into(#d)) })
65                .unwrap_or(quote! { None });
66            let default = attrs
67                .default
68                .map(|d| quote! { Some(#d) })
69                .unwrap_or(quote! { None });
70            let importance = attrs
71                .importance
72                .map(|i| quote! { Some(#i) })
73                .unwrap_or(quote! { None });
74            let validator = attrs
75                .validator
76                .map(|v| quote! { Some(#v) })
77                .unwrap_or(quote! { None });
78            let group = attrs
79                .group
80                .map(|g| quote! { Some(Into::<String>::into(#g)) })
81                .unwrap_or(quote! { None });
82            let internal_config = attrs.internal_config;
83
84            let (is_option, inner_ty) = {
85                let mut is_opt = false;
86                let mut inner = quote! { #field_ty };
87
88                if let Type::Path(type_path) = field_ty
89                    && type_path.path.segments.len() == 1
90                    && type_path.path.segments[0].ident == "Option"
91                    && let PathArguments::AngleBracketed(params) =
92                        &type_path.path.segments[0].arguments
93                    && let Some(GenericArgument::Type(t)) = params.args.first()
94                {
95                    is_opt = true;
96                    inner = quote! { #t };
97                }
98
99                (is_opt, inner)
100            };
101
102            config_key_inits.push(quote! {
103                vec![Box::new(ConfigKey::<#inner_ty>::new(
104                    #lookup_key,
105                    #docs,
106                    #default,
107                    #validator,
108                    #importance,
109                    #group,
110                    #internal_config,
111                )) as Box<dyn ConfigKeyTrait>]
112            });
113
114            // Reverted to separate logic paths for `T` and `Option<T>` to fix the error.
115            let from_props_logic = if is_option {
116                quote! {
117                    #field_name: {
118                        let key_name = #lookup_key;
119                        let meta_opt = def.find_key(key_name);
120                        if let Some(val_str) = props.get(key_name) {
121                            if let Some(meta) = meta_opt {
122                                if let Some(validator) = meta.validator() {
123                                    validator.validate(key_name, val_str)?;
124                                }
125                            }
126                            Some(<#inner_ty as ConfigValue>::parse(key_name, val_str)?)
127                        } else if let Some(meta) = meta_opt {
128                            if let Some(default_val_any) = meta.default_value_any() {
129                                let default_val = default_val_any.downcast_ref::<#inner_ty>().unwrap().clone();
130                                if let Some(validator) = meta.validator() {
131                                    validator.validate(key_name, &default_val.to_config_string())?;
132                                }
133                                Some(default_val)
134                            } else {
135                                None
136                            }
137                        } else {
138                            None
139                        }
140                    }
141                }
142            } else {
143                quote! {
144                    #field_name: {
145                        let key_name = #lookup_key;
146                        let meta = def.find_key(key_name).ok_or_else(|| ConfigError::MissingName(key_name.to_string()))?;
147                        if let Some(val_str) = props.get(key_name) {
148                            if let Some(validator) = meta.validator() {
149                                validator.validate(key_name, val_str)?;
150                            }
151                            <#inner_ty as ConfigValue>::parse(key_name, val_str)?
152                        } else if let Some(default_val_any) = meta.default_value_any() {
153                            let default_val = default_val_any.downcast_ref::<#inner_ty>().unwrap().clone();
154                            if let Some(validator) = meta.validator() {
155                                validator.validate(key_name, &default_val.to_config_string())?;
156                            }
157                            default_val
158                        } else {
159                            return Err(ConfigError::MissingName(key_name.to_string()));
160                        }
161                    }
162                }
163            };
164            from_props_fields.push(from_props_logic);
165        }
166    }
167
168    let expanded = quote! {
169        static CONFIG_DEF: once_cell::sync::OnceCell<ConfigDef> = once_cell::sync::OnceCell::new();
170
171        impl #struct_name {
172            #(#getter_methods)*
173        }
174
175        impl FromConfigDef for #struct_name {
176            fn from_props(props: &std::collections::HashMap<String, String>) -> Result<Self, ConfigError> {
177                let def = Self::config_def()?;
178                Ok(Self { #(#from_props_fields),* })
179            }
180
181            fn config_def() -> Result<&'static ConfigDef, ConfigError> {
182                CONFIG_DEF.get_or_try_init(|| {
183                    let keys: Vec<Box<dyn ConfigKeyTrait>> = vec![
184                        #(#config_key_inits),*
185                    ].into_iter().flatten().collect();
186                    ConfigDef::try_from(keys)
187                })
188            }
189        }
190    };
191    TokenStream::from(expanded)
192}
193
194/// A helper struct to organize parsed attributes within the macro.
195#[derive(Default)]
196struct ParsedAttributes {
197    name: Option<Expr>,
198    documentation: Option<Expr>,
199    default: Option<Expr>,
200    group: Option<Expr>,
201    importance: Option<Expr>,
202    validator: Option<Expr>,
203    getter: bool,
204    internal_config: bool,
205}
206
207impl ParsedAttributes {
208    fn populate_from(&mut self, parsed_attrs: Punctuated<Meta, token::Comma>) {
209        for meta in parsed_attrs {
210            match meta {
211                Meta::Path(path) if path.is_ident("getter") => {
212                    self.getter = true;
213                }
214                Meta::NameValue(nv) => {
215                    let ident = nv.path.get_ident().unwrap().to_string();
216                    match ident.as_str() {
217                        "name" => self.name = Some(nv.value),
218                        "documentation" => self.documentation = Some(nv.value),
219                        "default" => self.default = Some(nv.value),
220                        "group" => self.group = Some(nv.value),
221                        "importance" => self.importance = Some(nv.value),
222                        "validator" => self.validator = Some(nv.value),
223                        "internal_config" => {
224                            if let Expr::Lit(expr_lit) = nv.value
225                                && let Lit::Bool(lit_bool) = expr_lit.lit
226                            {
227                                self.internal_config = lit_bool.value();
228                            }
229                        }
230                        _ => panic!("Unknown attribute: {}", ident),
231                    }
232                }
233                _ => {}
234            }
235        }
236    }
237}