easy-config-macros 0.1.2

Procedural macros for the easy-config crate.
Documentation
extern crate proc_macro;

use proc_macro::TokenStream;
use quote::quote;
use syn::{
    Data, DeriveInput, Expr, Fields, GenericArgument, Lit, Meta, PathArguments, Type,
    parse_macro_input, punctuated::Punctuated, token,
};

#[proc_macro_derive(EasyConfig, attributes(attr, merge))]
pub fn easy_config_derive(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);
    let struct_name = &input.ident;

    let fields = if let Data::Struct(data) = input.data {
        if let Fields::Named(fields) = data.fields {
            fields.named
        } else {
            panic!("Only structs with named fields are supported")
        }
    } else {
        panic!("Only structs are supported")
    };

    let mut config_key_inits = Vec::new();
    let mut from_props_fields = Vec::new();
    let mut getter_methods = Vec::new();

    for f in fields.iter() {
        let field_name = f.ident.as_ref().unwrap();
        let ty = &f.ty;

        // Check if this field should be merged.
        let is_merge_field = f.attrs.iter().any(|attr| attr.path().is_ident("merge"));

        if is_merge_field {
            // --- Logic for a `#[merge]` field ---
            config_key_inits.push(quote! {
                <#ty as FromConfigDef>::config_def()?.config_keys().values().cloned()
            }); // This will be flattened later

            from_props_fields.push(quote! {
                #field_name: <#ty as FromConfigDef>::from_props(props)?
            });
        } else {
            let mut generate_getter = false;
            let field_name_str = field_name.to_string();
            let mut name_attr = None;
            let mut docs = None;
            let mut default = None;
            let mut importance = None;
            let mut validator = None;
            let mut group = None;
            let mut internal_config = false;

            for attr in &f.attrs {
                if attr.path().is_ident("attr") {
                    let parsed_attrs = attr
                        .parse_args_with(Punctuated::<Meta, token::Comma>::parse_terminated)
                        .expect("Failed to parse config attributes");

                    for meta in parsed_attrs {
                        match meta {
                            Meta::Path(path) if path.is_ident("getter") => {
                                generate_getter = true;
                            }
                            Meta::NameValue(nv) => {
                                let ident = nv.path.get_ident().unwrap().to_string();
                                match ident.as_str() {
                                    "name" => name_attr = Some(get_expr(&nv.value).unwrap()),
                                    "documentation" => docs = Some(get_expr(&nv.value).unwrap()),
                                    "default" => default = Some(get_expr(&nv.value).unwrap()),
                                    "group" => group = Some(get_expr(&nv.value).unwrap()),
                                    "importance" => importance = Some(get_expr(&nv.value).unwrap()),
                                    "validator" => validator = Some(get_expr(&nv.value).unwrap()),
                                    "internal_config" => {
                                        internal_config = get_bool_from_expr(&nv.value).unwrap()
                                    }
                                    _ => panic!("Unknown attribute: {}", ident),
                                }
                            }
                            _ => { /* Ignore other attribute types */ }
                        }
                    }
                }
            }

            if generate_getter {
                // We always return a borrow (`&T`). This is safe for all types.
                // For Copy types, the user can dereference with `*`.
                getter_methods.push(quote! {
                    pub fn #field_name(&self) -> &#ty {
                        &self.#field_name
                    }
                });
            }

            let lookup_key = name_attr.map_or(quote! { #field_name_str }, |e| quote! { #e });
            let docs_tokens = docs
                .map(|d| quote! { Some(Into::<String>::into(#d)) })
                .unwrap_or(quote! { None });
            let default_tokens = default
                .map(|d| quote! { Some(Into::<String>::into(#d)) })
                .unwrap_or(quote! { None });
            let importance_tokens = importance
                .map(|i| quote! { Some(#i) })
                .unwrap_or(quote! { None });
            let validator_tokens = validator
                .map(|v| quote! { Some(#v) })
                .unwrap_or(quote! { None });
            let group_tokens = group
                .map(|g| quote! { Some(Into::<String>::into(#g)) })
                .unwrap_or(quote! { None });

            config_key_inits.push(quote! {
                std::iter::once(ConfigKey {
                    name: #lookup_key,
                    documentation: #docs_tokens,
                    default_value: #default_tokens,
                    importance: #importance_tokens,
                    validator: #validator_tokens,
                    group: #group_tokens,
                    internal_config: #internal_config,
                })
            });

            let from_props_quote = if let Type::Path(type_path) = ty
                && type_path.path.segments.len() == 1
                && type_path.path.segments[0].ident == "Option"
            {
                let inner_ty = if let PathArguments::AngleBracketed(args) =
                    &type_path.path.segments[0].arguments
                {
                    if let Some(GenericArgument::Type(inner)) = args.args.first() {
                        inner
                    } else {
                        panic!("Option must have a generic type argument")
                    }
                } else {
                    panic!("Option must have a generic type argument")
                };

                // Logic for `Option<T>` fields
                quote! {
                    #field_name: {
                        if let Some(val_str) = get_value(#lookup_key).ok() {
                            Some(<#inner_ty as ConfigValue>::parse(#lookup_key, val_str)?)
                        } else {
                            None
                        }
                    }
                }
            } else {
                // Logic for required fields (non-Option)
                quote! {
                    #field_name: {
                        let val_str = get_value(#lookup_key)?;
                        <#ty as ConfigValue>::parse(#lookup_key, val_str)?
                    }
                }
            };
            from_props_fields.push(from_props_quote);
        }
    }

    let expanded = quote! {
        static CONFIG_DEF: once_cell::sync::OnceCell<ConfigDef> = once_cell::sync::OnceCell::new();
        impl #struct_name {
            pub fn config_def() -> Result<&'static ConfigDef, ConfigError> {
                CONFIG_DEF.get_or_try_init(|| {
                    let keys: Vec<ConfigKey> = [
                        #(#config_key_inits),*
                    ].into_iter().flatten().collect();

                    ConfigDef::try_from(keys)
                })
            }

            // Place generated getters in the main impl block
            #(#getter_methods)*
        }
        impl FromConfigDef for #struct_name {
            fn from_props(props: &std::collections::HashMap<String, String>) -> Result<Self, ConfigError> {
                let def = Self::config_def()?;
                let get_value = |name: &str| -> Result<&str, ConfigError> {
                    let meta = def.find_key(name).ok_or_else(|| ConfigError::MissingName(name.to_string()))?;
                    let val_str = props.get(name).map(|s| s.as_str()).or(meta.default_value.as_deref())
                        .ok_or_else(|| ConfigError::MissingName(name.to_string()))?;
                    if let Some(validator) = &meta.validator {
                        validator.validate(name, val_str)?;
                    }
                    Ok(val_str)
                };
                Ok(Self { #(#from_props_fields),* })
            }

            // Re-direct the trait method to our generated inherent method
            fn config_def() -> Result<&'static ConfigDef, ConfigError> {
                Self::config_def()
            }
        }
    };
    TokenStream::from(expanded)
}

// --- Helper Functions for Attribute Parsing ---

/// Extracts a `String` from a string literal expression (e.g., `"hello"`).
/// Returns a `syn::Error` if the expression is not a string literal.
#[allow(dead_code)]
fn get_string_lit_from_expr(expr: &Expr) -> syn::Result<String> {
    if let Expr::Lit(expr_lit) = expr
        && let Lit::Str(lit_str) = &expr_lit.lit
    {
        return Ok(lit_str.value());
    }
    Err(syn::Error::new_spanned(expr, "Expected a string literal"))
}

fn get_bool_from_expr(expr: &Expr) -> syn::Result<bool> {
    if let Expr::Lit(expr_lit) = expr
        && let Lit::Bool(lit_str) = &expr_lit.lit
    {
        return Ok(lit_str.value());
    }
    Err(syn::Error::new_spanned(expr, "Expected a bool literal"))
}

fn get_expr(expr: &Expr) -> syn::Result<Expr> {
    Ok(expr.clone())
}