confgr_derive 0.2.1

Procedural derive macro implementation for the confgr crate
Documentation
use quote::{quote, ToTokens};
use syn::{
    parse_macro_input, punctuated::Punctuated, Attribute, Data, DeriveInput, Error, Expr, ExprLit,
    Fields, Ident, Lit, Meta, Token, Type,
};

mod config;
mod convert;
mod env;
mod file;
mod merge;

const SUFFIX: &str = "ConfgrLayer";
const AUTOCONF_ATTRIBUTE: &str = "config";
const PATH_ATTRIBUTE: &str = "path";
const DEFAULT_PATH_ATTRIBUTE: &str = "default_path";
const ENV_PATH_ATTRIBUTE: &str = "env_path";
const KEY_ATTRIBUTE: &str = "key";
const PREFIX_ATTRIBUTE: &str = "prefix";
const SEPARATOR_ATTRIBUTE: &str = "separator";
const NEST_ATTRIBUTE: &str = "nest";
const SKIP_ATTRIBUTE: &str = "skip";
const NAME_ATTRIBUTE: &str = "name";

#[proc_macro_derive(Config, attributes(config))]
pub fn config_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    let ast = parse_macro_input!(input as DeriveInput);
    match impl_config_derive(&ast) {
        Ok(expanded) => expanded.into(),
        Err(errors) => to_compile_errors(errors).into(),
    }
}

fn impl_config_derive(ast: &DeriveInput) -> Result<proc_macro2::TokenStream, Vec<syn::Error>> {
    let name = &ast.ident;
    let struct_attributes =
        parse_config_field_attributes(&ast.attrs).unwrap_or_else(|_| ConfigAttributes::default());

    let field_data = extract_fields(ast)?;

    let layer_impl = merge::generate_layer(name, &struct_attributes, &field_data);
    let config_impl = config::generate_config_impl(name);
    let from_impl = convert::generate_conversion_impl(name, &field_data);
    let env_impl = env::generate_from_env(name, &struct_attributes, &field_data);
    let file_impl = file::generate_from_file(name, &struct_attributes);

    let expanded = quote! {
        #layer_impl
        #from_impl
        #env_impl
        #file_impl
        #config_impl
    };

    Ok(expanded)
}

pub(crate) fn extract_fields(
    input: &DeriveInput,
) -> Result<Vec<(&Ident, &Type, ConfigAttributes)>, Vec<syn::Error>> {
    if let Data::Struct(data) = &input.data {
        if let Fields::Named(fields) = &data.fields {
            let mut field_data = Vec::new();
            let mut errors = Vec::new();

            for f in fields.named.iter() {
                match parse_config_field_attributes(&f.attrs) {
                    Ok(attributes) => {
                        field_data.push((f.ident.as_ref().unwrap(), &f.ty, attributes))
                    }
                    Err(mut errs) => errors.append(&mut errs),
                }
            }

            if errors.is_empty() {
                Ok(field_data)
            } else {
                Err(errors)
            }
        } else {
            Err(vec![syn::Error::new_spanned(
                &data.fields,
                "Config derive macro only supports structs with named fields.",
            )])
        }
    } else {
        Err(vec![syn::Error::new_spanned(
            input,
            "Config derive macro only supports structs.",
        )])
    }
}

