diff --git a/README.md b/README.md index 4dbc2810..9735538b 100644 --- a/README.md +++ b/README.md @@ -1592,6 +1592,36 @@ impl MyRecord { } ``` +To reduce boilerplate code, one can also achieve this using the `format` attribute within `#[derive(Tabled)]`. + +```rust +use tabled::Tabled; + +#[derive(Tabled)] +pub struct Motorcycle { + weight: usize, + #[tabled(format = "{} cc")] + cc: usize, +} +``` + +In the above example, the cc field will be formatted using the specified format string "{} cc", where {} is replaced with the value of cc. + +Just like with `display_with` attribute, you can pass arguments for more complex formatting scenarios: + +```rust +use tabled::Tabled; + +#[derive(Tabled)] +pub struct Motorcycle { + weight: usize, + #[tabled(format = "{}/{} cc/kg", self.cc, self.weight)] + cc: usize, +} +``` + +In this case, the cc field will be formatted using the format string "{}/{} cc/kg", and {} will be replaced with the values of cc and weight, respectively. + ### Inline It's possible to inline internal data if it implements the `Tabled` trait using `#[tabled(inline)]`. diff --git a/tabled/tests/derive/derive_test.rs b/tabled/tests/derive/derive_test.rs index c8dfe53b..78406a3b 100644 --- a/tabled/tests/derive/derive_test.rs +++ b/tabled/tests/derive/derive_test.rs @@ -226,6 +226,28 @@ mod tuple { } ); + test_tuple!( + format_1, + t: { u8 #[tabled(format = "foo {}")] sstr }, + init: { 0 "v2" }, + expected: ["0", "1"], ["0", "foo v2"], + ); + + test_tuple!( + format_2, + t: { u8 #[tabled(format = "foo {:?}")] sstr }, + init: { 0 "v2" }, + expected: ["0", "1"], ["0", "foo \"v2\""], + ); + + // todo : self represents the tuple here. It should be the sstr element instead. + // test_tuple!( + // format_3, + // t: { u8 #[tabled(format("foo {} {}", 2, self))] sstr }, + // init: { 0 "v2" }, + // expected: ["0", "1"], ["0", "foo 2 v2"], + // ); + // #[test] // fn order_compile_fail_when_order_is_bigger_then_count_fields() { // #[derive(Tabled)] @@ -661,6 +683,30 @@ mod enum_ { test_enum!(order_15, t: { #[tabled(order = 2)] V1(u8) #[tabled(order = 2)] V2(u8) #[tabled(order = 2)] V3(u8) }, headers: ["V1", "V2", "V3"], tests: V1(0) => ["+", "", ""], V2(0) => ["", "+", ""], V3(0) => ["", "", "+"],); test_enum!(order_0_inlined, t: #[tabled(inline)] { #[tabled(order = 1)] V1(u8) V2(u8) V3(u8) }, headers: ["TestType"], tests: V1(0) => ["V1"], V2(0) => ["V2"], V3(0) => ["V3"],); + + test_enum!( + format, + t: { + #[tabled(inline)] + AbsdEgh { + #[tabled(format("{}-{}", self.a, self.b))] + a: u8, + b: i32 + } + #[tabled(format("{} s", 4))] + B(String) + #[tabled(inline)] + C(#[tabled(rename = "C", format("{} ss", 4))] String) + #[tabled(format = "k.")] + K + }, + headers: ["a", "b", "B", "C", "K"], + tests: + AbsdEgh { a: 0, b: 1 } => ["0-1", "1", "", "", ""], + B(String::new()) => ["", "", "4 s", "", ""], + C(String::new()) => ["", "", "", "4 ss", ""], + K => ["", "", "", "", "k."], + ); } mod unit { @@ -751,6 +797,21 @@ mod structure { init: { f1: 0, f2: Some("v2") } expected: ["f1", "f2"], ["0", "1 2 3"] ); + test_struct!( + display_with_args_using_self, + t: { + f1: u8, + #[tabled(display_with("display_option", self.f1, 2, 3))] + f2: Option, + } + pre: { + fn display_option(v1: &u8, v2: usize, v3: usize) -> String { + format!("{v1} {v2} {v3}") + } + } + init: { f1: 0, f2: Some("v2") } + expected: ["f1", "f2"], ["0", "0 2 3"] + ); test_struct!( display_with_self_static_method, t: { @@ -874,6 +935,28 @@ mod structure { expected: ["Hello", "F2"], ["0", "v2"] ); + test_struct!( + format, + t: { + #[tabled(format = "{} cc")] + f1: u8, + f2: u8, + } + init: { f1: 0, f2: 0 } + expected: ["f1", "f2"], ["0 cc", "0"] + ); + + test_struct!( + format_with_args, + t: { + #[tabled(format("{}/{} cc/kg", self.f1, self.f2))] + f1: u8, + f2: u8, + } + init: { f1: 1, f2: 2 } + expected: ["f1", "f2"], ["1/2 cc/kg", "2"] + ); + // #[test] // fn order_compile_fail_when_order_is_bigger_then_count_fields() { // #[derive(Tabled)] diff --git a/tabled_derive/src/attributes.rs b/tabled_derive/src/attributes.rs index 0e415ea6..3f796212 100644 --- a/tabled_derive/src/attributes.rs +++ b/tabled_derive/src/attributes.rs @@ -19,6 +19,8 @@ pub struct FieldAttributes { pub display_with: Option, pub display_with_args: Option>, pub order: Option, + pub format: Option, + pub format_with_args: Option>, } impl FieldAttributes { @@ -70,6 +72,16 @@ impl FieldAttributes { self.display_with_args = Some(args); } } + FieldAttrKind::FormatWith(format, comma, args) => { + self.format = Some(format.value()); + if comma.is_some() { + let args = args + .into_iter() + .map(|lit| parse_func_arg(&lit)) + .collect::, _>>()?; + self.format_with_args = Some(args); + } + } FieldAttrKind::Order(value) => self.order = Some(lit_int_to_usize(&value)?), } @@ -143,6 +155,7 @@ fn lit_int_to_usize(value: &LitInt) -> Result { #[derive(Debug)] pub enum FuncArg { SelfRef, + SelfProperty(String), Byte(u8), Char(char), Bool(bool), @@ -181,6 +194,10 @@ fn parse_func_arg(expr: &syn::Expr) -> syn::Result { Err(syn::Error::new(path.span(), "unsuported argument")) } } + syn::Expr::Field(field) => match &field.member { + syn::Member::Named(ident) => Ok(FuncArg::SelfProperty(ident.to_string())), + syn::Member::Unnamed(index) => Ok(FuncArg::SelfProperty(index.index.to_string())), + }, expr => Err(syn::Error::new(expr.span(), "unsuported argument")), } } diff --git a/tabled_derive/src/lib.rs b/tabled_derive/src/lib.rs index 58f2525b..0b29225a 100644 --- a/tabled_derive/src/lib.rs +++ b/tabled_derive/src/lib.rs @@ -183,7 +183,7 @@ fn info_from_fields( ) -> Result { let count_fields = fields.len(); - let fields = fields + let attributes = fields .into_iter() .enumerate() .map(|(i, field)| -> Result<_, Error> { @@ -198,7 +198,7 @@ fn info_from_fields( let mut reorder = HashMap::new(); let mut skipped = 0; - for result in fields { + for result in attributes { let (i, field, attributes) = result?; if attributes.is_ignored { skipped += 1; @@ -218,8 +218,8 @@ fn info_from_fields( let header = field_headers(field, i, &attributes, header_prefix, trait_path); headers.push(header); - let field_name = field_name(i, field); - let value = get_field_fields(&field_name, &attributes); + let field_name_result = field_name(i, field); + let value = get_field_fields(&field_name_result, &attributes, fields, &field_name); values.push(value); } @@ -404,7 +404,10 @@ fn info_from_variant( Some(args) => match args.is_empty() { true => None, false => { - let args = args.iter().map(fnarg_tokens).collect::>(); + let args = args + .iter() + .map(|arg| fnarg_tokens(arg, &Fields::Unit, field_var_name)) + .collect::>(); Some(quote!( #(#args,)* )) } }, @@ -416,6 +419,27 @@ fn info_from_variant( }; quote! { ::std::borrow::Cow::from(format!("{}", #result)) } + } else if let Some(custom_format) = &attr.format { + let args = match &attr.format_with_args { + None => None, + Some(args) => match args.is_empty() { + true => None, + false => { + let args = args + .iter() + .map(|arg| fnarg_tokens(arg, &Fields::Unit, field_var_name)) + .collect::>(); + Some(quote!( #(#args,)* )) + } + }, + }; + + let call = match args { + Some(args) => quote!(format!(#custom_format, #args)), + None => quote!(format!(#custom_format)), + }; + + quote! { ::std::borrow::Cow::from(#call) } } else { let default_value = "+"; quote! { ::std::borrow::Cow::Borrowed(#default_value) } @@ -454,7 +478,12 @@ fn get_type_headers( } } -fn get_field_fields(field: &TokenStream, attr: &FieldAttributes) -> TokenStream { +fn get_field_fields( + field: &TokenStream, + attr: &FieldAttributes, + fields: &Fields, + field_name: impl Fn(usize, &Field) -> TokenStream, +) -> TokenStream { if attr.inline { return quote! { #field.fields() }; } @@ -465,7 +494,10 @@ fn get_field_fields(field: &TokenStream, attr: &FieldAttributes) -> TokenStream Some(args) => match args.is_empty() { true => None, false => { - let args = args.iter().map(fnarg_tokens).collect::>(); + let args = args + .iter() + .map(|arg| fnarg_tokens(arg, fields, &field_name)) + .collect::>(); Some(quote!( #(#args,)* )) } }, @@ -477,6 +509,27 @@ fn get_field_fields(field: &TokenStream, attr: &FieldAttributes) -> TokenStream }; return quote!(vec![::std::borrow::Cow::from(format!("{}", #result))]); + } else if let Some(custom_format) = &attr.format { + let args = match &attr.format_with_args { + None => None, + Some(args) => match args.is_empty() { + true => None, + false => { + let args = args + .iter() + .map(|arg| fnarg_tokens(arg, fields, &field_name)) + .collect::>(); + Some(quote!( #(#args,)* )) + } + }, + }; + + let call = match args { + Some(args) => use_format(&args, custom_format), + None => use_format_no_args(custom_format, field), + }; + + return quote!(vec![::std::borrow::Cow::Owned(#call)]); } quote!(vec![::std::borrow::Cow::Owned(format!("{}", #field))]) @@ -508,6 +561,14 @@ fn use_function_no_args(function: &str) -> TokenStream { } } +fn use_format(args: &TokenStream, custom_format: &str) -> TokenStream { + quote! { format!(#custom_format, #args) } +} + +fn use_format_no_args(custom_format: &str, field: &TokenStream) -> TokenStream { + quote! { format!(#custom_format, #field) } +} + fn field_var_name(index: usize, field: &Field) -> TokenStream { let f = field.ident.as_ref().map_or_else( || Index::from(index).to_token_stream(), @@ -650,9 +711,27 @@ fn merge_attributes(attr: &mut FieldAttributes, global_attr: &TypeAttributes) { } } -fn fnarg_tokens(arg: &FuncArg) -> TokenStream { +fn fnarg_tokens( + arg: &FuncArg, + fields: &Fields, + field_name: impl Fn(usize, &Field) -> TokenStream, +) -> TokenStream { match arg { FuncArg::SelfRef => quote! { &self }, + FuncArg::SelfProperty(val) => { + let property_name = syn::Ident::new(val, proc_macro2::Span::call_site()); + + // We find the corresponding field in the local object fields instead of using self, + // which would be a higher level object. This is for nested structures. + for (i, field) in fields.iter().enumerate() { + let field_name_result = field_name(i, field); + if field_name_result.to_string() == *val { + return quote! { #field_name_result }; + } + } + + quote! { &self.#property_name } + } FuncArg::Byte(val) => quote! { #val }, FuncArg::Char(val) => quote! { #val }, FuncArg::Bool(val) => quote! { #val }, diff --git a/tabled_derive/src/parse/field_attr.rs b/tabled_derive/src/parse/field_attr.rs index 396b5bb3..761a3333 100644 --- a/tabled_derive/src/parse/field_attr.rs +++ b/tabled_derive/src/parse/field_attr.rs @@ -33,6 +33,7 @@ pub enum FieldAttrKind { RenameAll(LitStr), DisplayWith(LitStr, Option, Punctuated), Order(LitInt), + FormatWith(LitStr, Option, Punctuated), } impl Parse for FieldAttr { @@ -54,6 +55,9 @@ impl Parse for FieldAttr { "display_with" => { return Ok(Self::new(name, DisplayWith(lit, None, Punctuated::new()))) } + "format" => { + return Ok(Self::new(name, FormatWith(lit, None, Punctuated::new()))) + } _ => {} } } @@ -90,7 +94,7 @@ impl Parse for FieldAttr { let lit = nested.parse::()?; match name_str.as_str() { - "display_with" => { + "format" | "display_with" => { let mut args = Punctuated::new(); let mut comma = None; if nested.peek(Token![,]) { @@ -106,6 +110,10 @@ impl Parse for FieldAttr { } }; + if name_str.as_str() == "format" { + return Ok(Self::new(name, FormatWith(lit, comma, args))); + } + return Ok(Self::new(name, DisplayWith(lit, comma, args))); } "inline" => {