easy_config_macros/
lib.rs1use 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 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#[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}