congen-derive 0.1.0

congen helps you build configuration systems that support partial updates from structured changes and CLI input
Documentation
use proc_macro2::TokenStream;
use quote::{ToTokens, format_ident, quote};
use syn::{
    Expr, Field, GenericArgument, Ident, Meta, PathArguments, Token, Type, parse::Parse, parse2,
    punctuated::Punctuated,
};

pub enum AttributeParam {
    Flag(Ident),
    NameValue {
        ident: Ident,
        value: 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)]
pub struct CongenFieldAttribute {
    pub default: Option<CongenDefault>,
    pub inner_default: Option<Expr>,
}

pub enum CongenDefault {
    UseDefault,
    UseRust,
    Expr(Expr),
}

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

        let mut default = None;
        let mut inner_default = None;

        for arg in args {
            match arg.ident().to_string().as_str() {
                "rust_default" => {
                    if default.is_some() {
                        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,
                            "\"rust_default\" takes no arguments",
                        ));
                    }

                    default = Some(CongenDefault::UseRust);
                }
                "default" => {
                    if default.is_some() {
                        return Err(syn::Error::new_spanned(
                            arg,
                            "\"default\" should only be specified once in `congen` attribute",
                        ));
                    }

                    default = Some(match arg {
                        AttributeParam::Flag(_ident) => CongenDefault::UseDefault,
                        AttributeParam::NameValue { value, .. } => CongenDefault::Expr(value),
                    });
                }
                "inner_default" => {
                    if inner_default.is_some() {
                        return Err(syn::Error::new_spanned(
                            arg,
                            "\"inner_default\" should only be specified once in `congen` attribute",
                        ));
                    }

                    let AttributeParam::NameValue { value: expr, .. } = arg else {
                        return Err(syn::Error::new_spanned(
                            arg,
                            "\"inner_default\" requires expression as argument",
                        ));
                    };

                    inner_default = Some(expr);
                }
                _ => {
                    return Err(syn::Error::new_spanned(
                        arg,
                        "unknown argument to `congen` attribute",
                    ));
                }
            }
        }

        Ok(CongenFieldAttribute {
            default,
            inner_default,
        })
    }
}

pub struct CongenField {
    pub attr: CongenFieldAttribute,
    pub field: Field,
    pub inner_type: Option<InnerType>,
    pub is_option: bool,
}

#[allow(clippy::large_enum_variant)]
pub enum InnerType {
    /// used for Lists and Option
    Single(Type),
    /// used for map types (key, value)
    #[expect(unused)]
    Double(Type, Type),
}

impl InnerType {
    pub fn field_type(&self) -> &Type {
        match self {
            InnerType::Single(typ) => typ,
            InnerType::Double(_, typ) => typ,
        }
    }
}

impl CongenField {
    pub fn from_field(errors: &mut Vec<syn::Error>, field: Field) -> CongenField {
        let attr = field
            .attrs
            .iter()
            .find_map(|attr| match &attr.meta {
                Meta::List(meta_list) if meta_list.path.is_ident(&format_ident!("congen")) => {
                    Some(parse2(meta_list.tokens.clone()))
                }
                _ => None,
            })
            .unwrap_or(Ok(CongenFieldAttribute::default()))
            .unwrap_or_else(|err| {
                errors.push(err);
                CongenFieldAttribute::default()
            });

        let inner_type = get_inner_type(&field.ty).unwrap_or_else(|err| {
            errors.push(err);
            None
        });
        let is_option = is_option(&field.ty).unwrap_or_else(|err| {
            errors.push(err);
            false
        });

        CongenField {
            attr,
            field,
            inner_type,
            is_option,
        }
    }

    pub fn derive_default_constructor(&self) -> TokenStream {
        let Some(inner_type) = self.inner_type.as_ref() else {
            return quote! { None };
        };
        let inner_type = inner_type.field_type();

        if let Some(inner_default) = self.attr.inner_default.as_ref() {
            quote! {
                Some(|| {
                    let inner_default: #inner_type = #inner_default;
                    Box::new(inner_default)
                })
            }
        } else if let Some(CongenDefault::Expr(expr)) = self.attr.default.as_ref()
            && self.is_option
        {
            // Special case Option with default expr. If expr is `Some(inner)` we can reuse
            // `inner` as the default for the inner type
            quote! {
                Some(|| {
                    let outer_default: core::option::Option<#inner_type> = #expr;
                    let inner_default: #inner_type = core::option::Option::expect(outer_default,
                        concat!("In order to use default of Option<",
                            stringify!(#inner_type),
                            "> for inner type default the default must be Some(_)."));
                    Box::new(inner_default)
                })
            }
        } else {
            quote! {
                Some(|| {
                    let inner_default = <#inner_type as congen::internal::CongenInternal>::default();
                    let inner_default: #inner_type = core::result::Result::expect(inner_default,
                        concat!(stringify!(#inner_type), " does not implement Configuration::default")
                    );
                    Box::new(inner_default)
                })
            }
        }
    }
}

fn get_inner_type(ty: &Type) -> Result<Option<InnerType>, syn::Error> {
    let Type::Path(path) = ty else {
        return Ok(None);
    };

    if path.qself.is_some() {
        return Err(syn::Error::new_spanned(
            ty,
            "Qself path type not supported in option for congen",
        ));
    }

    let path = &path.path;

    let Some(last_segment) = path.segments.iter().last() else {
        return Err(syn::Error::new_spanned(
            ty,
            "Empty path. Is this even possible?",
        ));
    };

    let PathArguments::AngleBracketed(opt_inner_type_args) = &last_segment.arguments else {
        // no type arguments
        return Ok(None);
    };

    let mut args = opt_inner_type_args.args.iter().cloned();

    // we only support generic types. Const, lifetimes, etc means that this is a type we don't
    // support so we don't need to record the inner type.
    let Some(GenericArgument::Type(first_inner)) = args.next() else {
        return Ok(None);
    };
    let Some(GenericArgument::Type(second_inner)) = args.next() else {
        return Ok(Some(InnerType::Single(first_inner)));
    };
    Ok(Some(InnerType::Double(first_inner, second_inner)))
}

fn is_option(ty: &Type) -> Result<bool, syn::Error> {
    let Type::Path(path) = ty else {
        return Ok(false);
    };
    if path.qself.is_some() {
        return Err(syn::Error::new_spanned(
            ty,
            "Qself path type not supported in option for congen",
        ));
    }

    let path = &path.path;

    if path.leading_colon.is_some() {
        return Ok(false);
    }

    let mut segments = path.segments.iter();
    let Some(first) = segments.next() else {
        return Err(syn::Error::new_spanned(
            ty,
            "Empty path. Is this even possible?",
        ));
    };
    match first.ident.to_string().as_str() {
        "Option" => Ok(true),
        "std" | "core" => {
            let Some(opt) = segments.next() else {
                return Ok(false);
            };
            if opt.ident != "option" {
                return Ok(false);
            };
            let Some(opt) = segments.next() else {
                return Ok(false);
            };
            if opt.ident != "Option" {
                return Ok(false);
            };
            Ok(true)
        }
        _ => Ok(false),
    }
}