Skip to main content

figen_proc_macros/
lib.rs

1mod registry;
2
3use darling::{FromAttributes, FromMeta};
4use proc_macro::TokenStream;
5use quote::quote;
6use syn::parse_quote;
7use syn::spanned::Spanned;
8use syn::{parse_macro_input, Data, Ident, LitStr, Type};
9
10#[derive(Debug, FromAttributes)]
11#[darling(attributes(property))]
12struct PropertyArgs {
13    key: Option<LitStr>,
14    #[darling(default)]
15    optional: bool,
16    #[darling(default)]
17    flatten: bool,
18    indices: Option<Vec<LitStr>>,
19    array_ref: Option<ArrayRefArgs>,
20}
21
22#[derive(Debug, FromMeta)]
23struct ArrayRefArgs {
24    key: LitStr,
25    prefix: Option<LitStr>,
26}
27
28struct FieldDefinition {
29    pub ident: Ident,
30    pub ty: Type,
31    pub attrs: PropertyArgs,
32}
33
34#[proc_macro]
35pub fn expand_config_registry(input: TokenStream) -> TokenStream {
36    let input = parse_macro_input!(input as registry::RegistryDefinition);
37    registry::expand(input)
38        .into()
39}
40
41
42#[proc_macro_derive(Configuration, attributes(property))]
43pub fn derive_configuration(input: TokenStream) -> TokenStream {
44    let input = parse_macro_input!(input as syn::DeriveInput);
45    let ident = &input.ident;
46
47    let data = match input.data {
48        Data::Struct(d) => d,
49        _ => {
50            return syn::Error::new(input.span(), "Configuration can only be derived for structs")
51                .to_compile_error()
52                .into();
53        }
54    };
55
56    let field_defs: Vec<FieldDefinition> = data.fields.iter().map(|field| {
57        let attrs = &field.attrs;
58        let has_property_attr = attrs.iter().any(|attr| {
59            attr.path().is_ident("property")
60        });
61        assert!(has_property_attr, "Field {} does not have a #[property] attribute", field.ident.as_ref().unwrap());
62
63        let attrs: PropertyArgs = PropertyArgs::from_attributes(&attrs).unwrap();
64
65        FieldDefinition {
66            ident: field.ident.clone().expect("Fields must have a name"),
67            ty: field.ty.clone(),
68            attrs,
69        }
70    }).collect();
71
72    let bindings = field_defs.iter().map(|field_def| {
73        let field = field_def.ident.clone();
74        let key = field_def.attrs.key.clone().unwrap_or_else(|| LitStr::new(&field.to_string().as_str(), field.span()));
75
76        let handle_err = if field_def.attrs.optional {
77            quote!(
78                Ok(r) => {
79                    result = result.or(Ok(r));
80                },
81                Err(e) => {
82                    if e != figen::error::Error::NotFound {
83                        return Err(e);
84                    }
85                }
86            )
87        } else {
88            quote!(
89                Ok(r) => {
90                    result = result.or(Ok(r));
91                },
92                Err(figen::error::Error::NotFound) => {
93                    // If the field is required, we return a required error
94                    return Err(figen::error::Error::Required);
95                }
96                Err(e) => {
97                    return Err(e);
98                }
99            )
100        };
101
102
103        let bind_call = if let Type::Array(_) = &field_def.ty {
104            let indices = field_def.attrs.indices.as_ref().map(|indices| {
105                // Expand the indices to an array
106                quote!(
107                    {
108                        let _indices = &[#(#indices),*];
109                        assert!(_indices.len() == self.#field.len(), "Array indices length does not match array size");
110                        figen::binder::ArrayConfigIndicesMode::Custom(_indices)
111                    }
112                )
113            }).or_else(|| {
114                Some(quote!(figen::binder::ArrayConfigIndicesMode::ZeroIndexed))
115            });
116
117            quote!(
118                {
119
120                    let mut _binder = figen::binder::ArrayConfigBinder::new(#indices, &mut self.#field);
121                    match _binder.bind(path, loader) {
122                        Ok(r) => {
123                            result = result.or(Ok(r));
124                        },
125                        Err(e) => {
126                            if e != figen::error::Error::NotFound {
127                                return Err(e);
128                            }
129                        }
130                    };
131                }
132            )
133        } else {
134            if let Some(array_ref) = &field_def.attrs.array_ref {
135                let array_ref_key = &array_ref.key;
136                let array_ref_prefix = &array_ref.prefix;
137
138                let binder = if array_ref_prefix.is_some() {
139                    quote!(
140                        figen::binder::ArrayRefBinder::new(
141                            #array_ref_key,
142                            Some(#array_ref_prefix),
143                            &mut self.#field
144                        );
145                    )
146                } else {
147                    quote!(
148                        figen::binder::ArrayRefBinder::new(
149                            #array_ref_key,
150                            None,
151                            &mut self.#field
152                        );
153                    )
154                };
155
156                quote!(
157                    {
158                        let mut _binder = #binder
159                        match _binder.bind(path, loader) {
160                            Ok(r) => {
161                                result = result.or(Ok(r));
162                            },
163                            Err(e) => {
164                                if e != figen::error::Error::NotFound {
165                                    return Err(e);
166                                }
167                            }
168                        };
169                    }
170                )
171            } else {
172                quote!(
173                    match self.#field.bind(path, loader) {
174                        #handle_err
175                    };
176                )
177            }
178        };
179
180        // If the field is flattened, we bind it directly, otherwise we push the key to the path
181        if field_def.attrs.flatten {
182            bind_call
183        } else {
184            quote!(
185                path.push(#key);
186                #bind_call
187                path.pop();
188            )
189        }
190    });
191
192    let initializers = field_defs.iter().map(|field_def| {
193        let field = &field_def.ident;
194        let ty = &field_def.ty;
195        let key = field_def
196            .attrs
197            .key
198            .clone()
199            .unwrap_or_else(|| LitStr::new(field.to_string().as_str(), field.span()));
200
201        let initialize_call = if let Type::Array(_) = ty {
202            let indices = if let Some(indices) = &field_def.attrs.indices {
203                quote!(figen::binder::ArrayConfigIndicesMode::Custom(&[#(#indices),*]))
204            } else {
205                quote!(figen::binder::ArrayConfigIndicesMode::ZeroIndexed)
206            };
207
208            quote!(
209                figen::binder::ArrayConfigInitializer::initialize(
210                    #indices,
211                    path,
212                    loader,
213                )
214            )
215        } else if let Some(array_ref) = &field_def.attrs.array_ref {
216            let array_ref_key = &array_ref.key;
217            let prefix = if let Some(prefix) = &array_ref.prefix {
218                quote!(Some(#prefix))
219            } else {
220                quote!(None)
221            };
222
223            quote!(
224                figen::binder::ArrayRefInitializer::initialize(
225                    #array_ref_key,
226                    #prefix,
227                    path,
228                    loader,
229                )
230            )
231        } else {
232            quote!(
233                <#ty as figen::binder::ConfigInitializer<T, U>>::initialize(path, loader)
234            )
235        };
236
237        if field_def.attrs.flatten {
238            quote!(let #field: #ty = #initialize_call?;)
239        } else {
240            quote!(
241                path.push(#key);
242                let _result = #initialize_call;
243                path.pop();
244                let #field: #ty = _result?;
245            )
246        }
247    });
248
249    let initialized_fields = field_defs.iter().map(|field_def| &field_def.ident);
250
251    let result_expand = if field_defs.iter().all(|f| f.attrs.optional) {
252        quote!(let mut result = Err(figen::error::Error::NotFound);)
253    } else {
254        quote!(let mut result = Err(figen::error::Error::Required);)
255    };
256    quote!(
257        impl<T, U> figen::binder::ConfigBinder<T, U> for #ident where
258            T: figen::BindPath,
259            U: figen::loader::PropertyLoader,
260        {
261            #[cold]
262            #[inline(never)]
263            fn bind(&mut self, path: &mut T, loader: &U) -> figen::error::Result<()> {
264                #result_expand
265                #(#bindings);*
266                result
267            }
268        }
269
270        impl<T, U> figen::binder::ConfigInitializer<T, U> for #ident where
271            T: figen::BindPath,
272            U: figen::loader::PropertyLoader,
273        {
274            #[cold]
275            #[inline(never)]
276            fn initialize(path: &mut T, loader: &U) -> figen::error::Result<Self> {
277                #(#initializers)*
278                Ok(Self {
279                    #(#initialized_fields),*
280                })
281            }
282        }
283    ).into()
284}
285
286#[proc_macro_derive(ConfigBinder)]
287pub fn derive_config_binder(input: TokenStream) -> TokenStream {
288    let input = parse_macro_input!(input as syn::DeriveInput);
289    let ident = &input.ident;
290
291    let mut generics = input.generics.clone();
292    generics.params.push(parse_quote!(T));
293    generics.params.push(parse_quote!(U));
294    let where_clause = generics.make_where_clause();
295    where_clause.predicates.push(parse_quote!(T: figen::BindPath));
296    where_clause
297        .predicates
298        .push(parse_quote!(U: figen::loader::PropertyLoader));
299
300    let (impl_generics, _, where_clause) = generics.split_for_impl();
301    let (_, ty_generics, _) = input.generics.split_for_impl();
302
303    quote!(
304        impl #impl_generics figen::binder::ConfigInitializer<T, U> for #ident #ty_generics
305        #where_clause
306        {
307            #[cold]
308            #[inline(never)]
309            fn initialize(path: &mut T, loader: &U) -> figen::Result<Self> {
310                let key = path.current_path();
311                let value: figen::str_ty!() = loader.load_str_value(key)?;
312                value
313                    .as_str()
314                    .try_into()
315                    .map_err(|_| figen::error::Error::ParseError)
316            }
317        }
318
319        impl #impl_generics figen::binder::ConfigBinder<T, U> for #ident #ty_generics
320        #where_clause
321        {
322            #[cold]
323            #[inline(never)]
324            fn bind(&mut self, path: &mut T, loader: &U) -> figen::Result<()> {
325                *self = <Self as figen::binder::ConfigInitializer<T, U>>::initialize(path, loader)?;
326                Ok(())
327            }
328        }
329    )
330    .into()
331}