deboog-derive 0.4.0

Procedural macros for deboog crate
Documentation
use darling::{
    ast::{Data, Fields, Style},
    FromDeriveInput, FromField, FromMeta, FromVariant,
};
use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::{quote, ToTokens};
use syn::{parse_macro_input, spanned::Spanned, Generics, Ident, Index};

#[derive(Clone, Copy, Default, FromMeta)]
#[darling(default, rename_all = "snake_case")]
enum Masking {
    #[default]
    All,
    Pan,
    PanSuffix,
    Hidden,
}

type OptionData = Data<VariantOptions, FieldOptions>;

#[derive(FromDeriveInput)]
#[darling(attributes(deboog))]
struct Options {
    ident: Ident,
    generics: Generics,
    data: OptionData,
}

#[derive(FromField)]
#[darling(attributes(deboog))]
struct FieldOptions {
    ident: Option<Ident>,
    #[darling(default)]
    skip: bool,
    #[darling(default)]
    mask: Option<Masking>,
}

#[derive(FromVariant)]
#[darling(attributes(deboog))]
struct VariantOptions {
    ident: Ident,
    fields: Fields<FieldOptions>,
}

#[proc_macro_derive(Deboog, attributes(deboog))]
pub fn derive_deboog(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input);
    let opts = Options::from_derive_input(&input).unwrap();
    let debug_impl = debug_fmt_impl(&opts.ident, &opts.generics, &opts.data);

    let output = quote! { #debug_impl };
    output.into()
}

fn debug_fmt_impl(ident: &Ident, generics: &Generics, data: &OptionData) -> TokenStream2 {
    let debug_fmt = debug_fmt_body(ident, data);
    quote! {
        #[automatically_derived]
        impl #generics std::fmt::Debug for #ident #generics {
            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
                #debug_fmt
            }
        }
    }
}

fn debug_fmt_body(ident: &Ident, data: &OptionData) -> TokenStream2 {
    match data {
        Data::Enum(variants) => debug_fmt_enum(variants),
        Data::Struct(fields) => match fields.style {
            Style::Unit => debug_fmt_unit_struct(ident),
            Style::Struct => debug_fmt_normal_struct(ident, &fields.fields),
            Style::Tuple => debug_fmt_tuple_struct(ident, &fields.fields),
        },
    }
}

fn debug_fmt_unit_struct(ident: &Ident) -> TokenStream2 {
    let ident_str = ident.to_string();
    quote! {
        f.debug_struct(#ident_str).finish()
    }
}

fn debug_fmt_normal_struct(ident: &Ident, fields: &[FieldOptions]) -> TokenStream2 {
    let ident_str = ident.to_string();
    let field_chunks = fields.iter().filter(|f| !f.skip).map(|f| {
        let field = &f.ident;
        let field_str = field.to_token_stream().to_string();
        let field_val = transform_field(quote! { &self.#field }, f);
        quote! { .field(#field_str, #field_val) }
    });
    quote! {
        f.debug_struct(#ident_str)
            #(#field_chunks)*
            .finish()
    }
}

fn debug_fmt_tuple_struct(ident: &Ident, fields: &[FieldOptions]) -> TokenStream2 {
    let ident_str = ident.to_string();
    let field_chunks = fields
        .iter()
        .enumerate()
        .filter(|(_, f)| !f.skip)
        .map(|(i, f)| {
            let i = Index::from(i);
            let field_val = transform_field(quote! { &self.#i }, f);
            quote! { .field(#field_val) }
        });
    quote! {
        f.debug_tuple(#ident_str)
            #(#field_chunks)*
            .finish()
    }
}

fn debug_fmt_enum(variants: &[VariantOptions]) -> TokenStream2 {
    let variant_chunks = variants.iter().map(|v| {
        let var = &v.ident;
        let var_str = var.to_string();

        if v.fields.is_unit() {
            quote! {
                Self::#var => {
                    f.debug_struct(#var_str).finish()
                }
            }
        } else if v.fields.is_tuple() {
            let fields = v.fields.iter().enumerate().map(|(i, f)| {
                if f.skip {
                    Ident::new("_", v.ident.span())
                } else {
                    Ident::new(&format!("f{}", i), v.ident.span())
                }
            });
            let field_chunks = v
                .fields
                .iter()
                .enumerate()
                .filter(|(_, f)| !f.skip)
                .map(|(i, f)| Ident::new(&format!("f{}", i), f.ident.span()));
            quote! {
                Self::#var(#(#fields),*) => {
                    f.debug_tuple(#var_str)
                        #(.field(#field_chunks))*
                        .finish()
                }
            }
        } else {
            let fields = v.fields.iter().filter(|f| !f.skip).map(|f| &f.ident);
            let variant_fields = fields.clone().map(|i| {
                let field_str = i.to_token_stream().to_string();
                quote! { .field(#field_str, #i) }
            });
            quote! {
                Self::#var { #(#fields,)* .. } => {
                    f.debug_struct(#var_str)
                        #(#variant_fields)*
                        .finish()
                }
            }
        }
    });
    quote! {
        match self {
            #(#variant_chunks),*
        }
    }
}

fn transform_field(field: TokenStream2, opts: &FieldOptions) -> TokenStream2 {
    match opts.mask {
        None => field,
        Some(mask_type) => match mask_type {
            Masking::All => quote! { &deboog::field::Masked::All(#field) },
            Masking::Pan => quote! { &deboog::field::Masked::Pan(#field) },
            Masking::PanSuffix => quote! { &deboog::field::Masked::PanSuffix(#field) },
            Masking::Hidden => quote! { &deboog::field::Masked::Hidden(#field) },
        },
    }
}