einstellung_derive 0.1.3

Proc-Macro crate for einstellung
Documentation
use super::parser::{ConfigFieldReceiver, ConfigStructReceiver};
use crate::derive_config::parser::{DefaultStrategy, MergeStrategy};
use darling::util::SpannedValue;
use syn::{GenericArgument, PathArguments, Type, parse_quote};

#[derive(Debug)]
pub struct TransformedStruct {
    pub complete_ident: syn::Ident,
    pub partial_ident: syn::Ident,
    pub any_freezable: bool,
    pub vis: syn::Visibility,
    pub fields: Vec<TransformedField>,
    pub attrs_partial: Vec<darling::ast::NestedMeta>,
    pub einstellung: syn::Path,
}

#[derive(Debug)]
pub enum FallbackStrategy {
    Require,
    Keep, // Used when the complete type is an Option
    Value(syn::Expr),
    Call(syn::Expr),
    Standard, // Default::default()
}

#[derive(Debug)]
pub enum FieldKind {
    Subconfig {
        complete_is_optional: bool,
    },
    Extend {
        partial_is_optional: bool,
        complete_is_optional: bool,
    },
    Replace {
        fallback: FallbackStrategy,
    },
    CustomMerge {
        func_path: SpannedValue<syn::Path>,
        fallback: FallbackStrategy,
    },
}

#[derive(Debug, PartialEq, Eq)]
pub enum FreezeStrategy {
    NotFreezable,
    Wrapped,
    IntrinsicallyFreezable,
}

#[derive(Debug)]
pub struct TransformedField {
    pub ident: syn::Ident,
    pub vis: syn::Visibility,
    pub complete_type: syn::Type,
    pub partial_type: syn::Type,
    pub kind: FieldKind,
    pub freeze: FreezeStrategy,
    pub validate_func: Option<syn::Expr>,
    pub attrs: Vec<syn::Attribute>,
}

pub fn transform(receiver: ConfigStructReceiver) -> syn::Result<TransformedStruct> {
    let complete_ident = receiver.ident.clone();
    let partial_ident = syn::Ident::new(&format!("{complete_ident}Partial"), complete_ident.span());
    let vis = receiver.vis;
    let einstellung = receiver.einstellung;

    let struct_data = receiver
        .data
        .take_struct()
        .expect("Only named structs supported");

    let mut fields = Vec::new();
    let mut errors: Option<syn::Error> = None;

    let mut any_freezable = receiver.freezable;

    for field in struct_data {
        any_freezable |= field.freezable;

        match transform_field(field, &einstellung, receiver.freezable) {
            Ok(f) => fields.push(f),
            Err(e) => {
                if let Some(ref mut errs) = errors {
                    errs.combine(e);
                } else {
                    errors = Some(e);
                }
            }
        }
    }

    if let Some(err) = errors {
        Err(err)
    } else {
        Ok(TransformedStruct {
            complete_ident,
            partial_ident,
            any_freezable,
            attrs_partial: receiver.partial.into_iter().flat_map(|x| x.0).collect(),
            vis,
            fields,
            einstellung,
        })
    }
}

fn transform_field(
    field: ConfigFieldReceiver,
    einstellung: &syn::Path,
    all_freezeable: bool,
) -> syn::Result<TransformedField> {
    let ident = field.ident.clone().ok_or_else(|| {
        syn::Error::new(proc_macro2::Span::call_site(), "Named fields are required")
    })?;

    let partial_attrs = field
        .partial
        .into_iter()
        .flat_map(|attr| attr.0)
        .map(|attr| syn::parse_quote! { #[#attr] });

    let serde_attrs = field
        .serde
        .into_iter()
        .map(|meta| syn::parse_quote! { #[#meta] });

    let attrs = partial_attrs.chain(serde_attrs).collect();

    let complete_type = field.ty;
    let inner_type_if_optional = extract_type_from_option(&complete_type);
    let complete_is_optional = inner_type_if_optional.is_some();
    let core_type = inner_type_if_optional
        .cloned()
        .unwrap_or_else(|| complete_type.clone());

    let (partial_type, kind) = if field.subconfig {
        if let Some(strategy) = field.merge {
            return Err(syn::Error::new(
                strategy.span(),
                "Merge strategy is invalid on a subconfig",
            ));
        }

        let partial_type = syn::parse_quote!(Option<<#core_type as #einstellung::Config>::Partial>);

        (
            partial_type,
            FieldKind::Subconfig {
                complete_is_optional,
            },
        )
    } else {
        let span = field
            .merge
            .as_ref()
            .map(|s| s.span())
            .unwrap_or_else(|| ident.span());
        let merge_strategy = match field.merge {
            Some(m) => m.into_inner(),
            None => MergeStrategy::Replace,
        };

        match merge_strategy {
            MergeStrategy::Extend => {
                let partial_is_optional =
                    complete_is_optional || field.default == DefaultStrategy::Required;

                let partial_type = if partial_is_optional {
                    syn::parse_quote!(Option<#core_type>)
                } else {
                    syn::parse_quote!(#core_type)
                };

                (
                    partial_type,
                    FieldKind::Extend {
                        partial_is_optional,
                        complete_is_optional,
                    },
                )
            }
            MergeStrategy::Replace => (
                syn::parse_quote!(Option<#core_type>),
                FieldKind::Replace {
                    fallback: determine_fallback(&field.default, complete_is_optional),
                },
            ),
            MergeStrategy::Function(s) => {
                let func_path = syn::parse_str::<syn::Path>(&s).map_err(|_| {
                    syn::Error::new(span, format!("Invalid function path: '{}'", &*s))
                })?;

                let func_path = SpannedValue::new(func_path, s.span());

                let partial_type = syn::parse_quote!(Option<#core_type>);

                (
                    partial_type,
                    FieldKind::CustomMerge {
                        func_path,
                        fallback: determine_fallback(&field.default, complete_is_optional),
                    },
                )
            }
        }
    };

    let freezable = field.freezable || all_freezeable;

    let freeze = if !freezable {
        FreezeStrategy::NotFreezable
    } else if field.subconfig {
        FreezeStrategy::IntrinsicallyFreezable
    } else {
        FreezeStrategy::Wrapped
    };

    let partial_type = if freeze == FreezeStrategy::Wrapped {
        parse_quote! { #einstellung::Freeze<#partial_type>}
    } else {
        partial_type
    };

    Ok(TransformedField {
        ident,
        vis: field.vis,
        kind,
        freeze,
        partial_type,
        complete_type,
        validate_func: field.validate,
        attrs,
    })
}

fn determine_fallback(default: &DefaultStrategy, is_optional: bool) -> FallbackStrategy {
    match default {
        DefaultStrategy::Required => {
            if is_optional {
                FallbackStrategy::Keep
            } else {
                FallbackStrategy::Require
            }
        }
        DefaultStrategy::Standard => FallbackStrategy::Standard,
        DefaultStrategy::Value(e) => FallbackStrategy::Value(e.clone()),
        DefaultStrategy::Call(e) => FallbackStrategy::Call(e.clone()),
    }
}

fn extract_type_from_option(ty: &Type) -> Option<&Type> {
    if let Type::Path(type_path) = ty
        && type_path.qself.is_none()
        && let Some(segment) = type_path.path.segments.last()
        && segment.ident == "Option"
        && let PathArguments::AngleBracketed(args) = &segment.arguments
        && let Some(GenericArgument::Type(inner_ty)) = args.args.first()
    {
        return Some(inner_ty);
    }
    None
}