congen-derive 0.2.1

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

use crate::{AttributeParam, CongenItemAttribute};

pub fn enum_configuration(input: ItemEnum) -> TokenStream {
    let mut errors = Vec::new();

    let congen_attr = CongenItemAttribute::from_attrs(input.attrs.iter(), &mut errors);

    let mut default_variant = None;
    let variants: Vec<_> = input
        .variants
        .into_iter()
        .map(|variant| {
            let attrs = VariantAttribute::from_attrs(variant.attrs.iter(), &mut errors);
            if attrs.is_default {
                if default_variant.is_some() {
                    errors.push(syn::Error::new_spanned(
                        &variant,
                        "\"default\" must be specified at most once",
                    ));
                }
                default_variant = Some(variant.ident.clone());
            }
            if !matches!(variant.fields, Fields::Unit) {
                errors.push(syn::Error::new_spanned(
                    &variant,
                    "Configuration only supports Unit variants",
                ));
            }
            Variant { variant, attrs }
        })
        .collect();

    let vis = &input.vis;
    let typ = &input.ident;
    let change_type = format_ident!("{}Change", input.ident);

    let derive_debug = if congen_attr.debug {
        quote! { #[derive(Debug)] }
    } else {
        quote! {}
    };
    let derive_clone = if congen_attr.clone {
        quote! { #[derive(Clone)] }
    } else {
        quote! {}
    };

    let has_default = default_variant.is_some();

    let enum_default = if let Some(default_variant) = default_variant.as_ref() {
        quote! {
            Ok(Self::#default_variant)
        }
    } else {
        quote! {
            Err(congen::internal::NotSupported)
        }
    };

    let cong_default_impl = if default_variant.is_some() {
        quote! {
            impl congen::internal::CongenDefault for #typ {
            }
        }
    } else {
        quote! {}
    };

    let variant_from_str_matches = variants.iter().map(|variant| {
        let ident = &variant.variant.ident;
        let as_str = ident.to_string();
        quote! {
            #as_str => Ok(Ok(Self::Apply(#typ::#ident)))
        }
    });

    let errors = errors.iter().map(|e| e.to_compile_error());
    quote! {

        #(#errors)*

        impl congen::Configuration for #typ {
        }
        impl congen::internal::CongenInternal for #typ {
            type CongenChange = #change_type;

            fn apply_change_with_inner_default(
                &mut self,
                change: Self::CongenChange,
                _inner_default: Option<fn() -> std::boxed::Box<dyn std::any::Any>>
            ) {
                if let #change_type::Apply(value) = change {
                    *self = value;
                }
            }

            fn description(field_name: &'static str) -> congen::internal::Description {
                congen::internal::FieldDescription {
                    field_name,
                    type_name: Self::type_name(),
                    is_flag: false,
                    allow_unset: false,
                    has_default: #has_default,
                    clap_data: None,
                }.into()
            }

            fn default() -> Result<Self, congen::internal::NotSupported> {
                #enum_default
            }
        }

        #cong_default_impl

        #[derive(Default)]
        #derive_debug
        #derive_clone
        #vis enum #change_type {
            Apply(#typ),
            #[default]
            NoChange
        }

        impl congen::internal::CongenChange for #change_type {
            type Configuration = #typ;

            fn empty() -> Self {
                Self::NoChange
            }

            fn parse(input: &std::ffi::OsStr)
                -> std::result::Result<std::result::Result<Self, congen::internal::ParseError>, congen::internal::NotSupported>
            {

                let Some(input) = input.to_str() else {
                    return Ok(Err(congen::internal::ParseError("expected utf-8 encoded value".to_string())));
                };
                match input {
                    #(#variant_from_str_matches,)*
                    _ => Ok(Err(congen::internal::ParseError(format!("found unknown variant: {input}")))),
                }
            }

            fn apply_change(&mut self, change: Self) {
                if let Self::Apply(new_change) = change {
                    *self = Self::Apply(new_change);
                }
            }

            fn from_path_and_verb<'a, P>(mut path: P, verb: congen::internal::ChangeVerb)
                -> std::result::Result<Self, congen::internal::VerbError>
            where
                P: std::iter::Iterator<Item = &'a str>,
            {
                assert!(path.next().is_none(), concat!(stringify!(#change_type), " implies this is a field"));
                match verb {
                    congen::internal::ChangeVerb::Set(value) => Ok(Self::parse(&value)??),
                    congen::internal::ChangeVerb::SetAny(value) => Ok(Self::Apply(
                        *value.downcast().map_err(|_| congen::internal::VerbError::DowncastFailed)?
                    )),
                    congen::internal::ChangeVerb::UseDefault =>
                        Ok(Self::Apply(<#typ as congen::internal::CongenInternal>::default()?)),
                    congen::internal::ChangeVerb::SetFlag
                    | congen::internal::ChangeVerb::Unset
                    | congen::internal::ChangeVerb::List(_) =>
                        Err(congen::internal::VerbError::UnsupportedVerb(verb)),
                }
            }
        }
    }
}

struct Variant {
    variant: syn::Variant,
    #[expect(unused)]
    attrs: VariantAttribute,
}

#[derive(Default)]
struct VariantAttribute {
    is_default: bool,
}

impl VariantAttribute {
    fn combine(&self, other: &Self) -> Self {
        Self {
            is_default: self.is_default || other.is_default,
        }
    }

    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(Self::default(), |a, b| a.combine(&b))
    }
}

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

        let mut is_default = false;

        for arg in args {
            match arg.ident().to_string().as_str() {
                "default" => {
                    if is_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",
                        ));
                    }
                    is_default = true;
                }
                _ => {
                    return Err(syn::Error::new_spanned(
                        arg,
                        "unknown argument to `congen` attribute",
                    ));
                }
            }
        }

        Ok(VariantAttribute { is_default })
    }
}