Skip to main content

configulator_derive/
lib.rs

1//! # Configulator Derive Macro
2//!
3//! Derive macro for
4//! [`configulator-rs`](https://crates.io/crates/configulator-rs).
5//! This crate is not intended to be used directly, add
6//! `configulator-rs` as a dependency instead.
7
8#![warn(clippy::all)]
9#![forbid(unsafe_code)]
10
11use proc_macro::TokenStream;
12use quote::quote;
13use syn::{
14    parse_macro_input, DeriveInput, Data, Fields, Type, PathArguments, GenericArgument,
15    punctuated::Punctuated, Token,
16};
17
18/// Derive macro that generates `ConfigFields` and `FromValueMap` implementations for a struct.
19///
20/// Supports `#[configulator(name = "...", default = "...", description = "...")]` attributes.
21/// Falls back to field name if no `name` attribute is specified.
22///
23/// Scalar field types must implement [`FromStr`](std::str::FromStr) + `Default`. Nested struct
24/// types must also derive `Config`, detection is automatic at compile time.
25#[proc_macro_derive(Config, attributes(configulator))]
26pub fn derive_config(input: TokenStream) -> TokenStream {
27    let input = parse_macro_input!(input as DeriveInput);
28    match derive_config_impl(&input) {
29        Ok(tokens) => tokens.into(),
30        Err(err) => err.to_compile_error().into(),
31    }
32}
33
34fn derive_config_impl(input: &DeriveInput) -> Result<proc_macro2::TokenStream, syn::Error> {
35    let name = &input.ident;
36    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
37
38    let fields = extract_named_fields(input)?;
39
40    let mut field_info_tokens = Vec::new();
41    let mut from_map_tokens = Vec::new();
42
43    for field in fields.iter() {
44        let field_ident = field.ident.as_ref().unwrap();
45        let field_name_str = field_ident.to_string();
46        let field_ty = &field.ty;
47
48        let attrs = parse_configulator_attrs(&field.attrs)?;
49
50        let config_name_str = attrs.config_name.unwrap_or_else(|| field_name_str.clone());
51        let field_type_token = field_type_to_tokens(field_ty);
52
53        let default_tokens = match &attrs.default_val {
54            Some(v) => quote! { Some(#v) },
55            None => quote! { None },
56        };
57        let desc_tokens = match &attrs.description {
58            Some(v) => quote! { Some(#v) },
59            None => quote! { None },
60        };
61
62        field_info_tokens.push(quote! {
63            configulator::FieldInfo {
64                field_name: #field_name_str,
65                config_name: #config_name_str,
66                default_value: #default_tokens,
67                description: #desc_tokens,
68                field_type: #field_type_token,
69            }
70        });
71
72        let from_map_field = gen_from_value_map_field(field_ident, &config_name_str, field_ty);
73        from_map_tokens.push(from_map_field);
74    }
75
76    let expanded = quote! {
77        impl #impl_generics configulator::ConfigFields for #name #ty_generics #where_clause {
78            fn configulator_fields() -> Vec<configulator::FieldInfo> {
79                // Import trait so fallback scalar dispatch can resolve
80                use configulator::ConfiguratorScalar as _;
81                vec![
82                    #(#field_info_tokens),*
83                ]
84            }
85        }
86
87        impl #impl_generics configulator::FromValueMap for #name #ty_generics #where_clause {
88            fn from_value_map(
89                map: &configulator::ValueMap,
90            ) -> Result<Self, configulator::ConfigulatorError> {
91                // Import trait so fallback scalar dispatch can resolve
92                use configulator::ConfiguratorScalar as _;
93                Ok(Self {
94                    #(#from_map_tokens),*
95                })
96            }
97        }
98    };
99
100    Ok(expanded)
101}
102
103/// Extract named fields from a `DeriveInput`, returning an error for non-structs
104/// or structs without named fields.
105fn extract_named_fields(
106    input: &DeriveInput,
107) -> Result<&Punctuated<syn::Field, Token![,]>, syn::Error> {
108    match &input.data {
109        Data::Struct(data) => match &data.fields {
110            Fields::Named(fields) => Ok(&fields.named),
111            _ => Err(syn::Error::new_spanned(
112                &input.ident,
113                "Config can only be derived for structs with named fields",
114            )),
115        },
116        _ => Err(syn::Error::new_spanned(
117            &input.ident,
118            "Config can only be derived for structs",
119        )),
120    }
121}
122
123#[derive(Debug)]
124struct FieldConfigAttrs {
125    config_name: Option<String>,
126    default_val: Option<String>,
127    description: Option<String>,
128}
129
130/// Parse `#[configulator(...)]` attributes from a field's attribute list.
131/// Non-configulator attributes are skipped. Returns an error if attribute
132/// syntax is malformed.
133fn parse_configulator_attrs(attrs: &[syn::Attribute]) -> Result<FieldConfigAttrs, syn::Error> {
134    let mut result = FieldConfigAttrs {
135        config_name: None,
136        default_val: None,
137        description: None,
138    };
139    for attr in attrs {
140        if !attr.path().is_ident("configulator") {
141            continue;
142        }
143        attr.parse_nested_meta(|meta| {
144            if meta.path.is_ident("name") {
145                let value = meta.value()?;
146                let lit: syn::LitStr = value.parse()?;
147                result.config_name = Some(lit.value());
148            } else if meta.path.is_ident("default") {
149                let value = meta.value()?;
150                let lit: syn::LitStr = value.parse()?;
151                result.default_val = Some(lit.value());
152            } else if meta.path.is_ident("description") {
153                let value = meta.value()?;
154                let lit: syn::LitStr = value.parse()?;
155                result.description = Some(lit.value());
156            } else {
157                let name = meta.path.get_ident()
158                    .map(|id| id.to_string())
159                    .unwrap_or_else(|| "?".to_string());
160                return Err(meta.error(format_args!(
161                    "unknown configulator attribute `{name}`; \
162                     expected `name`, `default`, or `description`",
163                )));
164            }
165            Ok(())
166        })?;
167    }
168    Ok(result)
169}
170
171/// Map a Rust type to the simplified `FieldType` enum (Bool, Scalar, List, Struct).
172/// For non-bool, non-Vec types, uses compile-time autoref dispatch to detect
173/// whether the type is a nested struct or a scalar.
174fn field_type_to_tokens(ty: &Type) -> proc_macro2::TokenStream {
175    if let Type::Path(type_path) = ty {
176        if let Some(segment) = type_path.path.segments.last() {
177            if segment.ident == "bool" {
178                return quote! { configulator::FieldType::Bool };
179            }
180            if segment.ident == "Vec" {
181                if let PathArguments::AngleBracketed(_) = &segment.arguments {
182                    return quote! { configulator::FieldType::List };
183                }
184                return quote! {
185                    compile_error!("Vec fields must have a type argument, e.g. Vec<String>")
186                };
187            }
188        }
189    }
190    gen_config_detect_tokens(ty)
191}
192
193/// Generate the `ConfigDetect` autoref dispatch expression for a type.
194/// At compile time this resolves to either `FieldType::Struct` (for nested
195/// config structs) or `FieldType::Scalar` (for `FromStr` types).
196fn gen_config_detect_tokens(ty: &Type) -> proc_macro2::TokenStream {
197    quote! {
198        {
199            let __m = configulator::ConfigDetect::<#ty>(::std::marker::PhantomData);
200            __m.__configulator_field_type()
201        }
202    }
203}
204
205/// Generate the field assignment for `FromValueMap::from_value_map`.
206fn gen_from_value_map_field(
207    field_ident: &syn::Ident,
208    config_name: &str,
209    ty: &Type,
210) -> proc_macro2::TokenStream {
211    let kind = classify_type(ty);
212    match kind {
213        TypeKind::Bool => {
214            quote! {
215                #field_ident: configulator::parse_scalar::<bool>(map, #config_name)?
216            }
217        }
218        TypeKind::Vec(inner_ty) => {
219            quote! {
220                #field_ident: configulator::parse_list::<#inner_ty>(map, #config_name)?
221            }
222        }
223        TypeKind::Other => {
224            quote! {
225                #field_ident: {
226                    let __m = configulator::ConfigDetect::<#ty>(::std::marker::PhantomData);
227                    __m.__configulator_parse(map, #config_name)?
228                }
229            }
230        }
231    }
232}
233
234#[derive(Debug)]
235enum TypeKind {
236    Bool,
237    Vec(Box<Type>),
238    Other,
239}
240
241fn classify_type(ty: &Type) -> TypeKind {
242    if let Type::Path(type_path) = ty {
243        if let Some(segment) = type_path.path.segments.last() {
244            if segment.ident == "bool" {
245                return TypeKind::Bool;
246            }
247            if segment.ident == "Vec" {
248                if let PathArguments::AngleBracketed(args) = &segment.arguments {
249                    if let Some(GenericArgument::Type(inner)) = args.args.first() {
250                        return TypeKind::Vec(Box::new(inner.clone()));
251                    }
252                }
253                // Vec without type argument — emit a clear error
254                return TypeKind::Other;
255            }
256        }
257    }
258    TypeKind::Other
259}
260
261#[cfg(test)]
262mod tests {
263    use super::*;
264    use syn::parse_str;
265
266    // ── derive_config_impl tests ──
267
268    #[test]
269    fn derive_config_impl_valid_struct() {
270        let input: DeriveInput = parse_str("struct Foo { x: u32 }").unwrap();
271        assert!(derive_config_impl(&input).is_ok());
272    }
273
274    #[test]
275    fn derive_config_impl_rejects_enum() {
276        let input: DeriveInput = parse_str("enum Foo { A, B }").unwrap();
277        let err = derive_config_impl(&input).unwrap_err();
278        assert!(err.to_string().contains("only be derived for structs"));
279    }
280
281    #[test]
282    fn derive_config_impl_rejects_tuple_struct() {
283        let input: DeriveInput = parse_str("struct Foo(u32);").unwrap();
284        let err = derive_config_impl(&input).unwrap_err();
285        assert!(err.to_string().contains("named fields"));
286    }
287
288    #[test]
289    fn derive_config_impl_rejects_bad_attr() {
290        let input: DeriveInput = parse_str(
291            r#"struct Foo { #[configulator(name = 42)] f: String }"#,
292        )
293        .unwrap();
294        assert!(derive_config_impl(&input).is_err());
295    }
296
297    // ── extract_named_fields tests ──
298
299    #[test]
300    fn extract_named_fields_accepts_named_struct() {
301        let input: DeriveInput = parse_str("struct Foo { x: u32 }").unwrap();
302        assert!(extract_named_fields(&input).is_ok());
303    }
304
305    #[test]
306    fn extract_named_fields_rejects_tuple_struct() {
307        let input: DeriveInput = parse_str("struct Foo(u32);").unwrap();
308        let err = extract_named_fields(&input).unwrap_err();
309        assert!(
310            err.to_string().contains("named fields"),
311            "expected 'named fields' error, got: {err}"
312        );
313    }
314
315    #[test]
316    fn extract_named_fields_rejects_unit_struct() {
317        let input: DeriveInput = parse_str("struct Foo;").unwrap();
318        let err = extract_named_fields(&input).unwrap_err();
319        assert!(
320            err.to_string().contains("named fields"),
321            "expected 'named fields' error, got: {err}"
322        );
323    }
324
325    #[test]
326    fn extract_named_fields_rejects_enum() {
327        let input: DeriveInput = parse_str("enum Foo { A, B }").unwrap();
328        let err = extract_named_fields(&input).unwrap_err();
329        assert!(
330            err.to_string().contains("only be derived for structs"),
331            "expected 'only be derived for structs' error, got: {err}"
332        );
333    }
334
335    // ── parse_configulator_attrs tests ──
336
337    #[test]
338    fn parse_attrs_extracts_all_keys() {
339        let input: DeriveInput = parse_str(
340            r#"struct Foo { #[configulator(name = "n", default = "d", description = "desc")] f: u32 }"#,
341        )
342        .unwrap();
343        let fields = extract_named_fields(&input).unwrap();
344        let attrs = parse_configulator_attrs(&fields.first().unwrap().attrs).unwrap();
345        assert_eq!(attrs.config_name.as_deref(), Some("n"));
346        assert_eq!(attrs.default_val.as_deref(), Some("d"));
347        assert_eq!(attrs.description.as_deref(), Some("desc"));
348    }
349
350    #[test]
351    fn parse_attrs_skips_non_configulator() {
352        let input: DeriveInput = parse_str(
353            r#"struct Foo { #[allow(unused)] #[configulator(name = "bar")] f: String }"#,
354        )
355        .unwrap();
356        let fields = extract_named_fields(&input).unwrap();
357        let attrs = parse_configulator_attrs(&fields.first().unwrap().attrs).unwrap();
358        assert_eq!(attrs.config_name.as_deref(), Some("bar"));
359    }
360
361    #[test]
362    fn parse_attrs_rejects_unknown_key() {
363        let input: DeriveInput = parse_str(
364            r#"struct Foo { #[configulator(name = "bar", extra)] f: String }"#,
365        )
366        .unwrap();
367        let fields = extract_named_fields(&input).unwrap();
368        let err = parse_configulator_attrs(&fields.first().unwrap().attrs).unwrap_err();
369        assert!(
370            err.to_string().contains("unknown configulator attribute"),
371            "expected 'unknown configulator attribute' error, got: {err}"
372        );
373    }
374
375    #[test]
376    fn parse_attrs_error_on_bad_value_type() {
377        let input: DeriveInput = parse_str(
378            r#"struct Foo { #[configulator(name = 42)] f: String }"#,
379        )
380        .unwrap();
381        let fields = extract_named_fields(&input).unwrap();
382        assert!(parse_configulator_attrs(&fields.first().unwrap().attrs).is_err());
383    }
384
385    #[test]
386    fn parse_attrs_no_attrs_returns_none() {
387        let input: DeriveInput = parse_str("struct Foo { f: String }").unwrap();
388        let fields = extract_named_fields(&input).unwrap();
389        let attrs = parse_configulator_attrs(&fields.first().unwrap().attrs).unwrap();
390        assert!(attrs.config_name.is_none());
391        assert!(attrs.default_val.is_none());
392        assert!(attrs.description.is_none());
393    }
394
395    // ── classify_type tests ──
396
397    #[test]
398    fn classify_type_bool() {
399        let ty: Type = parse_str("bool").unwrap();
400        assert!(matches!(classify_type(&ty), TypeKind::Bool));
401    }
402
403    #[test]
404    fn classify_type_vec_with_inner() {
405        let ty: Type = parse_str("Vec<String>").unwrap();
406        assert!(matches!(classify_type(&ty), TypeKind::Vec(_)));
407    }
408
409    #[test]
410    fn classify_type_scalar_string() {
411        let ty: Type = parse_str("String").unwrap();
412        assert!(matches!(classify_type(&ty), TypeKind::Other));
413    }
414
415    #[test]
416    fn classify_type_reference_is_other() {
417        let ty: Type = parse_str("&str").unwrap();
418        assert!(matches!(classify_type(&ty), TypeKind::Other));
419    }
420
421    #[test]
422    fn classify_type_tuple_is_other() {
423        let ty: Type = parse_str("(i32, i32)").unwrap();
424        assert!(matches!(classify_type(&ty), TypeKind::Other));
425    }
426
427    #[test]
428    fn classify_type_bare_vec_without_type_args() {
429        let ty: Type = parse_str("Vec").unwrap();
430        assert!(matches!(classify_type(&ty), TypeKind::Other));
431    }
432
433    // ── field_type_to_tokens tests ──
434
435    #[test]
436    fn field_type_to_tokens_bool() {
437        let ty: Type = parse_str("bool").unwrap();
438        let tokens = field_type_to_tokens(&ty).to_string();
439        assert!(tokens.contains("FieldType"), "expected FieldType in: {tokens}");
440        assert!(tokens.contains("Bool"), "expected Bool in: {tokens}");
441    }
442
443    #[test]
444    fn field_type_to_tokens_vec() {
445        let ty: Type = parse_str("Vec<u32>").unwrap();
446        let tokens = field_type_to_tokens(&ty).to_string();
447        assert!(tokens.contains("FieldType"), "expected FieldType in: {tokens}");
448        assert!(tokens.contains("List"), "expected List in: {tokens}");
449    }
450
451    #[test]
452    fn field_type_to_tokens_scalar() {
453        let ty: Type = parse_str("String").unwrap();
454        let tokens = field_type_to_tokens(&ty).to_string();
455        assert!(
456            tokens.contains("ConfigDetect"),
457            "expected ConfigDetect dispatch in: {tokens}"
458        );
459    }
460
461    #[test]
462    fn field_type_to_tokens_non_path_fallback() {
463        let ty: Type = parse_str("&str").unwrap();
464        let tokens = field_type_to_tokens(&ty).to_string();
465        assert!(
466            tokens.contains("ConfigDetect"),
467            "expected ConfigDetect dispatch in fallback: {tokens}"
468        );
469    }
470}