Repository: zhiburt/tabled
Pull request #169 (2022-06-10 17:25:14)

tabled_derive/ Add self argument option for display_with


Patch:
diff --git a/src/style.rs b/src/style.rs
index aef9183..3296fdf 100644
--- a/src/style.rs
+++ b/src/style.rs
@@ -1292,7 +1292,8 @@ fn correct_span_styles(grid: &mut Grid) {
 
             let is_first = col == c;
             let has_up = row > 0 && has_vertical(grid, &spans, (row - 1, col));
-            let has_down = row + 1 < grid.count_rows() && has_vertical(grid, &spans, (row + 1, col));
+            let has_down =
+                row + 1 < grid.count_rows() && has_vertical(grid, &spans, (row + 1, col));
 
             let mut border = grid.get_border(row, col);
 
diff --git a/tabled_derive/src/lib.rs b/tabled_derive/src/lib.rs
index 609a43c..9c16180 100644
--- a/tabled_derive/src/lib.rs
+++ b/tabled_derive/src/lib.rs
@@ -5,7 +5,7 @@ use quote::*;
 use std::{collections::HashMap, str};
 use syn::{
     parse_macro_input, token, Attribute, Data, DataEnum, DataStruct, DeriveInput, Field, Fields,
-    Ident, Index, Lit, Meta, NestedMeta, Type, Variant,
+    Ident, Index, Lit, Meta, MetaNameValue, NestedMeta, Type, Variant,
 };
 
 #[proc_macro_derive(Tabled, attributes(tabled))]
@@ -295,7 +295,11 @@ fn get_field_fields(field: TokenStream, attr: &Attributes) -> TokenStream {
     }
 
     if let Some(func) = &attr.display_with {
-        let func_call = use_function_for(field, func);
+        let func_call = match attr.display_with_use_self {
+            true => use_function_with_self(func),
+            false => use_function_for(field, func),
+        };
+
         return quote!(vec![#func_call]);
     }
 
@@ -315,6 +319,19 @@ fn use_function_for(field: TokenStream, function: &str) -> TokenStream {
     }
 }
 
+fn use_function_with_self(function: &str) -> TokenStream {
+    let path: syn::Result<syn::ExprPath> = syn::parse_str(function);
+    match path {
+        Ok(path) => {
+            quote! { #path(&self) }
+        }
+        _ => {
+            let function = Ident::new(function, proc_macro2::Span::call_site());
+            quote! { #function(&self) }
+        }
+    }
+}
+
 fn field_var_name(index: usize, field: &Field) -> TokenStream {
     let f = field.ident.as_ref().map_or_else(
         || Index::from(index).to_token_stream(),
@@ -445,6 +462,7 @@ struct Attributes {
     inline_prefix: Option<String>,
     name: Option<String>,
     display_with: Option<String>,
+    display_with_use_self: bool,
     order: Option<usize>,
 }
 
@@ -453,12 +471,14 @@ impl Attributes {
         let is_ignored = attrs_has_ignore_sign(attrs);
         let should_be_inlined = should_be_inlined(attrs);
         let inline_prefix = look_for_inline_prefix(attrs);
-        let display_with = check_display_with_func(attrs);
         let override_header_name = override_header_name(attrs);
         let order = override_header_order(attrs);
+        let (display_with, display_with_use_self) =
+            check_display_with_func(attrs).map_or((None, false), |(a, b)| (Some(a), b));
 
         Self {
             display_with,
+            display_with_use_self,
             is_ignored,
             order,
             inline_prefix,
@@ -480,8 +500,11 @@ fn override_header_order(attrs: &[Attribute]) -> Option<usize> {
     find_name_attribute(attrs, "tabled", "order", look_up_nested_meta_usize)
 }
 
-fn check_display_with_func(attrs: &[Attribute]) -> Option<String> {
-    find_name_attribute(attrs, "tabled", "display_with", look_up_nested_meta_str)
+fn check_display_with_func(attrs: &[Attribute]) -> Option<(String, bool)> {
+    find_name_attribute_(attrs, |attr| {
+        Ok(parse_display_with_attribute(attr, "tabled"))
+    })
+    .unwrap()
 }
 
 fn should_be_inlined(attrs: &[Attribute]) -> bool {
@@ -590,7 +613,7 @@ fn look_up_nested_flag_str_in_attr(
     name: &str,
 ) -> Result<Option<String>, String> {
     match meta {
-        NestedMeta::Meta(Meta::List(list)) => {
+        NestedMeta::Meta(Meta::List(list)) if list.path.is_ident(name) => {
             parse_name_attribute_nested(list.nested.iter(), "", name, look_up_nested_meta_flag_str)
                 .ok_or_else(|| "An attribute doesn't have expected value".to_string())
                 .map(Some)
@@ -637,3 +660,85 @@ where
         .iter()
         .find_map(|attr| parse_name_attribute(attr, method, name, lookup.clone()))
 }
+
+fn find_name_attribute_<R, F>(attributes: &[Attribute], lookup: F) -> Result<Option<R>, String>
+where
+    F: Fn(&Attribute) -> Result<Option<R>, String>,
+{
+    for attr in attributes {
+        let result = (lookup)(attr)?;
+        if result.is_some() {
+            return Ok(result);
+        }
+    }
+
+    Ok(None)
+}
+
+fn parse_display_with_attribute(attr: &Attribute, attr_name: &str) -> Option<(String, bool)> {
+    if !attr.path.is_ident(attr_name) {
+        return None;
+    }
+
+    let meta = attr.parse_meta().ok()?;
+
+    if let Meta::List(list) = meta {
+        for meta in list.nested {
+            let val = parse_display_with_attribute_obj(&meta, "display_with");
+            match val {
+                Ok(Some(value)) => return Some(value),
+                Err(err) => {
+                    panic!("{error} macros {macro} field {name}", error=err, macro=attr_name, name="display_with")
+                }
+                _ => {}
+            }
+        }
+    }
+
+    None
+}
+
+fn parse_display_with_attribute_obj(
+    meta: &NestedMeta,
+    attr_name: &str,
+) -> Result<Option<(String, bool)>, String> {
+    match meta {
+        NestedMeta::Meta(Meta::NameValue(MetaNameValue { path, lit, .. }))
+            if path.is_ident(attr_name) =>
+        {
+            let value = take_str_literal(lit)?;
+            Ok(Some((value, false)))
+        }
+        NestedMeta::Meta(Meta::List(list)) if list.path.is_ident(attr_name) => {
+            let mut name = None;
+            let mut use_self = false;
+            for meta in &list.nested {
+                match meta {
+                    NestedMeta::Meta(Meta::Path(path)) if path.is_ident("args") => {
+                        use_self = true;
+                    }
+                    NestedMeta::Lit(lit) if name.is_none() => {
+                        name = Some(take_str_literal(lit)?);
+                    }
+                    _ => {}
+                }
+            }
+
+            match name {
+                Some(name) => Ok(Some((name, use_self))),
+                None => Ok(None),
+            }
+        }
+        _ => Ok(None),
+    }
+}
+
+fn take_str_literal(lit: &Lit) -> Result<String, String> {
+    match lit {
+        Lit::Str(value) => Ok(value.value()),
+        Lit::ByteStr(value) => str::from_utf8(&value.value())
+            .map(|s| s.to_owned())
+            .map_err(|_| "Expected a valid UTF-8 string for a field".to_owned()),
+        _ => Err("Expected a string but got something else".to_owned()),
+    }
+}


Test patch:
diff --git a/tests/derive_test.rs b/tests/derive_test.rs
index 54aad5a..75fb87c 100644
--- a/tests/derive_test.rs
+++ b/tests/derive_test.rs
@@ -54,7 +54,7 @@ mod tupple_structure {
 
     #[allow(dead_code)]
     #[test]
-    fn display_with_self_static_method() {
+    fn display_with_static_method() {
         #[derive(Tabled)]
         struct St(
             u8,
@@ -76,6 +76,76 @@ mod tupple_structure {
         assert_eq!(vec!["0".to_owned(), "1".to_owned()], St::headers());
     }
 
+    #[allow(dead_code)]
+    #[test]
+    fn display_with_self_static_method() {
+        #[derive(Tabled)]
+        struct St(
+            u8,
+            #[tabled(display_with("Self::display_option", args))] Option<&'static str>,
+        );
+
+        impl St {
+            fn display_option(&self) -> String {
+                match self.1 {
+                    Some(s) => format!("some {}", s),
+                    None => "none".to_string(),
+                }
+            }
+        }
+
+        let st = St(0, Some("v2"));
+
+        assert_eq!(vec!["0".to_owned(), "some v2".to_owned()], st.fields());
+        assert_eq!(vec!["0".to_owned(), "1".to_owned()], St::headers());
+    }
+
+    #[allow(dead_code)]
+    #[test]
+    fn display_with_self_static_method2() {
+        #[derive(Tabled)]
+        struct St(
+            u8,
+            #[tabled(display_with("Self::display_option", args))] Option<&'static str>,
+        );
+
+        impl St {
+            fn display_option(o: &St) -> String {
+                match o.1 {
+                    Some(s) => format!("some {}", s),
+                    None => "none".to_string(),
+                }
+            }
+        }
+
+        let st = St(0, Some("v2"));
+
+        assert_eq!(vec!["0".to_owned(), "some v2".to_owned()], st.fields());
+        assert_eq!(vec!["0".to_owned(), "1".to_owned()], St::headers());
+    }
+
+    #[allow(dead_code)]
+    #[test]
+    fn display_with_self_func() {
+        #[derive(Tabled)]
+        struct St(
+            u8,
+            #[tabled(display_with("display_option", args))] Option<&'static str>,
+        );
+
+        fn display_option(o: &St) -> String {
+            match o.1 {
+                Some(s) => format!("some {}", s),
+                None => "none".to_string(),
+            }
+        }
+
+        let st = St(0, Some("v2"));
+
+        assert_eq!(vec!["0".to_owned(), "some v2".to_owned()], st.fields());
+        assert_eq!(vec!["0".to_owned(), "1".to_owned()], St::headers());
+    }
+
     #[test]
     fn tuple() {
         #[derive(Tabled)]
@@ -516,6 +586,36 @@ mod enum_ {
         );
     }
 
+    #[test]
+    fn inline_field_with_display_self_function() {
+        #[derive(Tabled)]
+        enum Developer {
+            #[tabled(inline("backend::"))]
+            Backend {
+                #[tabled(rename = "name")]
+                #[tabled(display_with("display", args))]
+                specific: &'static str,
+            },
+            Frontend,
+        }
+        fn display(_: &Developer) -> String {
+            "asd".to_string()
+        }
+
+        assert_eq!(
+            vec!["backend::name".to_owned(), "Frontend".to_owned()],
+            Developer::headers()
+        );
+        assert_eq!(
+            vec!["asd".to_owned(), "".to_owned()],
+            Developer::Backend { specific: "123" }.fields(),
+        );
+        assert_eq!(
+            vec!["".to_owned(), "+".to_owned()],
+            Developer::Frontend.fields(),
+        );
+    }
+
     #[test]
     fn with_display() {
         #[allow(dead_code)]
@@ -539,6 +639,30 @@ mod enum_ {
         );
         assert_eq!(vec!["".to_owned(), "+".to_owned(),], Fact::Unknown.fields());
     }
+
+    #[test]
+    fn with_display_self() {
+        #[allow(dead_code)]
+        #[derive(Tabled)]
+        enum Fact {
+            #[tabled(inline)]
+            Known(#[tabled(display_with("Self::format::<4>", args))] &'static str),
+            Unknown,
+        }
+
+        impl Fact {
+            fn format<const ID: usize>(_: &Fact) -> String {
+                ID.to_string()
+            }
+        }
+
+        assert_eq!(vec!["0".to_owned(), "Unknown".to_owned(),], Fact::headers());
+        assert_eq!(
+            vec!["4".to_owned(), "".to_owned(),],
+            Fact::Known("Hello World").fields()
+        );
+        assert_eq!(vec!["".to_owned(), "+".to_owned(),], Fact::Unknown.fields());
+    }
 }
 
 mod unit {
@@ -796,6 +920,116 @@ mod structure {
         assert_eq!(vec!["f1".to_owned(), "f2".to_owned()], St::headers());
     }
 
+    #[allow(dead_code)]
+    #[test]
+    fn display_with_2_self_static_method() {
+        #[derive(Tabled)]
+        struct St {
+            f1: u8,
+            #[tabled(display_with("Self::display_option"))]
+            f2: Option<&'static str>,
+        }
+
+        impl St {
+            fn display_option(o: &Option<&'static str>) -> String {
+                match o {
+                    Some(s) => format!("some {}", s),
+                    None => "none".to_string(),
+                }
+            }
+        }
+
+        let st = St {
+            f1: 0,
+            f2: Some("v2"),
+        };
+
+        assert_eq!(vec!["0".to_owned(), "some v2".to_owned()], st.fields());
+        assert_eq!(vec!["f1".to_owned(), "f2".to_owned()], St::headers());
+    }
+
+    #[allow(dead_code)]
+    #[test]
+    fn display_with_self_self_static_method() {
+        #[derive(Tabled)]
+        struct St {
+            f1: u8,
+            #[tabled(display_with("Self::display_option", args))]
+            f2: Option<&'static str>,
+        }
+
+        impl St {
+            fn display_option(o: &St) -> String {
+                match o.f2 {
+                    Some(s) => format!("some {}", s),
+                    None => "none".to_string(),
+                }
+            }
+        }
+
+        let st = St {
+            f1: 0,
+            f2: Some("v2"),
+        };
+
+        assert_eq!(vec!["0".to_owned(), "some v2".to_owned()], st.fields());
+        assert_eq!(vec!["f1".to_owned(), "f2".to_owned()], St::headers());
+    }
+
+    #[allow(dead_code)]
+    #[test]
+    fn display_with_self_2_self_static_method() {
+        #[derive(Tabled)]
+        struct St {
+            f1: u8,
+            #[tabled(display_with("Self::display_option", args))]
+            f2: Option<&'static str>,
+        }
+
+        impl St {
+            fn display_option(&self) -> String {
+                match self.f2 {
+                    Some(s) => format!("some {}", s),
+                    None => "none".to_string(),
+                }
+            }
+        }
+
+        let st = St {
+            f1: 0,
+            f2: Some("v2"),
+        };
+
+        assert_eq!(vec!["0".to_owned(), "some v2".to_owned()], st.fields());
+        assert_eq!(vec!["f1".to_owned(), "f2".to_owned()], St::headers());
+    }
+
+    #[allow(dead_code)]
+    #[test]
+    fn display_with_self_3_self_static_method() {
+        #[derive(Tabled)]
+        struct St {
+            f1: u8,
+            #[tabled(display_with("display_option", args))]
+            f2: Option<&'static str>,
+        }
+
+        fn display_option(o: &St) -> String {
+            match o.f2 {
+                Some(s) => format!("some {}", s),
+                None => "none".to_string(),
+            }
+        }
+
+        let st = St {
+            f1: 0,
+            f2: Some("v2"),
+        };
+
+        assert_eq!(vec!["0".to_owned(), "some v2".to_owned()], st.fields());
+        assert_eq!(vec!["f1".to_owned(), "f2".to_owned()], St::headers());
+    }
+
     #[test]
     fn order_tabled() {
         {
@@ -1230,3 +1464,105 @@ fn skipped_fields_may_not_implement_display() {
         );
     }
 }
+
+#[test]
+fn display_with_used_with_inline() {
+    #[derive(Tabled)]
+    struct Struct1 {
+        f1: &'static str,
+        f2: &'static str,
+        #[tabled(display_with = "print", inline)]
+        f3: usize,
+    }
+
+    #[allow(dead_code)]
+    fn print<T>(_: T) -> String {
+        String::new()
+    }
+
+    let st = Struct1 {
+        f1: "123",
+        f2: "456",
+        f3: 789,
+    };
+
+    assert_eq!(Struct1::headers(), vec!["f1", "f2", "usize"],);
+    assert_eq!(st.fields(), vec!["123", "456", "789"]);
+}
+
+#[test]
+fn display_with_used_with_inline_2() {
+    #[derive(Tabled)]
+    struct Struct1 {
+        f1: &'static str,
+        f2: &'static str,
+        #[tabled(display_with = "print")]
+        #[tabled(inline)]
+        f3: usize,
+    }
+
+    #[allow(dead_code)]
+    fn print<T>(_: T) -> String {
+        String::new()
+    }
+
+    let st = Struct1 {
+        f1: "123",
+        f2: "456",
+        f3: 789,
+    };
+
+    assert_eq!(Struct1::headers(), vec!["f1", "f2", "usize"],);
+    assert_eq!(st.fields(), vec!["123", "456", "789"]);
+}
+
+#[test]
+fn display_with_rename() {
+    #[derive(Tabled)]
+    struct Struct1 {
+        f1: &'static str,
+        f2: &'static str,
+        #[tabled(display_with = "print", rename = "Field 3")]
+        f3: usize,
+    }
+
+    #[allow(dead_code)]
+    fn print<T>(_: T) -> String {
+        String::new()
+    }
+
+    let st = Struct1 {
+        f1: "123",
+        f2: "456",
+        f3: 789,
+    };
+
+    assert_eq!(Struct1::headers(), vec!["f1", "f2", "Field 3"],);
+    assert_eq!(st.fields(), vec!["123", "456", ""]);
+}
+
+#[test]
+fn display_with_rename_2() {
+    #[derive(Tabled)]
+    struct Struct1 {
+        f1: &'static str,
+        f2: &'static str,
+        #[tabled(display_with = "print")]
+        #[tabled(rename = "Field 3")]
+        f3: usize,
+    }
+
+    #[allow(dead_code)]
+    fn print<T>(_: T) -> String {
+        String::new()
+    }
+
+    let st = Struct1 {
+        f1: "123",
+        f2: "456",
+        f3: 789,
+    };
+
+    assert_eq!(Struct1::headers(), vec!["f1", "f2", "Field 3"],);
+    assert_eq!(st.fields(), vec!["123", "456", ""]);
+}