moxy-derive 0.0.3

derive macros for moxy crate
Documentation
use proc_macro2::TokenStream;
use quote::{format_ident, quote};

use crate::{Render, core::Field, params};

#[derive(Clone, Default)]
pub struct StructSyntax;

impl Render for StructSyntax {
    type Args = params::StructParams;

    fn render(&self, args: Self::Args) -> syn::Result<TokenStream> {
        let ident = &args.input.ident;
        let build_ident = format_ident!("{}Builder", &ident);
        let vis = &args.input.vis;
        let generics = &args.input.generics;
        let (impl_generics, type_generics, where_generics) = generics.split_for_impl();
        let type_params: Vec<_> = generics.type_params().collect();
        let type_param_idents: Vec<_> = generics.type_params().map(|tp| &tp.ident).collect();
        let lifetime_params: Vec<_> = generics.lifetimes().collect();
        let all_fields: Vec<_> = args
            .data
            .fields
            .iter()
            .enumerate()
            .map(|(i, field)| Field::parse(i, field))
            .collect::<syn::Result<Vec<_>>>()?;

        let fields: Vec<_> = all_fields
            .into_iter()
            .filter(|field| field.attrs().exists("build"))
            .collect();

        let is_optional = |f: &&Field| {
            self.render_default_tokens(f).ok().flatten().is_some()
                || self.render_option_inner(f).is_some()
        };

        let required: Vec<_> = fields.iter().filter(|f| !is_optional(f)).collect();

        let optional: Vec<_> = fields
            .iter()
            .filter(|f| self.render_default_tokens(f).ok().flatten().is_some())
            .collect();

        let option_fields: Vec<_> = fields
            .iter()
            .filter(|f| {
                self.render_option_inner(f).is_some()
                    && self.render_default_tokens(f).ok().flatten().is_none()
            })
            .collect();

        let const_param_idents: Vec<_> = required
            .iter()
            .map(|f| format_ident!("{}", f.name().to_string().to_uppercase()))
            .collect();

        let builder_fields: Vec<_> = fields
            .iter()
            .map(|field| {
                let name = field.name();
                if self.render_option_inner(field).is_some()
                    && self.render_default_tokens(field).ok().flatten().is_none()
                {
                    let ty = field.ty();
                    quote!(#name: #ty)
                } else {
                    let ty = field.ty();
                    quote!(#name: ::std::option::Option<#ty>)
                }
            })
            .collect();

        let field_names: Vec<_> = fields.iter().map(|f| f.name()).collect();
        let init_fields: Vec<_> = fields
            .iter()
            .map(|f| {
                let name = f.name();
                quote!(#name: ::std::option::Option::None)
            })
            .collect();

        let const_param_defs: Vec<_> = const_param_idents
            .iter()
            .map(|id| quote!(const #id: bool = false))
            .collect();

        let const_param_refs: Vec<_> = const_param_idents.iter().map(|id| quote!(#id)).collect();
        let const_param_true: Vec<_> = const_param_idents.iter().map(|_| quote!(true)).collect();
        let builder_struct = quote! {
            #vis struct #build_ident <#(#lifetime_params,)* #(#type_params,)* #(#const_param_defs,)*> #where_generics {
                #(#builder_fields,)*
            }
        };

        let required_setters: Vec<TokenStream> = required
            .iter()
            .enumerate()
            .map(|(i, field)| -> syn::Result<TokenStream> {
                let fname = field.name();
                let ty = field.ty();
                let method_name = match self.render_custom_method_name(field)? {
                    Some(id) => quote!(#id),
                    None => quote!(#fname),
                };

                let other_const_params: Vec<_> = const_param_idents
                    .iter()
                    .enumerate()
                    .filter(|(j, _)| *j != i)
                    .map(|(_, id)| quote!(const #id: bool))
                    .collect();

                let impl_const_refs: Vec<_> = const_param_idents
                    .iter()
                    .enumerate()
                    .map(|(j, id)| if j == i { quote!(false) } else { quote!(#id) })
                    .collect();

                let ret_const_refs: Vec<_> = const_param_idents
                    .iter()
                    .enumerate()
                    .map(|(j, id)| if j == i { quote!(true) } else { quote!(#id) })
                    .collect();

                let move_fields: Vec<_> = field_names
                    .iter()
                    .map(|n| {
                        if n.to_string() == fname.to_string() {
                            quote!(#n: ::std::option::Option::Some(value.into()))
                        } else {
                            quote!(#n: self.#n)
                        }
                    })
                    .collect();

                Ok(quote! {
                    impl <#(#lifetime_params,)* #(#type_params,)* #(#other_const_params,)*> #build_ident <#(#lifetime_params,)* #(#type_param_idents,)* #(#impl_const_refs,)*> #where_generics {
                        pub fn #method_name<V: Into<#ty>>(self, value: V) -> #build_ident <#(#lifetime_params,)* #(#type_param_idents,)* #(#ret_const_refs,)*> {
                            #build_ident {
                                #(#move_fields,)*
                            }
                        }
                    }
                })
            })
            .collect::<syn::Result<Vec<_>>>()?;

        let optional_setters: Vec<TokenStream> = optional
            .iter()
            .map(|field| -> syn::Result<TokenStream> {
                let fname = field.name();
                let ty = field.ty();
                let method_name = match self.render_custom_method_name(field)? {
                    Some(id) => quote!(#id),
                    None => quote!(#fname),
                };

                Ok(quote! {
                    pub fn #method_name<V: Into<#ty>>(mut self, value: V) -> Self {
                        self.#fname = ::std::option::Option::Some(value.into());
                        self
                    }
                })
            })
            .collect::<syn::Result<Vec<_>>>()?;

        let option_setters: Vec<TokenStream> = option_fields
            .iter()
            .map(|field| -> syn::Result<TokenStream> {
                let fname = field.name();
                let inner_ty = self.render_option_inner(field).unwrap();
                let method_name = match self.render_custom_method_name(field)? {
                    Some(id) => quote!(#id),
                    None => quote!(#fname),
                };

                Ok(quote! {
                    pub fn #method_name<V: Into<#inner_ty>>(mut self, value: V) -> Self {
                        self.#fname = ::std::option::Option::Some(value.into());
                        self
                    }
                })
            })
            .collect::<syn::Result<Vec<_>>>()?;

        let all_const_generic_params: Vec<_> = const_param_idents
            .iter()
            .map(|id| quote!(const #id: bool))
            .collect();

        let all_optional_setters: Vec<&TokenStream> = optional_setters
            .iter()
            .chain(option_setters.iter())
            .collect();

        let optional_setters_block = if all_optional_setters.is_empty() {
            quote!()
        } else {
            quote! {
                impl <#(#lifetime_params,)* #(#type_params,)* #(#all_const_generic_params,)*> #build_ident <#(#lifetime_params,)* #(#type_param_idents,)* #(#const_param_refs,)*> #where_generics {
                    #(#all_optional_setters)*
                }
            }
        };

        let build_fields_assign: Vec<_> = fields
            .iter()
            .map(|field| -> syn::Result<TokenStream> {
                let fname = field.name();
                let default = self.render_default_tokens(field)?;
                let is_option = self.render_option_inner(field).is_some() && default.is_none();

                Ok(if is_option {
                    quote!(#fname: self.#fname)
                } else if let Some(default) = default {
                    quote!(#fname: self.#fname.unwrap_or_else(|| #default.into()))
                } else {
                    quote!(#fname: self.#fname.unwrap())
                })
            })
            .collect::<syn::Result<Vec<_>>>()?;

        let build_impl = quote! {
            impl <#(#lifetime_params,)* #(#type_params,)*> #build_ident <#(#lifetime_params,)* #(#type_param_idents,)* #(#const_param_true,)*> #where_generics {
                pub fn build(self) -> #ident #type_generics {
                    #ident {
                        #(#build_fields_assign,)*
                        ..::std::default::Default::default()
                    }
                }
            }
        };

        let new_impl = quote! {
            impl #impl_generics #ident #type_generics #where_generics {
                pub fn new() -> #build_ident <#(#lifetime_params,)* #(#type_param_idents,)*> {
                    #build_ident {
                        #(#init_fields,)*
                    }
                }
            }
        };

        Ok(quote! {
            #builder_struct
            #(#required_setters)*
            #optional_setters_block
            #build_impl
            #new_impl
        })
    }
}

impl StructSyntax {
    fn render_option_inner<'a>(&self, field: &'a Field) -> Option<&'a syn::Type> {
        let syn::Type::Path(type_path) = field.ty() else {
            return None;
        };

        let segment = type_path.path.segments.last()?;

        if segment.ident != "Option" {
            return None;
        }

        let syn::PathArguments::AngleBracketed(args) = &segment.arguments else {
            return None;
        };

        let syn::GenericArgument::Type(inner) = args.args.first()? else {
            return None;
        };

        Some(inner)
    }

    fn render_custom_method_name(&self, field: &Field) -> syn::Result<Option<proc_macro2::Ident>> {
        let build_args = field.attrs().get("build")?;
        Ok(build_args
            .iter()
            .find_map(|arg| arg.as_attr())
            .and_then(|attr| {
                attr.args().iter().find_map(|a| {
                    if !a.path().is_ident("__value") {
                        return None;
                    }

                    a.as_lit().and_then(|lit| match lit {
                        syn::Lit::Str(s) => Some(format_ident!("{}", s.value())),
                        _ => None,
                    })
                })
            }))
    }

    fn render_default_tokens(
        &self,
        field: &Field,
    ) -> syn::Result<Option<proc_macro2::TokenStream>> {
        let build_args = field.attrs().get("build")?;
        Ok(build_args
            .iter()
            .find_map(|arg| arg.as_attr())
            .and_then(|attr| {
                attr.args()
                    .iter()
                    .find(|a| a.path().is_ident("default"))
                    .and_then(|a| a.as_value_tokens())
            }))
    }
}