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 (impl_generics, type_generics, where_generics) = &args.input.generics.split_for_impl();
        let fields: Vec<_> = args
            .data
            .fields
            .iter()
            .enumerate()
            .map(|(i, field)| Field::parse(i, field))
            .collect::<syn::Result<Vec<_>>>()?;

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

        let methods: Vec<TokenStream> = fields
            .iter()
            .map(|field| -> syn::Result<TokenStream> {
                let fname = field.name();
                let ty = field.ty();
                let docs = self.render_docs(field);
                let method_name = match self.render_custom_method_name(field)? {
                    Some(id) => id,
                    None => format_ident!("{}", fname.to_string()),
                };
                let on_callback = self.render_on_callback(field)?;
                let is_option = self.render_option_inner(field).is_some();
                let is_bool = self.render_is_bool(field);
                let has_copy = self.render_has_modifier(field, "copy")?;
                let has_clone = self.render_has_modifier(field, "clone")?;
                let has_mut = self.render_has_modifier(field, "mutable")?;

                let callback = on_callback
                    .as_ref()
                    .map(|expr| quote!(#expr;))
                    .unwrap_or_default();

                let getter = if is_option {
                    let inner_ty = self.render_option_inner(field).unwrap();
                    quote! {
                        #(#docs)*
                        pub fn #method_name(&self) -> ::std::option::Option<&<#inner_ty as ::std::ops::Deref>::Target> {
                            #callback
                            self.#fname.as_deref()
                        }
                    }
                } else if is_bool || has_copy {
                    quote! {
                        #(#docs)*
                        pub fn #method_name(&self) -> #ty {
                            #callback
                            self.#fname
                        }
                    }
                } else if has_clone {
                    quote! {
                        #(#docs)*
                        pub fn #method_name(&self) -> #ty {
                            #callback
                            self.#fname.clone()
                        }
                    }
                } else {
                    quote! {
                        #(#docs)*
                        pub fn #method_name(&self) -> &<#ty as ::std::ops::Deref>::Target {
                            #callback
                            &self.#fname
                        }
                    }
                };

                let mut_getter = if has_mut {
                    let mut_name = format_ident!("{}_mut", fname.to_string());
                    quote! {
                        pub fn #mut_name(&mut self) -> &mut #ty {
                            &mut self.#fname
                        }
                    }
                } else {
                    quote!()
                };

                Ok(quote! {
                    #getter
                    #mut_getter
                })
            })
            .collect::<syn::Result<Vec<_>>>()?;

        Ok(quote! {
            impl #impl_generics #ident #type_generics #where_generics {
                #(#methods)*
            }
        })
    }
}

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_is_bool(&self, field: &Field) -> bool {
        matches!(field.ty(), syn::Type::Path(p) if p.path.is_ident("bool"))
    }

    fn render_has_modifier(&self, field: &Field, name: &str) -> syn::Result<bool> {
        let get_args = field.attrs().get("get")?;
        Ok(get_args.iter().any(|arg| {
            arg.as_attr()
                .is_some_and(|attr| attr.args().iter().any(|a| a.path().is_ident(name)))
        }))
    }

    fn render_custom_method_name(&self, field: &Field) -> syn::Result<Option<proc_macro2::Ident>> {
        let get_args = field.attrs().get("get")?;
        Ok(get_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_on_callback(&self, field: &Field) -> syn::Result<Option<proc_macro2::TokenStream>> {
        let get_args = field.attrs().get("get")?;
        Ok(get_args
            .iter()
            .find_map(|arg| arg.as_attr())
            .and_then(|attr| {
                attr.args()
                    .iter()
                    .find(|a| a.path().is_ident("on"))
                    .and_then(|a| a.as_value_tokens())
            }))
    }

    fn render_docs<'a>(&self, field: &'a Field) -> Vec<&'a syn::Attribute> {
        field
            .raw_attrs()
            .iter()
            .filter(|a| a.path().is_ident("doc"))
            .collect()
    }
}