use darling::FromAttributes;
use proc_macro2::TokenStream as TokenStream2;
use proc_macro_error::abort;
use quote::quote;
use syn::{Data, DataStruct, DeriveInput, FieldsNamed};
#[derive(Debug, Default, FromAttributes)]
#[darling(attributes(better_debug))]
struct FieldOptions {
ignore: Option<bool>,
rename_to: Option<String>,
secret: Option<bool>,
cust_formatter: Option<String>,
cust_formatter_skip_if_none: Option<bool>,
}
impl FieldOptions {
fn is_invalid(&self) -> bool {
if self.ignore.unwrap_or(false)
&& (self.rename_to.is_some()
|| self.secret.unwrap_or(false)
|| self.cust_formatter.is_some()
|| self.cust_formatter_skip_if_none.unwrap_or(false))
{
return true;
}
if self.cust_formatter.is_none() && self.cust_formatter_skip_if_none.unwrap_or(false) {
return true;
}
if self.secret.unwrap_or(false) && self.cust_formatter.is_some() {
return true;
}
false
}
}
pub(crate) fn expand(ast: DeriveInput) -> syn::Result<TokenStream2> {
let iden = &ast.ident;
let fields = if let Data::Struct(DataStruct {
fields: syn::Fields::Named(FieldsNamed { ref named, .. }),
..
}) = ast.data
{
named
} else {
abort!(iden, "BetterDebug only works with structs");
};
let mut out = vec![];
for field in fields {
let field_attributes = FieldOptions::from_attributes(&field.attrs)?;
if field_attributes.is_invalid() {
abort!(
field.ident,
"Selected options aren't compatible with each other."
);
}
if field_attributes.ignore.unwrap_or(false) {
continue;
}
let field_ident = match &field.ident {
Some(ident) => ident,
None => abort!(field, "Field must have an identifier."),
};
let field_name = match field_attributes.rename_to {
Some(name) => name,
None => field_ident.to_string(),
};
if let Some(func) = field_attributes.cust_formatter {
let expr = syn::parse_str::<syn::Expr>(&func)?;
match field_attributes
.cust_formatter_skip_if_none
.unwrap_or(false)
{
true => out.push(quote! {
if let Some(out) = #expr(&self){
dbg_struct.field(#field_name, &out);
}
}),
false => out.push(quote! {
if let Some(out) = #expr(&self){
dbg_struct.field(#field_name, &out);
} else {
dbg_struct.field(#field_name, &self.#field_ident);
}
}),
}
} else if field_attributes.secret.unwrap_or(false) {
out.push(quote! {
dbg_struct.field(#field_name, &"<SECRET>");
});
} else {
out.push(quote! {
dbg_struct.field(#field_name, &self.#field_ident);
});
}
}
let ident_name = iden.to_string();
let expanded = quote! {
impl core::fmt::Debug for #iden {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
let mut dbg_struct = f.debug_struct(#ident_name);
#(#out)*
dbg_struct.finish()
}
}
};
Ok(expanded)
}
#[cfg(test)]
mod tests {
use super::*;
use syn::parse_quote;
#[test]
fn test_expand() {
let input = parse_quote! {
#[derive(BetterDebug)]
struct Foo {
bar: String,
baz: String,
}
};
let expected = quote! {
impl core::fmt::Debug for Foo {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
let mut dbg_struct = f.debug_struct("Foo");
dbg_struct.field("bar", &self.bar);
dbg_struct.field("baz", &self.baz);
dbg_struct.finish()
}
}
};
let expanded = expand(input).unwrap();
assert_eq!(expanded.to_string(), expected.to_string());
}
#[test]
fn test_expand_rename() {
let input = parse_quote! {
#[derive(BetterDebug)]
struct Foo {
#[better_debug(rename_to = "new_name")]
bar: String,
baz: String,
}
};
let expected = quote! {
impl core::fmt::Debug for Foo {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
let mut dbg_struct = f.debug_struct("Foo");
dbg_struct.field("new_name", &self.bar);
dbg_struct.field("baz", &self.baz);
dbg_struct.finish()
}
}
};
let expanded = expand(input).unwrap();
assert_eq!(expanded.to_string(), expected.to_string());
}
#[test]
fn test_expand_rename_secret() {
let input = parse_quote! {
#[derive(BetterDebug)]
struct Foo {
#[better_debug(rename_to = "new_name", secret)]
bar: String,
baz: String,
}
};
let expected = quote! {
impl core::fmt::Debug for Foo {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
let mut dbg_struct = f.debug_struct("Foo");
dbg_struct.field("new_name", &"<SECRET>");
dbg_struct.field("baz", &self.baz);
dbg_struct.finish()
}
}
};
let expanded = expand(input).unwrap();
assert_eq!(expanded.to_string(), expected.to_string());
}
#[test]
fn test_expand_secret() {
let input = parse_quote! {
#[derive(BetterDebug)]
struct Foo {
#[better_debug(secret)]
bar: String,
baz: String,
}
};
let expected = quote! {
impl core::fmt::Debug for Foo {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
let mut dbg_struct = f.debug_struct("Foo");
dbg_struct.field("bar", &"<SECRET>");
dbg_struct.field("baz", &self.baz);
dbg_struct.finish()
}
}
};
let expanded = expand(input).unwrap();
assert_eq!(expanded.to_string(), expected.to_string());
}
#[test]
fn test_expand_cust_formatter() {
let input = parse_quote! {
#[derive(BetterDebug)]
struct Foo {
#[better_debug(cust_formatter = "foo")]
bar: String,
baz: String,
}
};
let expected = quote! {
impl core::fmt::Debug for Foo {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
let mut dbg_struct = f.debug_struct("Foo");
if let Some(out) = foo(&self){
dbg_struct.field("bar", &out);
} else {
dbg_struct.field("bar", &self.bar);
}
dbg_struct.field("baz", &self.baz);
dbg_struct.finish()
}
}
};
let expanded = expand(input).unwrap();
assert_eq!(expanded.to_string(), expected.to_string());
}
#[test]
fn test_expand_cust_formatter_skip_if_none() {
let input = parse_quote! {
#[derive(BetterDebug)]
struct Foo {
#[better_debug(cust_formatter = "foo", cust_formatter_skip_if_none)]
bar: String,
baz: String,
}
};
let expected = quote! {
impl core::fmt::Debug for Foo {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
let mut dbg_struct = f.debug_struct("Foo");
if let Some(out) = foo(&self){
dbg_struct.field("bar", &out);
}
dbg_struct.field("baz", &self.baz);
dbg_struct.finish()
}
}
};
let expanded = expand(input).unwrap();
assert_eq!(expanded.to_string(), expected.to_string());
}
#[test]
fn test_expand_ignore() {
let input = parse_quote! {
#[derive(BetterDebug)]
struct Foo {
#[better_debug(ignore)]
bar: String,
baz: String,
}
};
let expected = quote! {
impl core::fmt::Debug for Foo {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
let mut dbg_struct = f.debug_struct("Foo");
dbg_struct.field("baz", &self.baz);
dbg_struct.finish()
}
}
};
let expanded = expand(input).unwrap();
assert_eq!(expanded.to_string(), expected.to_string());
}
#[test]
#[should_panic]
fn test_invalid_ignore_cust_formatter() {
let input = parse_quote! {
#[derive(BetterDebug)]
struct Foo {
#[better_debug(ignore, cust_formatter = "foo")]
bar: String,
baz: String,
}
};
expand(input).unwrap();
}
#[test]
#[should_panic]
fn test_invalid_ignore_rename_to() {
let input = parse_quote! {
#[derive(BetterDebug)]
struct Foo {
#[better_debug(ignore, rename_to = "foo")]
bar: String,
baz: String,
}
};
expand(input).unwrap();
}
#[test]
#[should_panic]
fn test_invalid_secret() {
let input = parse_quote! {
#[derive(BetterDebug)]
struct Foo {
#[better_debug(ignore, secret)]
bar: String,
baz: String,
}
};
expand(input).unwrap();
}
#[test]
#[should_panic]
fn test_invalid_secret_cust_formatter() {
let input = parse_quote! {
#[derive(BetterDebug)]
struct Foo {
#[better_debug(secret, cust_formatter = "foo")]
bar: String,
baz: String,
}
};
expand(input).unwrap();
}
#[test]
#[should_panic]
fn test_invalid_cust_formatter_option() {
let input = parse_quote! {
#[derive(BetterDebug)]
struct Foo {
#[better_debug(cust_formatter_skip_if_none)]
bar: String,
baz: String,
}
};
expand(input).unwrap();
}
}