Skip to main content

rcman_derive/
lib.rs

1//! Derive macros for `rcman` settings library.
2//!
3//! This crate provides `#[derive(SettingsSchema)]` for automatically generating
4//! settings schema implementations from Rust structs. It translates strongly-typed
5//! native Rust definitions directly into runtime `rcman::SettingMetadata`, preventing bugs
6//! and ensuring absolute schema correctness via compile-time semantic validation.
7//!
8//! # Features
9//!
10//! - **Native Type Binding**: Automatically translates `String`, `PathBuf`, integers, floats, `bool`, and `Vec<T>` into their corresponding `rcman::SettingType`.
11//! - **Strict Verification**: The macro prevents contradictory constraints at compile time (e.g. `min > max` or `options` on `bool`).
12//! - **Dynamic UI Metadata**: Every unknown attribute literal (e.g., `label = "Server"`) is automatically injected into the schema as customizable metadata.
13//! - **`#[cfg]` Forwarding**: Safely obeys macro feature flags attached to struct fields.
14//!
15//! # Usage
16//!
17//! ```rust
18//! use rcman::DeriveSettingsSchema as SettingsSchema;
19//! use serde::{Serialize, Deserialize};
20//!
21//! #[derive(SettingsSchema, Default, Serialize, Deserialize)]
22//! #[schema(category = "network")] // Required: sets the root prefix for the UI
23//! struct NetworkSettings {
24//!     #[setting(rename = "server-auth-port")]
25//!     pub port: u16,
26//!
27//!     #[setting(rename = "enable_tls")]
28//!     pub tls: bool,
29//!
30//!     #[setting(rename = "server-url")]
31//!     pub url: String,
32//!     
33//!     pub roles: Vec<String>,
34//! }
35//!
36//! fn main() {}
37//! ```
38//!
39//! ---
40//!
41//! # Attribute Reference
42//!
43//! ## Container Attributes (`#[schema(...)]`)
44//! Apply these directly to the `struct`.
45//!
46//! | Attribute | Description | Required | Example |
47//! |-----------|-------------|----------|---------|
48//! | `category` | The root grouping prefix used for all fields. | **Yes** | `#[schema(category = "general")]` |
49//!
50//! ## Field Attributes (`#[setting(...)]`)
51//! Apply these to individual struct fields.
52//!
53//! | Attribute | Type Mapping | Description | Example |
54//! |-----------|--------------|-------------|---------|
55//! | `rename` | *All* | Overrides the field name when constructing the schema key (`category.rename`) | `#[setting(rename = "App-Theme")]` |
56//! | `skip` | *All* | Silently ignores the field; it will not appear in the settings schema | `#[setting(skip)]` |
57//! | `secret` | *All* | Asserts the field contains sensitive data, diverting it to the OS Keychain backing | `#[setting(secret)]` |
58//! | `category` | *All* | Overrides the container `category` specifically for this single field | `#[setting(category = "overridden")]` |
59//! | `nested` | Structs | Extracts the schema from an inner struct and flattens it upward | `#[setting(nested)]` |
60//! | `min` | Number | Sets a numeric minimum constraint (must be `<= max`) | `#[setting(min = 1.0)]` |
61//! | `max` | Number | Sets a numeric maximum constraint (must be `>= min`) | `#[setting(max = 100.0)]` |
62//! | `step` | Number | Defines valid increment stepping | `#[setting(step = 5.0)]` |
63//! | `pattern` | Text | Enforces standard Regex validation string | `#[setting(pattern = "^[a-z]+$")]` |
64//! | `options` | Text/Num | Enforces strict dropdown alternatives mappings | `#[setting(options(("val", "Label")))]` |
65//!
66//! ## Dynamic Metadata
67//! Any `key = value` assignment in `#[setting(...)]` that isn't functionally reserved above is transparently forwarded into the resulting `SettingMetadata` map for your UI components to access dynamically.
68//!
69//! ```rust
70//! use rcman::DeriveSettingsSchema as SettingsSchema;
71//! use serde::{Serialize, Deserialize};
72//!
73//! #[derive(SettingsSchema, Default, Serialize, Deserialize)]
74//! #[schema(category = "network")]
75//! struct ServerSettings {
76//!     #[setting(
77//!         min = 1024,                  // 1. Reserved constraint
78//!         label = "Server Port",       // 2. -> .meta_str("label", "Server Port")
79//!         order = 1,                   // 3. -> .meta_num("order", 1)
80//!         advanced = false             // 4. -> .meta_bool("advanced", false)
81//!     )]
82//!     pub port: u16,
83//! }
84//!
85//! fn main() {}
86//! ```
87//!
88//! # Panics
89//!
90//! This macro performs completely safe compile-time error reporting (yielding `syn::Error`) returning targeted IDE-friendly error underlines instead of panicking. It blocks:
91//! - Setting `min`/`max`/`step` on non-numeric types (`bool`, `Vec`, `String`).
92//! - Setting `pattern` on non-Text types (`bool`, `Vec`, `i32`).
93//! - Unknown/Unsupported types missing `#[setting(skip)]` (e.g. `Duration` or `HashMap`) so that you never accidentally leak invalid config metadata to the UI.
94
95use proc_macro::TokenStream;
96use quote::quote;
97use syn::{Attribute, Data, DeriveInput, Expr, Field, Fields, Lit, Meta, Type, parse_macro_input};
98
99/// Derive macro for generating `SettingsSchema` implementations. See the crate-level documentation for full attribute reference.
100#[proc_macro_derive(SettingsSchema, attributes(schema, setting))]
101pub fn derive_settings_schema(input: TokenStream) -> TokenStream {
102    let input = parse_macro_input!(input as DeriveInput);
103
104    match derive_settings_schema_impl(&input) {
105        Ok(expanded) => TokenStream::from(expanded),
106        Err(err) => TokenStream::from(err.to_compile_error()),
107    }
108}
109
110fn derive_settings_schema_impl(
111    input: &DeriveInput,
112) -> Result<proc_macro2::TokenStream, syn::Error> {
113    let name = &input.ident;
114    let container_attrs = parse_container_attrs(&input.attrs)?;
115
116    let fields = match &input.data {
117        Data::Struct(data) => match &data.fields {
118            Fields::Named(fields) => {
119                if fields.named.is_empty() {
120                    return Err(syn::Error::new_spanned(
121                        input,
122                        "SettingsSchema can only be derived for structs with named fields",
123                    ));
124                }
125                &fields.named
126            }
127            _ => {
128                return Err(syn::Error::new_spanned(
129                    input,
130                    "SettingsSchema can only be derived for structs with named fields",
131                ));
132            }
133        },
134        _ => {
135            return Err(syn::Error::new_spanned(
136                input,
137                "SettingsSchema can only be derived for structs, not enums or unions",
138            ));
139        }
140    };
141
142    let mut metadata_entries = Vec::new();
143    let mut errors = None::<syn::Error>;
144
145    for field in fields {
146        match process_single_field(field, &container_attrs) {
147            Ok(Some(entry)) => metadata_entries.push(entry),
148            Ok(None) => {} // Skipped
149            Err(e) => {
150                if let Some(ref mut combined) = errors {
151                    combined.combine(e);
152                } else {
153                    errors = Some(e);
154                }
155            }
156        }
157    }
158
159    if let Some(err) = errors {
160        return Err(err);
161    }
162
163    Ok(quote! {
164        impl rcman::SettingsSchema for #name {
165            fn get_metadata() -> std::collections::HashMap<String, rcman::SettingMetadata> {
166                let defaults = <#name as Default>::default();
167                let mut map = std::collections::HashMap::new();
168                #(#metadata_entries)*
169                map
170            }
171        }
172    })
173}
174
175fn process_single_field(
176    field: &Field,
177    container_attrs: &ContainerAttrs,
178) -> Result<Option<proc_macro2::TokenStream>, syn::Error> {
179    let attrs = parse_field_attrs(&field.attrs)?;
180    if attrs.skip {
181        return Ok(None);
182    }
183
184    let mut cfg_attrs = Vec::new();
185    for attr in &field.attrs {
186        if attr.path().is_ident("cfg") {
187            cfg_attrs.push(attr);
188        }
189    }
190
191    let entry = process_field(field, &attrs, container_attrs)?;
192
193    if cfg_attrs.is_empty() {
194        Ok(Some(entry))
195    } else {
196        Ok(Some(quote! {
197            #(#cfg_attrs)*
198            {
199                #entry
200            }
201        }))
202    }
203}
204
205fn process_field(
206    field: &Field,
207    attrs: &FieldAttrs,
208    container_attrs: &ContainerAttrs,
209) -> Result<proc_macro2::TokenStream, syn::Error> {
210    let Some(field_name) = &field.ident else {
211        return Err(syn::Error::new_spanned(
212            field,
213            "Field must have a name (internal error: expected named field)",
214        ));
215    };
216    let field_type = &field.ty;
217
218    // Check if this is a nested struct (not a primitive type)
219    if attrs.nested || is_nested_struct(field_type) {
220        return Ok(generate_nested_field_constructor(field_name, field_type));
221    }
222
223    let inner_ty = extract_inner_type_from_option(field_type).unwrap_or(field_type);
224    let type_info = classify_type(inner_ty);
225
226    // If it's classified as Unknown and we didn't catch it as a nested struct, it is unsupported
227    if let TypeInfo::Unknown = type_info {
228        return Err(syn::Error::new_spanned(
229            field_type,
230            "Unsupported type for SettingsSchema. Use `#[setting(skip)]` to ignore it, or `#[setting(nested)]` if it is a custom schema struct.",
231        ));
232    }
233
234    validate_field_type_constraints(field, type_info, attrs)?;
235
236    let category_str = resolve_field_category(field, attrs, container_attrs)?;
237    let final_field_name = attrs
238        .rename
239        .clone()
240        .unwrap_or_else(|| field_name.to_string());
241
242    let key = if category_str.is_empty() {
243        final_field_name.clone()
244    } else {
245        format!("{category_str}.{final_field_name}")
246    };
247
248    let constructor = generate_field_constructor(field_name, field_type, type_info, attrs);
249    let modifiers = generate_field_modifiers(attrs);
250
251    Ok(quote! {
252        map.insert(
253            #key.to_string(),
254            { #constructor } #(#modifiers)*
255        );
256    })
257}
258
259fn generate_nested_field_constructor(
260    field_name: &syn::Ident,
261    field_type: &syn::Type,
262) -> proc_macro2::TokenStream {
263    let prefix = field_name.to_string();
264    quote! {
265        // Merge nested struct's metadata with prefix
266        // Keys from nested struct are "category.field_name", we extract just "field_name"
267        for (key, meta) in <#field_type as rcman::SettingsSchema>::get_metadata() {
268            // Extract just the field name (part after last dot)
269            let field_only = key.rsplit('.').next().unwrap_or(&key);
270            let prefixed_key = format!("{}.{}", #prefix, field_only);
271            // Note: Category is structural (in key), not stored in metadata
272            map.insert(prefixed_key, meta);
273        }
274    }
275}
276
277fn validate_field_type_constraints(
278    field: &Field,
279    type_info: TypeInfo,
280    attrs: &FieldAttrs,
281) -> Result<(), syn::Error> {
282    // Semantic Compile-Time Validation
283    if let (Some(min), Some(max)) = (attrs.min, attrs.max)
284        && min > max
285    {
286        return Err(syn::Error::new_spanned(
287            field,
288            format!("`min` ({min}) cannot be greater than `max` ({max})"),
289        ));
290    }
291
292    if let Some(step) = attrs.step
293        && step <= 0.0
294    {
295        return Err(syn::Error::new_spanned(
296            field,
297            format!("`step` must be positive, got {step}"),
298        ));
299    }
300
301    match type_info {
302        TypeInfo::Number => {
303            if attrs.pattern.is_some() {
304                return Err(syn::Error::new_spanned(
305                    field,
306                    "`pattern` is only valid for text settings, not numbers",
307                ));
308            }
309        }
310        TypeInfo::Text | TypeInfo::Path => {
311            if attrs.min.is_some() || attrs.max.is_some() || attrs.step.is_some() {
312                return Err(syn::Error::new_spanned(
313                    field,
314                    "`min/max/step` are only valid for numeric settings, not text",
315                ));
316            }
317        }
318        TypeInfo::Toggle => {
319            if attrs.min.is_some() || attrs.max.is_some() || attrs.step.is_some() {
320                return Err(syn::Error::new_spanned(
321                    field,
322                    "`min/max/step` are only valid for numeric settings, not booleans",
323                ));
324            }
325            if attrs.pattern.is_some() {
326                return Err(syn::Error::new_spanned(
327                    field,
328                    "`pattern` is only valid for text settings, not booleans",
329                ));
330            }
331            if !attrs.options.is_empty() {
332                return Err(syn::Error::new_spanned(
333                    field,
334                    "`options` are only valid for text/number settings, not booleans",
335                ));
336            }
337        }
338        TypeInfo::List => {
339            if attrs.min.is_some() || attrs.max.is_some() || attrs.step.is_some() {
340                return Err(syn::Error::new_spanned(
341                    field,
342                    "`min/max/step` are only valid for numeric settings, not lists",
343                ));
344            }
345            if attrs.pattern.is_some() {
346                return Err(syn::Error::new_spanned(
347                    field,
348                    "`pattern` is only valid for text settings, not lists",
349                ));
350            }
351            if !attrs.options.is_empty() {
352                return Err(syn::Error::new_spanned(
353                    field,
354                    "`options` are only valid for text/number settings, not lists",
355                ));
356            }
357        }
358        TypeInfo::Unknown => unreachable!(),
359    }
360    Ok(())
361}
362
363fn resolve_field_category(
364    field: &Field,
365    attrs: &FieldAttrs,
366    container_attrs: &ContainerAttrs,
367) -> Result<String, syn::Error> {
368    attrs
369        .category
370        .as_ref()
371        .or(container_attrs.category.as_ref())
372        .cloned()
373        .ok_or_else(|| {
374            syn::Error::new_spanned(
375                field,
376                "Category is required. Add #[schema(category = \"name\")] to the struct or #[setting(category = \"name\")] to this field",
377            )
378        })
379}
380
381fn generate_field_constructor(
382    field_name: &syn::Ident,
383    field_type: &syn::Type,
384    type_info: TypeInfo,
385    attrs: &FieldAttrs,
386) -> proc_macro2::TokenStream {
387    if attrs.options.is_empty() {
388        generate_setting_type(field_name, field_type, type_info)
389    } else {
390        let options: Vec<_> = attrs
391            .options
392            .iter()
393            .map(|(val, lbl)| {
394                quote! { rcman::SettingOption::new(#val, #lbl) }
395            })
396            .collect();
397        quote! {
398            rcman::SettingMetadata::select(
399                defaults.#field_name.clone(),
400                vec![#(#options),*]
401            )
402        }
403    }
404}
405
406fn generate_field_modifiers(attrs: &FieldAttrs) -> Vec<proc_macro2::TokenStream> {
407    let mut modifiers = Vec::new();
408
409    if let Some(min) = attrs.min {
410        modifiers.push(quote! { .min(#min) });
411    }
412    if let Some(max) = attrs.max {
413        modifiers.push(quote! { .max(#max) });
414    }
415    if let Some(step) = attrs.step {
416        modifiers.push(quote! { .step(#step) });
417    }
418    if let Some(pattern) = &attrs.pattern {
419        modifiers.push(quote! { .pattern(#pattern) });
420    }
421    if attrs.secret {
422        modifiers.push(quote! { .secret() });
423    }
424    if !attrs.reserved.is_empty() {
425        let reserved_items = &attrs.reserved;
426        modifiers.push(quote! { .reserved(vec![#(#reserved_items.to_string()),*]) });
427    }
428
429    for (key, value) in &attrs.metadata_str {
430        modifiers.push(quote! { .meta_str(#key, #value) });
431    }
432    for (key, value) in &attrs.metadata_bool {
433        modifiers.push(quote! { .meta_bool(#key, #value) });
434    }
435    for (key, value) in &attrs.metadata_num {
436        modifiers.push(quote! { .meta_num(#key, #value) });
437    }
438
439    modifiers
440}
441
442fn parse_field_attrs(attrs: &[Attribute]) -> Result<FieldAttrs, syn::Error> {
443    let mut result = FieldAttrs::default();
444
445    for attr in attrs {
446        if attr.path().is_ident("setting") {
447            let nested = attr.parse_args_with(
448                syn::punctuated::Punctuated::<Meta, syn::Token![,]>::parse_terminated,
449            )?;
450
451            for meta in nested {
452                parse_single_field_attr(meta, &mut result)?;
453            }
454        }
455    }
456
457    Ok(result)
458}
459
460fn parse_single_field_attr(meta: Meta, result: &mut FieldAttrs) -> Result<(), syn::Error> {
461    match meta {
462        Meta::Path(path) => {
463            if path.is_ident("secret") {
464                result.secret = true;
465            } else if path.is_ident("skip") {
466                result.skip = true;
467            } else if path.is_ident("nested") {
468                result.nested = true;
469            }
470        }
471        Meta::NameValue(nv) => {
472            let value = &nv.value;
473            if nv.path.is_ident("category") {
474                result.category = Some(parse_lit_str(value, "category")?);
475            } else if nv.path.is_ident("min") {
476                result.min = parse_number_constraint(parse_lit_expr(value, "min")?, "min")?;
477            } else if nv.path.is_ident("max") {
478                result.max = parse_number_constraint(parse_lit_expr(value, "max")?, "max")?;
479            } else if nv.path.is_ident("step") {
480                result.step = parse_number_constraint(parse_lit_expr(value, "step")?, "step")?;
481            } else if nv.path.is_ident("pattern") {
482                result.pattern = Some(parse_lit_str(value, "pattern")?);
483            } else if nv.path.is_ident("rename") {
484                result.rename = Some(parse_lit_str(value, "rename")?);
485            } else {
486                let key = nv
487                    .path
488                    .get_ident()
489                    .map(std::string::ToString::to_string)
490                    .unwrap_or_default();
491                let lit = parse_lit_expr(value, &key)?;
492                parse_metadata_value(key, lit, result)?;
493            }
494        }
495        Meta::List(list) => {
496            if list.path.is_ident("options") {
497                parse_options_list(&list, result)?;
498            } else if list.path.is_ident("reserved") {
499                parse_reserved_list(&list, result)?;
500            }
501        }
502    }
503    Ok(())
504}
505
506fn parse_lit_str(expr: &syn::Expr, name: &str) -> Result<String, syn::Error> {
507    if let syn::Expr::Lit(lit) = expr
508        && let Lit::Str(s) = &lit.lit
509    {
510        return Ok(s.value());
511    }
512    Err(syn::Error::new_spanned(
513        expr,
514        format!("#[setting({name})] must be a string literal"),
515    ))
516}
517
518fn parse_lit_expr<'a>(expr: &'a syn::Expr, name: &str) -> Result<&'a syn::ExprLit, syn::Error> {
519    if let syn::Expr::Lit(lit) = expr {
520        Ok(lit)
521    } else {
522        Err(syn::Error::new_spanned(
523            expr,
524            format!("#[setting({name})] must be a literal"),
525        ))
526    }
527}
528
529/// Container-level attributes from #[schema(...)]
530#[derive(Default)]
531struct ContainerAttrs {
532    category: Option<String>,
533}
534
535/// Field-level attributes from #[setting(...)]
536#[derive(Default)]
537struct FieldAttrs {
538    category: Option<String>,
539    min: Option<f64>,
540    max: Option<f64>,
541    step: Option<f64>,
542    pattern: Option<String>,
543    options: Vec<(String, String)>, // (value, label) pairs for select type
544    reserved: Vec<String>,
545    secret: bool,
546    skip: bool,
547    nested: bool, // Explicit marker for nested structs
548    rename: Option<String>,
549    // Dynamic metadata: any key=value that isn't a known constraint
550    metadata_str: Vec<(String, String)>,
551    metadata_bool: Vec<(String, bool)>,
552    metadata_num: Vec<(String, f64)>,
553}
554
555fn parse_container_attrs(attrs: &[Attribute]) -> Result<ContainerAttrs, syn::Error> {
556    let mut result = ContainerAttrs::default();
557
558    for attr in attrs {
559        if attr.path().is_ident("schema") {
560            let nested = attr.parse_args_with(
561                syn::punctuated::Punctuated::<Meta, syn::Token![,]>::parse_terminated,
562            )?;
563
564            for meta in nested {
565                if let Meta::NameValue(nv) = meta
566                    && nv.path.is_ident("category")
567                {
568                    if let Expr::Lit(lit) = &nv.value {
569                        if let Lit::Str(s) = &lit.lit {
570                            result.category = Some(s.value());
571                        } else {
572                            return Err(syn::Error::new_spanned(
573                                lit,
574                                "#[schema(category)] must be a string literal",
575                            ));
576                        }
577                    } else {
578                        return Err(syn::Error::new_spanned(
579                            &nv.value,
580                            "#[schema(category)] must be a string literal, not an expression",
581                        ));
582                    }
583                }
584            }
585        }
586    }
587
588    Ok(result)
589}
590
591/// Parse a numeric constraint (min, max, or step)
592fn parse_number_constraint(
593    lit: &syn::ExprLit,
594    constraint_name: &str,
595) -> Result<Option<f64>, syn::Error> {
596    match &lit.lit {
597        Lit::Float(f) => Ok(f.base10_parse().ok()),
598        Lit::Int(i) => Ok(i.base10_parse().ok()),
599        Lit::Str(_) => Err(syn::Error::new_spanned(
600            lit,
601            format!(
602                "#[setting({constraint_name})] expects a number, found string literal (hint: remove quotes, use `{constraint_name} = 10`)"
603            ),
604        )),
605        Lit::Bool(_) => Err(syn::Error::new_spanned(
606            lit,
607            format!(
608                "#[setting({constraint_name})] expects a number, found boolean (hint: use `{constraint_name} = 10`)"
609            ),
610        )),
611        _ => Err(syn::Error::new_spanned(
612            lit,
613            format!(
614                "#[setting({constraint_name})] must be a number literal (e.g., `{constraint_name} = 10` or `{constraint_name} = 10.5`)"
615            ),
616        )),
617    }
618}
619
620/// Parse custom metadata value from literal
621fn parse_metadata_value(
622    key: String,
623    lit: &syn::ExprLit,
624    result: &mut FieldAttrs,
625) -> Result<(), syn::Error> {
626    match &lit.lit {
627        Lit::Str(s) => {
628            result.metadata_str.push((key, s.value()));
629            Ok(())
630        }
631        Lit::Bool(b) => {
632            result.metadata_bool.push((key, b.value()));
633            Ok(())
634        }
635        Lit::Int(i) => {
636            if let Ok(val) = i.base10_parse::<i64>() {
637                #[allow(clippy::cast_precision_loss)]
638                result.metadata_num.push((key, val as f64));
639            }
640            Ok(())
641        }
642        Lit::Float(f) => {
643            if let Ok(val) = f.base10_parse::<f64>() {
644                result.metadata_num.push((key, val));
645            }
646            Ok(())
647        }
648        _ => Err(syn::Error::new_spanned(
649            lit,
650            format!(
651                "Metadata value for '{key}' must be a string, number, or boolean literal (hint: use \\\"text\\\", 123, or true/false)"
652            ),
653        )),
654    }
655}
656
657/// Parse options list from #[setting(options = [...])]
658fn parse_options_list(list: &syn::MetaList, result: &mut FieldAttrs) -> Result<(), syn::Error> {
659    let items = list
660        .parse_args_with(syn::punctuated::Punctuated::<Expr, syn::Token![,]>::parse_terminated)?;
661
662    for item in items {
663        let Expr::Tuple(tuple) = &item else {
664            return Err(syn::Error::new_spanned(
665                &item,
666                "#[setting(options)] must be an array of tuples: [(\"val\", \"Label\"), ...]",
667            ));
668        };
669
670        if tuple.elems.len() != 2 {
671            return Err(syn::Error::new_spanned(
672                tuple,
673                "#[setting(options)] tuples must have exactly 2 elements: (\"value\", \"Label\")",
674            ));
675        }
676
677        let mut vals = tuple.elems.iter();
678        match (vals.next(), vals.next()) {
679            (Some(Expr::Lit(v)), Some(Expr::Lit(l))) => match (&v.lit, &l.lit) {
680                (Lit::Str(val), Lit::Str(label)) => {
681                    result.options.push((val.value(), label.value()));
682                }
683                _ => {
684                    return Err(syn::Error::new_spanned(
685                        tuple,
686                        "#[setting(options)] tuple elements must be string literals",
687                    ));
688                }
689            },
690            _ => {
691                return Err(syn::Error::new_spanned(
692                    tuple,
693                    "#[setting(options)] tuple elements must be string literals",
694                ));
695            }
696        }
697    }
698    Ok(())
699}
700
701fn parse_reserved_list(list: &syn::MetaList, result: &mut FieldAttrs) -> Result<(), syn::Error> {
702    let items = list
703        .parse_args_with(syn::punctuated::Punctuated::<Expr, syn::Token![,]>::parse_terminated)?;
704
705    for item in items {
706        if let Expr::Lit(lit) = item {
707            if let Lit::Str(s) = lit.lit {
708                result.reserved.push(s.value());
709            } else {
710                return Err(syn::Error::new_spanned(
711                    lit,
712                    "#[setting(reserved)] values must be string literals",
713                ));
714            }
715        } else {
716            return Err(syn::Error::new_spanned(
717                item,
718                "#[setting(reserved)] values must be string literals",
719            ));
720        }
721    }
722    Ok(())
723}
724
725/// Classification of Rust types for settings generation
726#[derive(Copy, Clone)]
727enum TypeInfo {
728    Toggle,  // bool
729    Text,    // String
730    Path,    // PathBuf
731    Number,  // i8, i16, i32, u32, f32, f64, etc.
732    List,    // Vec<T>
733    Unknown, // Everything else (may be nested struct or std type we don't handle)
734}
735
736/// Extract the last segment's identifier from a type path, ignoring generics.
737/// Example: `std::vec::Vec<String>` -> `Some(Vec)`
738fn get_last_path_segment_ident(ty: &Type) -> Option<&syn::Ident> {
739    if let Type::Path(path) = ty {
740        path.path.segments.last().map(|seg| &seg.ident)
741    } else {
742        None
743    }
744}
745
746/// Classify a type for settings schema generation
747///
748/// Uses a whitelist approach: known primitives/std types are classified,
749/// everything else returns Unknown (could be nested struct or unsupported std type).
750fn classify_type(ty: &Type) -> TypeInfo {
751    if let Some(ident) = get_last_path_segment_ident(ty) {
752        let name = ident.to_string();
753        match name.as_str() {
754            "bool" => return TypeInfo::Toggle,
755            "String" => return TypeInfo::Text,
756            "PathBuf" => return TypeInfo::Path,
757            "i8" | "i16" | "i32" | "i64" | "i128" | "isize" | "u8" | "u16" | "u32" | "u64"
758            | "u128" | "usize" | "f32" | "f64" => return TypeInfo::Number,
759            // Check for Vec specifically
760            "Vec" => return TypeInfo::List,
761            // Other std types that are NOT nested structs
762            "str" | "char" | "OsString" | "CString" | "Duration" | "Instant" | "SystemTime"
763            | "Box" | "Rc" | "Arc" | "Cow" | "VecDeque" | "HashMap" | "HashSet" | "BTreeMap"
764            | "BTreeSet" | "LinkedList" | "Option" | "Result" => {
765                return TypeInfo::Unknown;
766            }
767            _ => return TypeInfo::Unknown,
768        }
769    }
770
771    TypeInfo::Unknown
772}
773
774/// Extract the inner type from Option<T> if the given type is an Option
775fn extract_inner_type_from_option(ty: &Type) -> Option<&Type> {
776    if let Type::Path(path) = ty
777        && let Some(segment) = path.path.segments.last()
778        && segment.ident == "Option"
779        && let syn::PathArguments::AngleBracketed(args) = &segment.arguments
780        && let Some(syn::GenericArgument::Type(inner_ty)) = args.args.first()
781    {
782        return Some(inner_ty);
783    }
784    None
785}
786
787/// Check if a type is likely a nested struct (not a primitive)
788///
789/// This uses a conservative whitelist approach: known primitive/std types
790/// return false, everything else is assumed to be a nested struct.
791///
792/// For edge cases (like `Option<MyStruct>`), use explicit `#[setting(nested)]`.
793fn is_nested_struct(ty: &Type) -> bool {
794    // If it's an Option<T>, check the inner type T
795    if let Some(inner) = extract_inner_type_from_option(ty) {
796        return is_nested_struct(inner);
797    }
798
799    // Only simple path types with single ident can be nested
800    if let Type::Path(path_ty) = ty
801        && get_last_path_segment_ident(ty).is_some()
802    {
803        // Must not have type arguments (like Option<T> or Vec<T>) to be auto-detected as a nested struct
804        if path_ty.path.segments.last().unwrap().arguments.is_empty() {
805            // Use classify_type: Unknown + simple ident = likely custom struct
806            return matches!(classify_type(ty), TypeInfo::Unknown);
807        }
808    }
809    false
810}
811
812/// Generate the appropriate `SettingMetadata` constructor based on type
813fn generate_setting_type(
814    field_name: &syn::Ident,
815    ty: &Type,
816    type_info: TypeInfo,
817) -> proc_macro2::TokenStream {
818    let is_option = extract_inner_type_from_option(ty).is_some();
819
820    match type_info {
821        TypeInfo::Toggle => {
822            if is_option {
823                quote! { rcman::SettingMetadata::toggle(defaults.#field_name.unwrap_or_default()) }
824            } else {
825                quote! { rcman::SettingMetadata::toggle(defaults.#field_name) }
826            }
827        }
828        TypeInfo::Text => {
829            if is_option {
830                quote! { rcman::SettingMetadata::text(defaults.#field_name.clone().unwrap_or_default()) }
831            } else {
832                quote! { rcman::SettingMetadata::text(defaults.#field_name.clone()) }
833            }
834        }
835        TypeInfo::Path => {
836            if is_option {
837                quote! {
838                    rcman::SettingMetadata::text(
839                        defaults.#field_name.as_ref()
840                            .map(|p| p.to_string_lossy().into_owned())
841                            .unwrap_or_default()
842                    )
843                    .meta_str("input_type", "path")
844                }
845            } else {
846                quote! {
847                    rcman::SettingMetadata::text(
848                        defaults.#field_name.to_string_lossy().into_owned()
849                    )
850                    .meta_str("input_type", "path")
851                }
852            }
853        }
854        TypeInfo::Number => {
855            if is_option {
856                quote! { rcman::SettingMetadata::number(defaults.#field_name.unwrap_or_default() as f64) }
857            } else {
858                quote! { rcman::SettingMetadata::number(defaults.#field_name as f64) }
859            }
860        }
861        TypeInfo::List => {
862            quote! {
863                rcman::SettingMetadata::list(
864                    &(defaults.#field_name
865                        .iter()
866                        .map(|it| it.to_string())
867                        .collect::<Vec<String>>())[..]
868                )
869            }
870        }
871        TypeInfo::Unknown => {
872            unreachable!("Unknown types are rejected in process_field")
873        }
874    }
875}