einstellung_derive 0.1.3

Proc-Macro crate for einstellung
Documentation
use crate::derive_config::transformer::{FallbackStrategy, FieldKind, FreezeStrategy, TransformedField, TransformedStruct};
use proc_macro2::TokenStream;
use quote::{ToTokens, quote, quote_spanned};
use std::fmt::Write;
use syn::{ parse_quote_spanned, spanned::Spanned};

impl ToTokens for TransformedStruct {
    fn to_tokens(&self, tokens: &mut TokenStream) {
        generate_partial_struct(self).to_tokens(tokens);
        generate_partial_impl(self).to_tokens(tokens);
        generate_config_impl(self).to_tokens(tokens);
    }
}

fn path_to_litstr(path: &syn::Path) -> syn::LitStr {
    let mut s = String::new();
    let mut iter = path.segments.iter();

    if path.leading_colon.is_some() {
        s += "::";
    }

    if let Some(first) = iter.next() {
        write!(&mut s, "{}", first.ident).unwrap();
        for seg in iter {
            write!(&mut s, "::{}", seg.ident).unwrap();
        }
    }

    syn::LitStr::new(&s, path.span())
}

fn generate_partial_struct(model: &TransformedStruct) -> TokenStream {
    let partial_ident = &model.partial_ident;
    let vis = &model.vis;
    let einstellung = &model.einstellung;
    let attrs = &model.attrs_partial;

    let fields = model.fields.iter().map(|f| {
        let ident = &f.ident;
        let attrs = &f.attrs;
        let f_vis = &f.vis;
        let partial_type = &f.partial_type;

        let needs_serde_default = if let FieldKind::Extend { complete_is_optional, .. } = &f.kind {
            !complete_is_optional
        } else {
            false
        };

        let default_attr = if needs_serde_default {
            quote! { #[serde(default)] }
        } else {
            quote! {}
        };

        quote_spanned! { partial_type.span() =>
            #default_attr
            #(#attrs)*
            #f_vis #ident: #partial_type
        }
    });

    let serde: syn::Path = parse_quote_spanned!(einstellung.span() => #einstellung::serde);
    let serde_lit = path_to_litstr(&serde);

    quote! {
        #[derive(::core::default::Default, #einstellung::serde::Deserialize)]
        #(#[#attrs])*
        #[serde(crate = #serde_lit)]
        #vis struct #partial_ident {
            #(#fields,)*
        }
    }
}

fn generate_field_merge(f: &TransformedField, einstellung: &syn::Path, complete_str: &str, left: TokenStream, right: TokenStream) -> TokenStream {

    let partial_type = &f.partial_type;

    match &f.kind {
        FieldKind::Replace { .. } => quote! { #right.or(#left) },
        FieldKind::Extend {
            partial_is_optional,
            ..
        } => {
            if *partial_is_optional {
                quote! {
                    match (#left, #right) {
                        (Some(mut a), Some(b)) => {
                            ::core::iter::Extend::extend(&mut a, b);
                            Some(a)
                        },
                        (a, b) => a.or(b)
                    }
                }
            } else {
                quote! {
                    {
                        let mut tmp = #left;
                        tmp.extend(#right);
                        tmp
                    }
                }
            }
        }
        FieldKind::CustomMerge { func_path, .. } => {

            let span = func_path.span();
            let func_path: &syn::Path = func_path;

            let ident_str = f.ident.to_string();

            quote_spanned! { span =>
                {
                    let _: #einstellung::MergeFunction<#partial_type> = #func_path;
                    #func_path(#left, #right).map_err(|reason| #einstellung::ConfigError::CustomMerge {
                        field: #einstellung::FieldPath::new(#complete_str, #ident_str),
                        reason,
                    })
                }
            }
        },
        FieldKind::Subconfig { .. } => quote! {
            match (#left, #right) {
                (Some(a), Some(b)) => Some(#einstellung::PartialConfig::merge(a, b)?),
                (a, b) => a.or(b)
            }
        },
    }
}

fn generate_partial_impl(model: &TransformedStruct) -> TokenStream {
    let partial_ident = &model.partial_ident;
    let complete_ident = &model.complete_ident;
    let einstellung = &model.einstellung;
    let complete_str = complete_ident.to_string();

    let merge_fields = model.fields.iter().map(|f| {
        let ident = &f.ident;

        let merged = match f.freeze {
            FreezeStrategy::NotFreezable => {
                generate_field_merge(f, einstellung, &complete_str, quote!(self.#ident), quote!(next.#ident))
            }
            FreezeStrategy::Wrapped => {
                let ident_str = ident.to_string();
                let merge = generate_field_merge(f, einstellung, &complete_str, quote!(left), quote!(right));

                quote!{
                    match #einstellung::FreezeCombination::of_freeze(self.#ident, next.#ident) {
                        #einstellung::FreezeCombination::BothFree(left, right) => #einstellung::Freeze::Free(#merge),
                        #einstellung::FreezeCombination::OneFrozen(x) => #einstellung::Freeze::Frozen(x),
                        #einstellung::FreezeCombination::BothFrozen => return ::core::result::Result::Err(#einstellung::ConfigError::FreezeCollision(#einstellung::FieldPath::new(#complete_str, #ident_str))),
                    }
                }
            },
            FreezeStrategy::IntrinsicallyFreezable => {
                let ident_str = ident.to_string();
                let merge = generate_field_merge(f, einstellung, &complete_str, quote!(left), quote!(right));
                
                quote!{
                    match #einstellung::FreezeCombination::of(self.#ident, next.#ident) {
                        #einstellung::FreezeCombination::BothFree(left, right) => #merge,
                        #einstellung::FreezeCombination::OneFrozen(x) => x,
                        #einstellung::FreezeCombination::BothFrozen => return ::core::result::Result::Err(#einstellung::ConfigError::FreezeCollision(#einstellung::FieldPath::new(#complete_str, #ident_str))),
                    }
                }
            },
        };

        quote! { #ident: #merged }
    });

    let build_fields = model.fields.iter().map(|f| {
        let ident = &f.ident;
        let ident_str = ident.to_string();
        let complete_type = &f.complete_type;

        let unfreeze = if f.freeze == FreezeStrategy::Wrapped {
            quote! { #einstellung::Freeze::into_inner(self.#ident) }
        } else {
            quote! { self.#ident }
        };

        let resolve = match &f.kind {
            FieldKind::Subconfig { complete_is_optional, .. } => {
                if *complete_is_optional {
                    quote! { #unfreeze.map(|x| #einstellung::build_with_context(x, #complete_str, #ident_str)).transpose()? }
                } else {
                    quote! { #einstellung::build_with_context(#unfreeze.unwrap_or_default(), #complete_str, #ident_str)? }
                }
            }
            FieldKind::Extend { partial_is_optional, complete_is_optional, .. } => {
                if *partial_is_optional && !*complete_is_optional {
                    quote! { #unfreeze.ok_or(#einstellung::ConfigError::MissingField(#einstellung::FieldPath::new(#complete_str, #ident_str)))? }
                } else {
                    quote! { #unfreeze }
                }
            }
            FieldKind::Replace { fallback, .. } | FieldKind::CustomMerge { fallback, .. } => match fallback {
                FallbackStrategy::Require => quote! { #unfreeze.ok_or(#einstellung::ConfigError::MissingField(#einstellung::FieldPath::new(#complete_str, #ident_str)))? },
                FallbackStrategy::Keep => quote! { #unfreeze },
                FallbackStrategy::Value(value) => quote! { #unfreeze.unwrap_or(#value) },
                FallbackStrategy::Call(value) => quote! { #unfreeze.unwrap_or_else(#value) },
                FallbackStrategy::Standard => quote! { #unfreeze.unwrap_or_else(::core::default::Default::default) },
            }
        };


        if let Some(validate_func) = &f.validate_func {
            quote_spanned! { complete_type.span() => 

                let #ident: #complete_type = #resolve;
                let _: #einstellung::ValidationFunction<#complete_type, _> = #validate_func;
                if let Err(e) = (#validate_func)(&#ident) {
                    return Err(#einstellung::ConfigError::Validation {
                        field: #einstellung::FieldPath::new(#complete_str, #ident_str),
                        #[allow(clippy::useless_conversion)]
                        reason: e.into(),
                    });
                }
            }
        } else {
            quote_spanned! { complete_type.span() => let #ident = #resolve; }
        }
    });


    let freeze_fields = model.fields.iter().map(|f| {
        let ident = &f.ident;

        match f.freeze {
            FreezeStrategy::NotFreezable => quote! {
                #ident: self.#ident
            },
            FreezeStrategy::IntrinsicallyFreezable | FreezeStrategy::Wrapped => quote! {
                #ident: #einstellung::Freezable::freeze(self.#ident)
            },
        }
    });

    let is_field_frozen = model.fields.iter().filter_map(|f| {
        let ident = &f.ident;

        match f.freeze {
            FreezeStrategy::NotFreezable => None,
            FreezeStrategy::IntrinsicallyFreezable | FreezeStrategy::Wrapped => Some(quote! {
                #einstellung::Freezable::is_frozen(&self.#ident)
            }),
        }
    });

    let freezable_impl = if model.any_freezable {
        quote! {
            #[automatically_derived]
            impl #einstellung::Freezable for #partial_ident {
                fn freeze(self) -> Self {
                    Self { #(#freeze_fields,)* }
                }
                fn is_frozen(&self) -> bool {
                    #(#is_field_frozen)||*
                }
            }
        }
    } else {
        quote! {}
    };

    let field_names = model.fields.iter().map(|f| &f.ident);

    quote_spanned! { partial_ident.span() =>
        #[automatically_derived]
        impl #einstellung::PartialConfig for #partial_ident {
            type Complete = #complete_ident;

            fn merge(self, next: Self) -> ::core::result::Result<Self, #einstellung::ConfigError> {
                ::core::result::Result::Ok(Self { #(#merge_fields,)* })
            }

            fn build(self) -> ::core::result::Result<Self::Complete, #einstellung::ConfigError> {
                #(#build_fields)*
                ::core::result::Result::Ok(#complete_ident { #(#field_names,)* })
            }
        }

        #freezable_impl
    }
}

fn generate_config_impl(model: &TransformedStruct) -> TokenStream {
    let complete_ident = &model.complete_ident;
    let partial_ident = &model.partial_ident;
    let einstellung = &model.einstellung;

    quote_spanned! { complete_ident.span() =>
        #[automatically_derived]
        impl #einstellung::Config for #complete_ident {
            type Partial = #partial_ident;
        }
    }
}