pub(crate) fn parse_config_field_attributes(
    attrs: &[Attribute],
) -> Result<ConfigAttributes, Vec<syn::Error>> {
    let mut attributes = ConfigAttributes::new();
    let mut errors = Vec::new();

    for attr in attrs
        .iter()
        .filter(|a| a.path().is_ident(AUTOCONF_ATTRIBUTE))
    {
        let result = attr.parse_args_with(Punctuated::<Meta, Token![,]>::parse_terminated);
        match result {
            Ok(list) => {
                for meta in list {
                    match meta {
                        Meta::Path(path) if path.is_ident(SKIP_ATTRIBUTE) => attributes.skip = true,
                        Meta::Path(path) if path.is_ident(NEST_ATTRIBUTE) => attributes.nest = true,
                        Meta::NameValue(named_value)
                            if named_value.path.is_ident(PATH_ATTRIBUTE) =>
                        {
                            if let Expr::Lit(ExprLit {
                                lit: Lit::Str(path),
                                ..
                            }) = &named_value.value
                            {
                                attributes.path = Some(path.value());
                            } else {
                                errors.push(Error::new_spanned(
                                    named_value.into_token_stream(),
                                    "Expected a valid path for 'path'",
                                ));
                            }
                        }
                        Meta::NameValue(named_value)
                            if named_value.path.is_ident(PREFIX_ATTRIBUTE) =>
                        {
                            if let Expr::Lit(ExprLit {
                                lit: Lit::Str(prefix),
                                ..
                            }) = &named_value.value
                            {
                                attributes.prefix = Some(prefix.value());
                            } else {
                                errors.push(Error::new_spanned(
                                    named_value.into_token_stream(),
                                    "Expected a string for 'prefix'",
                                ));
                            }
                        }
                        Meta::NameValue(named_value)
                            if named_value.path.is_ident(KEY_ATTRIBUTE) =>
                        {
                            if let Expr::Lit(ExprLit {
                                lit: Lit::Str(key), ..
                            }) = &named_value.value
                            {
                                attributes.key = Some(key.value());
                            } else {
                                errors.push(Error::new_spanned(
                                    named_value.into_token_stream(),
                                    "Expected a string for 'key'",
                                ));
                            }
                        }
                        Meta::NameValue(named_value)
                            if named_value.path.is_ident(SEPARATOR_ATTRIBUTE) =>
                        {
                            if let Expr::Lit(ExprLit {
                                lit: Lit::Str(seperator),
                                ..
                            }) = &named_value.value
                            {
                                attributes.separator = Some(seperator.value());
                            } else {
                                errors.push(Error::new_spanned(
                                    named_value.into_token_stream(),
                                    "Expected a string for 'seperator'",
                                ));
                            }
                        }
                        Meta::NameValue(named_value)
                            if named_value.path.is_ident(ENV_PATH_ATTRIBUTE) =>
                        {
                            if let Expr::Lit(ExprLit {
                                lit: Lit::Str(env_path),
                                ..
                            }) = &named_value.value
                            {
                                attributes.env_path = Some(env_path.value());
                            } else {
                                errors.push(Error::new_spanned(
                                    named_value.into_token_stream(),
                                    "Expected a string for 'env_path'",
                                ));
                            }
                        }
                        Meta::NameValue(named_value)
                            if named_value.path.is_ident(DEFAULT_PATH_ATTRIBUTE) =>
                        {
                            if let Expr::Lit(ExprLit {
                                lit: Lit::Str(default_path),
                                ..
                            }) = &named_value.value
                            {
                                attributes.default_path = Some(default_path.value());
                            } else {
                                errors.push(Error::new_spanned(
                                    named_value.into_token_stream(),
                                    "Expected a string for 'default_path'",
                                ));
                            }
                        }
                        Meta::NameValue(named_value)
                            if named_value.path.is_ident(NAME_ATTRIBUTE) =>
                        {
                            if let Expr::Lit(ExprLit {
                                lit: Lit::Str(name),
                                ..
                            }) = &named_value.value
                            {
                                attributes.name = Some(name.value());
                            } else {
                                errors.push(Error::new_spanned(
                                    named_value.into_token_stream(),
                                    "Expected a string for 'name'",
                                ));
                            }
                        }
                        _ => {
                            errors.push(Error::new_spanned(
                                meta.into_token_stream(),
                                "Unsupported attribute in 'config'",
                            ));
                        }
                    }
                }
            }
            Err(e) => errors.push(e),
        }
    }

    if errors.is_empty() {
        Ok(attributes)
    } else {
        Err(errors)
    }
}

pub(crate) fn get_ident_from_type(ty: &Type) -> proc_macro2::Ident {
    if let Type::Path(type_path) = ty {
        type_path.path.segments.last().unwrap().ident.clone()
    } else {
        panic!("Type is not a Type::Path, which is required for nested builder patterns")
    }
}

#[derive(Debug, Default)]
pub(crate) struct ConfigAttributes {
    skip: bool,
    nest: bool,
    prefix: Option<String>,
    key: Option<String>,
    separator: Option<String>,
    path: Option<String>,
    env_path: Option<String>,
    default_path: Option<String>,
    name: Option<String>,
}

impl ConfigAttributes {
    fn new() -> Self {
        Self::default()
    }
}

fn to_compile_errors(errors: Vec<syn::Error>) -> proc_macro2::TokenStream {
    let compile_errors = errors.iter().map(syn::Error::to_compile_error);
    quote!(#(#compile_errors)*)
}