From 33662b43c27a36a83db797593a908393260f2e05 Mon Sep 17 00:00:00 2001 From: Thomas Simmer Date: Sun, 24 Mar 2024 22:32:58 +0100 Subject: [PATCH 1/7] Add format attribute on Tabled derive --- tabled/tests/derive/derive_test.rs | 41 +++++++++++++++++++++++ tabled_derive/src/attributes.rs | 19 +++++++++++ tabled_derive/src/lib.rs | 52 ++++++++++++++++++++++++++++++ tabled_derive/src/parse.rs | 10 +++++- 4 files changed, 121 insertions(+), 1 deletion(-) diff --git a/tabled/tests/derive/derive_test.rs b/tabled/tests/derive/derive_test.rs index e742583f..8dfa45b7 100644 --- a/tabled/tests/derive/derive_test.rs +++ b/tabled/tests/derive/derive_test.rs @@ -661,6 +661,25 @@ 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"],); + + // todo + // test_enum!( + // format, + // t: { + // #[tabled(format("display1"))] + // AbsdEgh { a: u8, b: i32 } + // #[tabled(format("display1"))] + // B(String) + // #[tabled(format("display1"))] + // K + // }, + // headers: ["AbsdEgh", "B", "K"], + // tests: + // AbsdEgh { a: 0, b: 0 } => ["1 2", "", ""], + // B(String::new()) => ["", "asd 200 Hello World", ""], + // K => ["", "", "100 200"], + // ); + } mod unit { @@ -874,6 +893,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 2cf5dc43..d98a761b 100644 --- a/tabled_derive/src/attributes.rs +++ b/tabled_derive/src/attributes.rs @@ -12,6 +12,8 @@ pub struct Attributes { pub display_with: Option, pub display_with_args: Option>, pub order: Option, + pub format: Option, + pub format_with_args: Option>, } impl Attributes { @@ -64,6 +66,16 @@ impl Attributes { } } parse::TabledAttrKind::Order(value) => self.order = Some(lit_int_to_usize(&value)?), + parse::TabledAttrKind::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); + } + } } Ok(()) @@ -104,6 +116,7 @@ fn lit_int_to_usize(value: &LitInt) -> Result { #[derive(Debug)] pub enum FuncArg { SelfRef, + SelfRefProperty(String), Byte(u8), Char(char), Bool(bool), @@ -142,6 +155,12 @@ fn parse_func_arg(expr: &syn::Expr) -> syn::Result { Err(syn::Error::new(path.span(), "unsuported argument")) } } + syn::Expr::Field(field) => { + if let syn::Member::Named(ident) = &field.member { + return Ok(FuncArg::SelfRefProperty(ident.to_string())); + } + Err(syn::Error::new(field.base.span(), "unsupported argument")) + } expr => Err(syn::Error::new(expr.span(), "unsuported argument")), } } diff --git a/tabled_derive/src/lib.rs b/tabled_derive/src/lib.rs index 26a1f355..16651e4b 100644 --- a/tabled_derive/src/lib.rs +++ b/tabled_derive/src/lib.rs @@ -390,6 +390,30 @@ 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(fnarg_tokens).collect::>(); + Some(quote!( #(#args,)* )) + } + }, + }; + + match args { + Some(args) => { + quote!(vec![::std::borrow::Cow::Owned( + format!(#custom_format, #args) + )]) + } + None => { + quote!(vec![::std::borrow::Cow::Owned( + format!(#custom_format, #variant) + )]) + } + } } else { let default_value = "+"; quote! { ::std::borrow::Cow::Borrowed(#default_value) } @@ -446,6 +470,30 @@ fn get_field_fields(field: &TokenStream, attr: &Attributes) -> 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(fnarg_tokens).collect::>(); + Some(quote!( #(#args,)* )) + } + }, + }; + + let _ = match args { + Some(args) => { + return quote!(vec![::std::borrow::Cow::Owned( + format!(#custom_format, #args) + )]) + } + None => { + return quote!(vec![::std::borrow::Cow::Owned( + format!(#custom_format, #field) + )]) + } + }; } quote!(vec![::std::borrow::Cow::Owned(format!("{}", #field))]) @@ -621,6 +669,10 @@ fn merge_attributes(attr: &mut Attributes, global_attr: &StructAttributes) { fn fnarg_tokens(arg: &FuncArg) -> TokenStream { match arg { FuncArg::SelfRef => quote! { &self }, + FuncArg::SelfRefProperty(val) => { + let property_name = syn::Ident::new(val, proc_macro2::Span::call_site()); + 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.rs b/tabled_derive/src/parse.rs index 001f38c4..fd45db99 100644 --- a/tabled_derive/src/parse.rs +++ b/tabled_derive/src/parse.rs @@ -33,6 +33,7 @@ pub enum TabledAttrKind { RenameAll(LitStr), DisplayWith(LitStr, Option, Punctuated), Order(LitInt), + FormatWith(LitStr, Option, Punctuated), } impl Parse for TabledAttr { @@ -54,6 +55,9 @@ impl Parse for TabledAttr { "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 TabledAttr { 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 TabledAttr { } }; + 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" => { From 7b1e39f7d2d82a2f12b17914c40c26dc5dd7fc5b Mon Sep 17 00:00:00 2001 From: Thomas Simmer Date: Mon, 25 Mar 2024 23:25:38 +0100 Subject: [PATCH 2/7] Handle format for nested structs --- tabled/tests/derive/derive_test.rs | 63 +++++++++++----- tabled_derive/src/attributes.rs | 12 ++- tabled_derive/src/lib.rs | 114 ++++++++++++++++++----------- 3 files changed, 122 insertions(+), 67 deletions(-) diff --git a/tabled/tests/derive/derive_test.rs b/tabled/tests/derive/derive_test.rs index 8dfa45b7..dc6b15e9 100644 --- a/tabled/tests/derive/derive_test.rs +++ b/tabled/tests/derive/derive_test.rs @@ -226,6 +226,28 @@ mod tuple { } ); + test_tuple!( + format, + t: { u8 #[tabled(format = "foo")] sstr }, + init: { 0 "v2" }, + expected: ["0", "1"], ["0", "foo"], + ); + + test_tuple!( + format_1, + t: { u8 #[tabled(format = "foo {}")] sstr }, + init: { 0 "v2" }, + expected: ["0", "1"], ["0", "foo v2"], + ); + + // todo : self is the tuple here. It should be the sstr element instead. + // test_tuple!( + // format_2, + // 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)] @@ -662,24 +684,29 @@ mod enum_ { 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"],); - // todo - // test_enum!( - // format, - // t: { - // #[tabled(format("display1"))] - // AbsdEgh { a: u8, b: i32 } - // #[tabled(format("display1"))] - // B(String) - // #[tabled(format("display1"))] - // K - // }, - // headers: ["AbsdEgh", "B", "K"], - // tests: - // AbsdEgh { a: 0, b: 0 } => ["1 2", "", ""], - // B(String::new()) => ["", "asd 200 Hello World", ""], - // K => ["", "", "100 200"], - // ); - + 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 { diff --git a/tabled_derive/src/attributes.rs b/tabled_derive/src/attributes.rs index d98a761b..b039fa05 100644 --- a/tabled_derive/src/attributes.rs +++ b/tabled_derive/src/attributes.rs @@ -116,7 +116,7 @@ fn lit_int_to_usize(value: &LitInt) -> Result { #[derive(Debug)] pub enum FuncArg { SelfRef, - SelfRefProperty(String), + SelfProperty(String), Byte(u8), Char(char), Bool(bool), @@ -155,12 +155,10 @@ fn parse_func_arg(expr: &syn::Expr) -> syn::Result { Err(syn::Error::new(path.span(), "unsuported argument")) } } - syn::Expr::Field(field) => { - if let syn::Member::Named(ident) = &field.member { - return Ok(FuncArg::SelfRefProperty(ident.to_string())); - } - Err(syn::Error::new(field.base.span(), "unsupported 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 16651e4b..2bc6e4d6 100644 --- a/tabled_derive/src/lib.rs +++ b/tabled_derive/src/lib.rs @@ -164,22 +164,23 @@ fn info_from_fields( ) -> Result { let count_fields = fields.len(); - let fields = fields - .into_iter() - .enumerate() - .map(|(i, field)| -> Result<_, Error> { - let mut attributes = Attributes::parse(&field.attrs)?; - merge_attributes(&mut attributes, attrs); - - Ok((i, field, attributes)) - }); + let fields_with_attributes = + fields + .into_iter() + .enumerate() + .map(|(i, field)| -> Result<_, Error> { + let mut attributes = Attributes::parse(&field.attrs)?; + merge_attributes(&mut attributes, attrs); + + Ok((i, field, attributes)) + }); let mut headers = Vec::new(); let mut values = Vec::new(); let mut reorder = HashMap::new(); let mut skipped = 0; - for result in fields { + for result in fields_with_attributes { let (i, field, attributes) = result?; if attributes.is_ignored() { skipped += 1; @@ -199,8 +200,8 @@ fn info_from_fields( let header = field_headers(field, i, &attributes, header_prefix); 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); } @@ -378,7 +379,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,)* )) } }, @@ -396,24 +400,21 @@ 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,)* )) } }, }; - match args { - Some(args) => { - quote!(vec![::std::borrow::Cow::Owned( - format!(#custom_format, #args) - )]) - } - None => { - quote!(vec![::std::borrow::Cow::Owned( - format!(#custom_format, #variant) - )]) - } - } + 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) } @@ -447,7 +448,12 @@ fn get_type_headers(field_type: &Type, inline_prefix: &str, prefix: &str) -> Tok } } -fn get_field_fields(field: &TokenStream, attr: &Attributes) -> TokenStream { +fn get_field_fields( + field: &TokenStream, + attr: &Attributes, + fields: &Fields, + field_name: impl Fn(usize, &Field) -> TokenStream, +) -> TokenStream { if attr.inline { return quote! { #field.fields() }; } @@ -458,7 +464,10 @@ fn get_field_fields(field: &TokenStream, attr: &Attributes) -> 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,)* )) } }, @@ -476,24 +485,21 @@ fn get_field_fields(field: &TokenStream, attr: &Attributes) -> 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,)* )) } }, }; - let _ = match args { - Some(args) => { - return quote!(vec![::std::borrow::Cow::Owned( - format!(#custom_format, #args) - )]) - } - None => { - return quote!(vec![::std::borrow::Cow::Owned( - format!(#custom_format, #field) - )]) - } + 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))]) @@ -525,6 +531,17 @@ 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 { + if custom_format.contains("{}") { + return quote! { format!(#custom_format, #field) }; + } + return quote! { format!(#custom_format) }; +} + fn field_var_name(index: usize, field: &Field) -> TokenStream { let f = field.ident.as_ref().map_or_else( || Index::from(index).to_token_stream(), @@ -666,11 +683,24 @@ fn merge_attributes(attr: &mut Attributes, global_attr: &StructAttributes) { } } -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::SelfRefProperty(val) => { + FuncArg::SelfProperty(val) => { let property_name = syn::Ident::new(val, proc_macro2::Span::call_site()); + + // We find the field in the neighbours instead of taking self, which would be the top object. + // This is for nested formatting. + for (i, field) in fields.iter().enumerate() { + let field_name_result = field_name(i, field); + if field_name_result.to_string() == val.to_string() { + return quote! { #field_name_result }; + } + } quote! { &self.#property_name } } FuncArg::Byte(val) => quote! { #val }, From c94aa8dccc951170f7631b18d2fa84277a1c0618 Mon Sep 17 00:00:00 2001 From: Thomas Simmer Date: Tue, 26 Mar 2024 07:53:54 +0100 Subject: [PATCH 3/7] Add format attribute in readme --- README.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/README.md b/README.md index 4dbc2810..24cae7bd 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)]`. From 74558dbbbcda05989530d15735ad462a1b18498c Mon Sep 17 00:00:00 2001 From: Thomas Simmer Date: Tue, 2 Apr 2024 18:03:04 +0200 Subject: [PATCH 4/7] Fix README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 24cae7bd..9735538b 100644 --- a/README.md +++ b/README.md @@ -1615,7 +1615,7 @@ use tabled::Tabled; #[derive(Tabled)] pub struct Motorcycle { weight: usize, - #[tabled(format = "{}/{} cc/kg", "self.cc", "self.weight")] + #[tabled(format = "{}/{} cc/kg", self.cc, self.weight)] cc: usize, } ``` From 92f5ccf664942631ff0ad6ffe8a685dbcfbcf2af Mon Sep 17 00:00:00 2001 From: Thomas Simmer Date: Tue, 2 Apr 2024 18:03:47 +0200 Subject: [PATCH 5/7] Fix tests --- tabled/tests/derive/derive_test.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tabled/tests/derive/derive_test.rs b/tabled/tests/derive/derive_test.rs index dc6b15e9..d8474a62 100644 --- a/tabled/tests/derive/derive_test.rs +++ b/tabled/tests/derive/derive_test.rs @@ -227,22 +227,22 @@ mod tuple { ); test_tuple!( - format, - t: { u8 #[tabled(format = "foo")] sstr }, + format_1, + t: { u8 #[tabled(format = "foo {}")] sstr }, init: { 0 "v2" }, - expected: ["0", "1"], ["0", "foo"], + expected: ["0", "1"], ["0", "foo v2"], ); test_tuple!( - format_1, - t: { u8 #[tabled(format = "foo {}")] sstr }, + format_2, + t: { u8 #[tabled(format = "foo {:?}")] sstr }, init: { 0 "v2" }, - expected: ["0", "1"], ["0", "foo v2"], + expected: ["0", "1"], ["0", "foo \"v2\""], ); - // todo : self is the tuple here. It should be the sstr element instead. + // todo : self represents the tuple here. It should be the sstr element instead. // test_tuple!( - // format_2, + // format_3, // t: { u8 #[tabled(format("foo {} {}", 2, self))] sstr }, // init: { 0 "v2" }, // expected: ["0", "1"], ["0", "foo 2 v2"], From d75af888ca090070b63746d7769375a74521a87a Mon Sep 17 00:00:00 2001 From: Thomas Simmer Date: Tue, 2 Apr 2024 18:04:02 +0200 Subject: [PATCH 6/7] Simplify use_format_no_args --- tabled_derive/src/lib.rs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/tabled_derive/src/lib.rs b/tabled_derive/src/lib.rs index 2bc6e4d6..e03e1761 100644 --- a/tabled_derive/src/lib.rs +++ b/tabled_derive/src/lib.rs @@ -536,10 +536,7 @@ fn use_format(args: &TokenStream, custom_format: &str) -> TokenStream { } fn use_format_no_args(custom_format: &str, field: &TokenStream) -> TokenStream { - if custom_format.contains("{}") { - return quote! { format!(#custom_format, #field) }; - } - return quote! { format!(#custom_format) }; + return quote! { format!(#custom_format, #field) }; } fn field_var_name(index: usize, field: &Field) -> TokenStream { @@ -693,14 +690,15 @@ fn fnarg_tokens( FuncArg::SelfProperty(val) => { let property_name = syn::Ident::new(val, proc_macro2::Span::call_site()); - // We find the field in the neighbours instead of taking self, which would be the top object. - // This is for nested formatting. + // 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.to_string() { return quote! { #field_name_result }; } } + quote! { &self.#property_name } } FuncArg::Byte(val) => quote! { #val }, From b57b2d44a4b9a3ed485bebeecc61e7a347b332f5 Mon Sep 17 00:00:00 2001 From: Thomas Simmer Date: Tue, 2 Apr 2024 18:14:40 +0200 Subject: [PATCH 7/7] Add a test on display_with using self property --- tabled/tests/derive/derive_test.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tabled/tests/derive/derive_test.rs b/tabled/tests/derive/derive_test.rs index d8474a62..f0e767fe 100644 --- a/tabled/tests/derive/derive_test.rs +++ b/tabled/tests/derive/derive_test.rs @@ -797,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: {