use darling::{FromDeriveInput, FromField, FromMeta, FromVariant, ast::{Data, Fields, Style}};
use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::{quote, ToTokens};
use syn::{parse_macro_input, Ident, Index, spanned::Spanned};
#[derive(Clone, Copy, Default, FromMeta)]
#[darling(default, rename_all = "snake_case")]
enum Masking {
Pan,
PanSuffix,
#[default]
All,
Hidden,
}
type OptionData = Data<VariantOptions, FieldOptions>;
#[derive(FromDeriveInput)]
#[darling(attributes(deboog))]
struct Options {
ident: Ident,
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.data);
let output = quote! { #debug_impl };
output.into()
}
fn debug_fmt_impl(ident: &Ident, data: &OptionData) -> TokenStream2 {
let debug_fmt = debug_fmt_body(ident, data);
quote! {
#[automatically_derived]
impl std::fmt::Debug for #ident {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
#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(masking) => {
let mask_method = Ident::new(match masking {
Masking::Pan => "mask_pan",
Masking::PanSuffix => "mask_pan_suffix",
Masking::All => "mask_all",
Masking::Hidden => "mask_hide",
}, field.span());
quote! { {
use deboog::field::DeboogField;
#field.#mask_method()
} }
}
}
}