use crate::{
errors::Result,
formatting_data::{
FieldFormattingData, FormattingData, FormattingDataSource, VariantFormattingData,
},
implementation::FmtBodySource,
};
use proc_macro2::{Ident, TokenStream};
use quote::{format_ident, quote};
use std::{iter::Enumerate, slice::Iter};
use syn::{parse_str, Index, Path};
impl<T: FormattingDataSource> FmtBodySource for T {
fn generate_fmt_body(&self, ident: &str) -> Result<TokenStream> {
let formatting_data = self.to_formatting_data()?;
let fmt_body = match formatting_data {
FormattingData::StructData(fields) => generate_struct_fmt_body(ident, &fields),
FormattingData::EnumData(variants) => generate_enum_fmt_body(ident, &variants),
};
Ok(fmt_body)
}
}
fn generate_struct_fmt_body(ident: &str, fields: &[FieldFormattingData]) -> TokenStream {
if fields.is_empty() {
quote! { write!(f, #ident) }
} else {
generate_write_invocation(
ident,
&fields.iter().enumerate(),
|idx: usize, ident: std::option::Option<proc_macro2::Ident>| {
generate_struct_accessor(idx, &ident)
},
)
}
}
fn generate_enum_fmt_body(ident: &str, variants: &[VariantFormattingData]) -> TokenStream {
let write_invocations = variants
.iter()
.map(|v| generate_variant_write_invocation(ident, v));
quote! { match &self { #(#write_invocations),* } }
}
fn generate_write_invocation<T: Fn(usize, Option<Ident>) -> TokenStream>(
ident: &str,
fields: &Enumerate<Iter<FieldFormattingData>>,
accessor_generator: T,
) -> TokenStream {
let field_fmt_strs = collect_field_fmt_strs(fields);
let field_accessors = fields
.clone()
.map(|(idx, field)| generate_accessor(idx, field, &accessor_generator));
let fmt_str = ident.to_owned() + " {{ " + &field_fmt_strs.join(", ") + " }}";
quote! { write!(f, #fmt_str, #(#field_accessors),*) }
}
fn generate_variant_write_invocation(
enum_ident: &str,
variant: &VariantFormattingData,
) -> TokenStream {
let variant_name: String = enum_ident.to_owned() + "::" + &variant.ident;
let variant_path: Path = parse_str(&variant_name).expect("String is not tokens");
if variant.fields.is_empty() {
quote! { #variant_path => write!(f, #variant_name) }
} else {
let fields = variant.fields.iter().enumerate().map(|(idx, field)| {
generate_variant_accessor(idx, field.ident.clone().map(|it| format_ident!("{}", it)))
});
let write_invocation = generate_write_invocation(
&variant_name,
&variant.fields.iter().enumerate(),
generate_variant_accessor,
);
if variant.fields[0].ident.is_some() {
quote! { #variant_path{#(#fields),*} => #write_invocation }
} else {
quote! { #variant_path(#(#fields),*) => #write_invocation }
}
}
}
fn generate_accessor<T: Fn(usize, Option<Ident>) -> TokenStream>(
idx: usize,
field: &FieldFormattingData,
quote: &T,
) -> TokenStream {
if field.sensitive {
quote! { "<redacted>" }
} else {
let ident = field.ident.clone().map(|it| format_ident!("{}", it));
quote(idx, ident)
}
}
fn generate_struct_accessor(idx: usize, ident: &Option<Ident>) -> TokenStream {
let index: Index = idx.into();
match &ident {
None => quote! { self.#index },
Some(ident) => quote! { self.#ident },
}
}
fn generate_variant_accessor(idx: usize, ident: Option<Ident>) -> TokenStream {
let id = match ident {
None => format_ident!("f{}", idx),
Some(it) => it,
};
quote! {#id}
}
fn collect_field_fmt_strs(fields: &Enumerate<Iter<FieldFormattingData>>) -> Vec<String> {
fields
.clone()
.map(|(idx, field)| {
get_field_name_or_index(idx, field) + ": " + if field.sensitive { "{}" } else { "{:?}" }
})
.collect::<Vec<String>>()
}
fn get_field_name_or_index(idx: usize, field: &FieldFormattingData) -> String {
match &field.ident {
None => format!("{idx}"),
Some(ident) => ident.clone(),
}
}
#[cfg(test)]
mod tests {
use super::FmtBodySource;
use crate::formatting_data::*;
use pretty_assertions::assert_eq;
use quote::quote;
#[test]
fn should_generate_fmt_body_for_empty_struct_data() {
let mut source = MockFormattingDataSource::new();
source
.expect_to_formatting_data()
.returning(|| Ok(FormattingData::StructData(vec![])));
assert_eq!(
source
.generate_fmt_body("TestStruct")
.expect("Should have succeeded")
.to_string(),
quote!(write!(f, "TestStruct")).to_string()
);
}
#[test]
fn should_generate_fmt_body_for_named_field_struct_data() {
let mut source = MockFormattingDataSource::new();
source.expect_to_formatting_data().returning(|| {
Ok(FormattingData::StructData(vec![
FieldFormattingData {
ident: Some("a".to_owned()),
sensitive: false,
},
FieldFormattingData {
ident: Some("b".to_owned()),
sensitive: true,
},
]))
});
assert_eq!(
source
.generate_fmt_body("TestStruct")
.expect("Should have succeeded")
.to_string(),
quote!(write!(
f,
"TestStruct {{ a: {:?}, b: {} }}",
self.a, "<redacted>"
))
.to_string()
);
}
#[test]
fn should_generate_fmt_body_for_unnamed_field_struct_data() {
let mut source = MockFormattingDataSource::new();
source.expect_to_formatting_data().returning(|| {
Ok(FormattingData::StructData(vec![
FieldFormattingData {
ident: None,
sensitive: false,
},
FieldFormattingData {
ident: None,
sensitive: true,
},
]))
});
assert_eq!(
source
.generate_fmt_body("TestStruct")
.expect("Should have succeeded")
.to_string(),
quote!(write!(
f,
"TestStruct {{ 0: {:?}, 1: {} }}",
self.0, "<redacted>"
))
.to_string()
);
}
#[test]
#[rustfmt::skip]
fn should_generate_fmt_body_for_empty_enum_data() {
let mut source = MockFormattingDataSource::new();
source.expect_to_formatting_data().returning(|| {
Ok(FormattingData::EnumData(vec![VariantFormattingData {
ident: "A".to_owned(),
fields: vec![],
}]))
});
assert_eq!(
source
.generate_fmt_body("TestEnum")
.expect("Should have succeeded")
.to_string(),
quote!(match &self {
TestEnum::A => write!(f, "TestEnum::A")
})
.to_string()
);
}
#[test]
fn should_generate_fmt_body_for_named_field_enum_data() {
let mut source = MockFormattingDataSource::new();
source.expect_to_formatting_data().returning(|| {
Ok(FormattingData::EnumData(vec![VariantFormattingData {
ident: "A".to_owned(),
fields: vec![
FieldFormattingData {
ident: Some("a".to_owned()),
sensitive: false,
},
FieldFormattingData {
ident: Some("b".to_owned()),
sensitive: true,
},
],
}]))
});
assert_eq!(
source.generate_fmt_body("TestEnum").expect("Should have succeeded").to_string(),
quote! {
match &self {
TestEnum::A{a, b} => write!(f, "TestEnum::A {{ a: {:?}, b: {} }}", a, "<redacted>")
}
}.to_string()
);
}
#[test]
fn should_generate_fmt_body_for_unnamed_field_enum_data() {
let mut source = MockFormattingDataSource::new();
source.expect_to_formatting_data().returning(|| {
Ok(FormattingData::EnumData(vec![VariantFormattingData {
ident: "A".to_owned(),
fields: vec![
FieldFormattingData {
ident: None,
sensitive: false,
},
FieldFormattingData {
ident: None,
sensitive: true,
},
],
}]))
});
assert_eq!(
source.generate_fmt_body("TestEnum").expect("Should have succeeded").to_string(),
quote! {
match &self {
TestEnum::A(f0, f1) => write!(f, "TestEnum::A {{ 0: {:?}, 1: {} }}", f0, "<redacted>")
}
}.to_string()
);
}
}