schnauzer-derive 0.1.0

Derives for schnauzer library
Documentation
use proc_macro::{self, TokenStream};
use quote::quote;
use syn::{parse_macro_input, token::Comma};

#[proc_macro_derive(AutoEnumFields)]
pub fn derive(input: TokenStream) -> TokenStream {
    let ast: syn::DeriveInput = parse_macro_input!(input);

    impl_auto_enum_fields(&ast).into()
}

fn impl_auto_enum_fields(ast: &syn::DeriveInput) -> proc_macro2::TokenStream {
    let id = &ast.ident;
    let fields = match &ast.data {
        syn::Data::Struct(data) => match &data.fields {
            syn::Fields::Named(f) => fields_token_stream(&f.named),
            syn::Fields::Unnamed(f) => fields_token_stream(&f.unnamed),
            syn::Fields::Unit => panic!("Unit structs is not supported by derive"),
        },
        syn::Data::Enum(data) => enum_cases_token_stream(id, data),
        _ => panic!("Fields enumeration derive can only implemented for structs and enums"),
    };

    let ident = &ast.ident;

    let output = quote! {
        impl AutoEnumFields for #ident {
            fn all_fields(&self) -> Vec<Field> {
                let mut v: Vec<Field> = Vec::new();
                #fields;
                v
            }
        }
    };

    output.into()
}

fn enum_cases_token_stream(
    ident: &syn::Ident,
    data_enum: &syn::DataEnum,
) -> proc_macro2::TokenStream {
    let matches: Vec<_> = data_enum
        .variants
        .iter()
        .map(|var| arm_tokens(var, ident))
        .collect();

    quote! {
        match self {
            #(#matches)*
        };
    }
}

fn arm_tokens(var: &syn::Variant, enum_id: &syn::Ident) -> proc_macro2::TokenStream {
    let var_id = &var.ident;
    let fields_tokens = match &var.fields {
        syn::Fields::Named(f) => case_fields_tokens(&f.named),
        syn::Fields::Unnamed(f) => case_fields_tokens(&f.unnamed),
        syn::Fields::Unit => Vec::new(),
    };

    if fields_tokens.len() == 0 {
        return quote! {
            #enum_id::#var_id => {

            }
        };
    }

    let inserts_tokens: Vec<_> = fields_tokens
        .iter()
        .map(|field| {
            quote! {
                {
                    let mut tmp = #field.all_fields();
                    v.append(&mut tmp);
                }
            }
        })
        .collect();

    quote! {
        #enum_id::#var_id(#(#fields_tokens),*) => {
            #(#inserts_tokens)*
        }
    }
}

fn case_fields_tokens(
    fields: &syn::punctuated::Punctuated<syn::Field, Comma>,
) -> Vec<proc_macro2::TokenStream> {
    fields
        .iter()
        .enumerate()
        .map(|(idx, field)| {
            let ident = &field.ident.as_ref().map(|i| quote! {#i}).unwrap_or({
                let new_name = format!("field{idx}");
                let new_id = proc_macro2::Ident::new(&new_name, proc_macro2::Span::call_site());
                quote! {#new_id}
            });
            quote!(#ident)
        })
        .collect()
}

fn fields_token_stream(
    fields: &syn::punctuated::Punctuated<syn::Field, Comma>,
) -> proc_macro2::TokenStream {
    let v: Vec<_> = fields
        .iter()
        .filter(|field| match &field.vis {
            syn::Visibility::Public(_) => true,
            _ => false,
        })
        .enumerate()
        .map(|(idx, field)| {
            let ident = &field.ident.as_ref().map(|i| quote! {#i}).unwrap_or({
                let t = proc_macro2::Literal::usize_unsuffixed(idx);
                quote! {#t}
            });
            let value = quote! {
                format!("{:?}", self.#ident)
            };
            quote! {
                v.push(Field::new(stringify!(#ident).to_string(), #value));
            }
        })
        .collect();

    quote! {
        #(#v)*;
    }
}