congen-derive 0.2.0

congen helps you build configuration systems that support partial updates from structured changes and CLI input
Documentation
use quote::{ToTokens, format_ident, quote};
use syn::{
    Attribute, Ident, Item, Meta, Token, parse::Parse, parse_macro_input, parse2,
    punctuated::Punctuated,
};

use crate::{enum_config::enum_configuration, struct_config::struct_configuration};

mod enum_config;
mod struct_config;

/// See documentation in the congen crate
#[proc_macro_derive(Configuration, attributes(congen))]
pub fn configuration(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    let input = parse_macro_input!(input as Item);

    match input {
        Item::Struct(input) => struct_configuration(input),
        Item::Enum(input) => enum_configuration(input),
        _ => quote! { compile_error!("Configuration only supports structs and enums") },
    }
    .into()
}

enum AttributeParam {
    Flag(Ident),
    NameValue {
        ident: Ident,
        value: syn::Expr,
        eq: Token![=],
    },
}

impl AttributeParam {
    pub fn ident(&self) -> &Ident {
        match self {
            AttributeParam::Flag(ident) => ident,
            AttributeParam::NameValue { ident, .. } => ident,
        }
    }
}

impl Parse for AttributeParam {
    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
        let ident: Ident = input.parse()?;

        if input.peek(Token![=]) {
            let eq = input.parse()?;
            let value = input.parse()?;

            Ok(AttributeParam::NameValue { ident, value, eq })
        } else {
            Ok(AttributeParam::Flag(ident))
        }
    }
}

impl ToTokens for AttributeParam {
    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
        match self {
            AttributeParam::Flag(ident) => ident.to_tokens(tokens),
            AttributeParam::NameValue { ident, value, eq } => {
                ident.to_tokens(tokens);
                eq.to_tokens(tokens);
                value.to_tokens(tokens);
            }
        }
    }
}

#[derive(Default)]
struct CongenItemAttribute {
    debug: bool,
    default: bool,
    clone: bool,
}

impl CongenItemAttribute {
    fn combine(&self, other: &Self) -> Self {
        Self {
            debug: self.debug || other.debug,
            default: self.default || other.default,
            clone: self.clone || other.clone,
        }
    }

    fn from_attrs<'a, I: IntoIterator<Item = &'a Attribute>>(
        iter: I,
        errors: &mut Vec<syn::Error>,
    ) -> Self {
        iter.into_iter()
            .filter_map(|attr| match &attr.meta {
                Meta::List(meta_list) if meta_list.path.is_ident(&format_ident!("congen")) => {
                    match parse2(meta_list.tokens.clone()) {
                        Ok(attr) => Some(attr),
                        Err(err) => {
                            errors.push(err);
                            None
                        }
                    }
                }
                _ => None,
            })
            .fold(CongenItemAttribute::default(), |a, b| a.combine(&b))
    }
}

impl Parse for CongenItemAttribute {
    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
        let args = Punctuated::<AttributeParam, Token![,]>::parse_terminated(input)?;

        let mut impl_debug = false;
        let mut gen_default = false;
        let mut clone = false;

        for arg in args {
            match arg.ident().to_string().as_str() {
                "default" => {
                    if gen_default {
                        return Err(syn::Error::new_spanned(
                            arg,
                            "\"default\" should only be specified once in `congen` attribute",
                        ));
                    }
                    if !matches!(arg, AttributeParam::Flag(_)) {
                        return Err(syn::Error::new_spanned(
                            arg,
                            "\"default\" is a flag and takes no arguments",
                        ));
                    }
                    gen_default = true;
                }
                "debug" => {
                    if impl_debug {
                        return Err(syn::Error::new_spanned(
                            arg,
                            "\"debug\" should only be specified once in `congen` attribute",
                        ));
                    }
                    if !matches!(arg, AttributeParam::Flag(_)) {
                        return Err(syn::Error::new_spanned(
                            arg,
                            "\"debug\" is a flag and takes no arguments",
                        ));
                    }
                    impl_debug = true;
                }
                "clone" => {
                    if clone {
                        return Err(syn::Error::new_spanned(
                            arg,
                            "\"clone\" should only be specified once in `congen` attribute",
                        ));
                    }
                    if !matches!(arg, AttributeParam::Flag(_)) {
                        return Err(syn::Error::new_spanned(
                            arg,
                            "\"clone\" is a flag and takes no arguments",
                        ));
                    }
                    clone = true;
                }
                _ => {
                    return Err(syn::Error::new_spanned(
                        arg,
                        "unknown argument to `congen` attribute",
                    ));
                }
            }
        }

        Ok(CongenItemAttribute {
            debug: impl_debug,
            default: gen_default,
            clone,
        })
    }
}