Skip to content

Commit

Permalink
Tabled add attribute format (#398)
Browse files Browse the repository at this point in the history
* Add format attribute on Tabled derive

* Handle format for nested structs

* Add format attribute in readme

* Fix README

* Fix tests

* Simplify use_format_no_args

* Add a test on display_with using self property

---------

Co-authored-by: Maxim Zhiburt <[email protected]>
  • Loading branch information
thomassimmer and zhiburt authored Apr 4, 2024
1 parent 5534053 commit 6ce5d34
Show file tree
Hide file tree
Showing 5 changed files with 226 additions and 9 deletions.
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)]`.
Expand Down
83 changes: 83 additions & 0 deletions tabled/tests/derive/derive_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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<sstr>,
}
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: {
Expand Down Expand Up @@ -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)]
Expand Down
17 changes: 17 additions & 0 deletions tabled_derive/src/attributes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ pub struct FieldAttributes {
pub display_with: Option<String>,
pub display_with_args: Option<Vec<FuncArg>>,
pub order: Option<usize>,
pub format: Option<String>,
pub format_with_args: Option<Vec<FuncArg>>,
}

impl FieldAttributes {
Expand Down Expand Up @@ -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::<Result<Vec<_>, _>>()?;
self.format_with_args = Some(args);
}
}
FieldAttrKind::Order(value) => self.order = Some(lit_int_to_usize(&value)?),
}

Expand Down Expand Up @@ -143,6 +155,7 @@ fn lit_int_to_usize(value: &LitInt) -> Result<usize, Error> {
#[derive(Debug)]
pub enum FuncArg {
SelfRef,
SelfProperty(String),
Byte(u8),
Char(char),
Bool(bool),
Expand Down Expand Up @@ -181,6 +194,10 @@ fn parse_func_arg(expr: &syn::Expr) -> syn::Result<FuncArg> {
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")),
}
}
95 changes: 87 additions & 8 deletions tabled_derive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ fn info_from_fields(
) -> Result<Impl, Error> {
let count_fields = fields.len();

let fields = fields
let attributes = fields
.into_iter()
.enumerate()
.map(|(i, field)| -> Result<_, Error> {
Expand All @@ -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;
Expand All @@ -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);
}

Expand Down Expand Up @@ -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::<Vec<_>>();
let args = args
.iter()
.map(|arg| fnarg_tokens(arg, &Fields::Unit, field_var_name))
.collect::<Vec<_>>();
Some(quote!( #(#args,)* ))
}
},
Expand All @@ -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::<Vec<_>>();
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) }
Expand Down Expand Up @@ -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() };
}
Expand All @@ -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::<Vec<_>>();
let args = args
.iter()
.map(|arg| fnarg_tokens(arg, fields, &field_name))
.collect::<Vec<_>>();
Some(quote!( #(#args,)* ))
}
},
Expand All @@ -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::<Vec<_>>();
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))])
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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 },
Expand Down
10 changes: 9 additions & 1 deletion tabled_derive/src/parse/field_attr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ pub enum FieldAttrKind {
RenameAll(LitStr),
DisplayWith(LitStr, Option<Token!(,)>, Punctuated<syn::Expr, Token!(,)>),
Order(LitInt),
FormatWith(LitStr, Option<Token!(,)>, Punctuated<syn::Expr, Token!(,)>),
}

impl Parse for FieldAttr {
Expand All @@ -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())))
}
_ => {}
}
}
Expand Down Expand Up @@ -90,7 +94,7 @@ impl Parse for FieldAttr {
let lit = nested.parse::<LitStr>()?;

match name_str.as_str() {
"display_with" => {
"format" | "display_with" => {
let mut args = Punctuated::new();
let mut comma = None;
if nested.peek(Token![,]) {
Expand All @@ -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" => {
Expand Down

0 comments on commit 6ce5d34

Please sign in to comment.