use darling::FromMeta;
use proc_macro2::TokenStream;
use quote::quote;
use syn::{Fields, Result};
use synstructure::{decl_derive, AddBounds, BindingInfo, Structure, VariantInfo};
use crate::field_attributes::{DebugFormat, FieldAttributes, SkipMode};
use crate::result_into_stream_ext::ResultIntoStreamExt;
use crate::retain_ext::RetainExt;
mod field_attributes;
mod result_into_stream_ext;
mod retain_ext;
#[cfg(test)]
mod tests;
decl_derive!([Debug, attributes(debug)] => custom_debug_derive);
fn custom_debug_derive(mut structure: Structure) -> Result<TokenStream> {
filter_out_skipped_fields(&mut structure)?;
structure.add_bounds(AddBounds::Fields);
let match_arms =
structure.each_variant(|variant| generate_match_arm_body(variant).into_stream());
Ok(structure.gen_impl(quote! {
gen impl ::core::fmt::Debug for @Self {
fn fmt(&self, fmt: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
match self {
#match_arms
}
}
}
}))
}
fn filter_out_skipped_fields(structure: &mut Structure) -> Result<()> {
structure.try_retain(|binding| {
let field_attributes = parse_field_attributes(binding)?;
Ok(field_attributes.skip_mode != SkipMode::Always)
})?;
Ok(())
}
fn generate_match_arm_body(variant: &VariantInfo) -> Result<TokenStream> {
let name = variant.ast().ident.to_string();
let debug_builder = match variant.ast().fields {
Fields::Named(_) | Fields::Unit => quote! { debug_struct },
Fields::Unnamed(_) => quote! { debug_tuple },
};
let mut debug_builder_calls = Vec::new();
for binding in variant.bindings() {
let field_attributes = parse_field_attributes(binding)?;
let debug_builder_call = match &field_attributes.skip_mode {
SkipMode::Default => generate_debug_builder_call(binding, &field_attributes)?,
SkipMode::Condition(condition) => {
let debug_builder_call = generate_debug_builder_call(binding, &field_attributes)?;
quote! {
if (!#condition(#binding)) {
#debug_builder_call
}
}
}
SkipMode::Always => quote! {},
};
debug_builder_calls.push(debug_builder_call);
}
Ok(quote! {
let mut debug_builder = fmt.#debug_builder(#name);
#(#debug_builder_calls)*
debug_builder.finish()
})
}
fn generate_debug_builder_call(
binding: &BindingInfo,
field_attributes: &FieldAttributes,
) -> Result<TokenStream> {
let format = generate_debug_impl(binding, &field_attributes.debug_format);
let debug_builder_call =
if let Some(ref name) = binding.ast().ident.as_ref().map(<_>::to_string) {
quote! {
debug_builder.field(#name, #format);
}
} else {
quote! {
debug_builder.field(#format);
}
};
Ok(debug_builder_call)
}
fn generate_debug_impl(binding: &BindingInfo, debug_format: &DebugFormat) -> TokenStream {
match debug_format {
DebugFormat::Default => quote! { #binding },
DebugFormat::Format(format) => quote! { &format_args!(#format, #binding) },
DebugFormat::With(with) => quote! {
{
struct DebugWith<'a, T: 'a> {
data: &'a T,
fmt: fn(&T, &mut ::core::fmt::Formatter) -> ::core::fmt::Result,
}
impl<'a, T: 'a> ::core::fmt::Debug for DebugWith<'a, T> {
fn fmt(&self, fmt: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
(self.fmt)(self.data, fmt)
}
}
&DebugWith {
data: #binding,
fmt: #with,
}
}
},
}
}
fn parse_field_attributes(binding: &BindingInfo<'_>) -> Result<FieldAttributes> {
let mut combined_field_attributes = FieldAttributes::default();
for attr in &binding.ast().attrs {
if !attr.path().is_ident("debug") {
continue;
}
let field_attributes = FieldAttributes::from_meta(&attr.meta)?;
combined_field_attributes = combined_field_attributes.try_combine(field_attributes)?;
}
Ok(combined_field_attributes)
}