Repository: cobalt-org/liquid-rust
Pull request #385 (2020-01-15 20:43:59)

Derive macros for ObjectView / ValueView
- [ ] Tests created for any new feature or regression tests for bugfixes.



Patch:
diff --git a/Cargo.toml b/Cargo.toml
index 5db1a9b..a86af37 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -44,6 +44,7 @@ url = "1.5"
 doc-comment = "0.3"
 kstring = { version = "0.1.0", path = "crates/kstring" }
 liquid-core = { version = "^0.19", path = "crates/core" }
+liquid-derive = { version = "^0.19", path = "crates/derive" }
 
 serde = { version = "1.0", optional = true, features = ["derive"] }
 deunicode = "1.0.0"
diff --git a/crates/derive/src/lib.rs b/crates/derive/src/lib.rs
index f765629..f2b7a59 100644
--- a/crates/derive/src/lib.rs
+++ b/crates/derive/src/lib.rs
@@ -7,7 +7,9 @@ use syn;
 mod filter;
 mod filter_parameters;
 pub(crate) mod helpers;
+mod object_view;
 mod parse_filter;
+mod value_view;
 
 use proc_macro::TokenStream;
 
@@ -249,3 +251,15 @@ pub fn derive_display_filter(item: TokenStream) -> TokenStream {
     let input = syn::parse_macro_input!(item as syn::DeriveInput);
     filter::display::derive(&input).into()
 }
+
+#[proc_macro_derive(ValueView)]
+pub fn derive_value_view(item: TokenStream) -> TokenStream {
+    let input = syn::parse_macro_input!(item as syn::DeriveInput);
+    value_view::derive(&input).into()
+}
+
+#[proc_macro_derive(ObjectView)]
+pub fn derive_object_view(item: TokenStream) -> TokenStream {
+    let input = syn::parse_macro_input!(item as syn::DeriveInput);
+    object_view::derive(&input).into()
+}
diff --git a/crates/derive/src/object_view.rs b/crates/derive/src/object_view.rs
new file mode 100644
index 0000000..ef16af3
--- /dev/null
+++ b/crates/derive/src/object_view.rs
@@ -0,0 +1,113 @@
+use proc_macro2::*;
+use proc_quote::*;
+use syn::*;
+
+pub fn derive(input: &DeriveInput) -> TokenStream {
+    let DeriveInput {
+        ident,
+        data,
+        generics,
+        ..
+    } = input;
+
+    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
+
+    let fields = match get_fields(data) {
+        Ok(fields) => fields,
+        Err(err) => return err.to_compile_error(),
+    };
+    let num_fields = fields.len();
+
+    quote! {
+        impl #impl_generics ::liquid::ObjectView for #ident #ty_generics #where_clause {
+            fn size(&self) -> i32 {
+                #num_fields as i32
+            }
+
+            fn keys<'liquid_derive_k>(&'liquid_derive_k self) -> Box<dyn Iterator<Item = ::kstring::KStringCow<'liquid_derive_k>> + 'liquid_derive_k> {
+                let mut keys = Vec::with_capacity(#num_fields);
+                #(
+                    keys.push(::kstring::KStringCow::from_static(stringify!(#fields)));
+                )*
+                Box::new(keys.into_iter())
+            }
+
+            fn values<'liquid_derive_k>(&'liquid_derive_k self) -> Box<dyn Iterator<Item = &'liquid_derive_k dyn ::liquid::ValueView> + 'liquid_derive_k> {
+                let mut values = Vec::<&dyn ::liquid::ValueView>::with_capacity(#num_fields);
+                #(
+                    values.push(&self.#fields);
+                )*
+                Box::new(values.into_iter())
+            }
+
+            fn iter<'liquid_derive_k>(&'liquid_derive_k self) -> Box<dyn Iterator<Item = (::kstring::KStringCow<'liquid_derive_k>, &'liquid_derive_k dyn ::liquid::ValueView)> + 'liquid_derive_k> {
+                let mut values = Vec::<(::kstring::KStringCow<'liquid_derive_k>, &'liquid_derive_k dyn ::liquid::ValueView)>::with_capacity(#num_fields);
+                #(
+                    values.push((
+                        ::kstring::KStringCow::from_static(stringify!(#fields)),
+                        &self.#fields,
+                    ));
+                )*
+                Box::new(values.into_iter())
+            }
+
+            fn contains_key(&self, index: &str) -> bool {
+                match index {
+                    #(
+                        stringify!(#fields) => true,
+                    )*
+                    _ => false,
+                }
+            }
+
+            fn get<'liquid_derive_s>(&'liquid_derive_s self, index: &str) -> Option<&'liquid_derive_s dyn ::liquid::ValueView> {
+                match index {
+                    #(
+                        stringify!(#fields) => Some(&self.#fields),
+                    )*
+                    _ => None,
+                }
+            }
+        }
+    }
+}
+
+fn get_fields(data: &Data) -> Result<Vec<&Ident>> {
+    let fields = match data {
+        Data::Struct(data) => &data.fields,
+        Data::Enum(data) => {
+            return Err(Error::new_spanned(
+                data.enum_token,
+                "`ObjectView` support for `enum` is unimplemented.",
+            ));
+        }
+        Data::Union(data) => {
+            return Err(Error::new_spanned(
+                data.union_token,
+                "Unions cannot impl ObjectView.",
+            ));
+        }
+    };
+
+    let fields = match fields {
+        Fields::Named(fields) => fields,
+        Fields::Unnamed(fields) => {
+            return Err(Error::new_spanned(
+                fields,
+                "`ObjectView` support for tuple-structs is unimplemented.",
+            ))
+        }
+        Fields::Unit => {
+            return Err(Error::new_spanned(
+                fields,
+                "`ObjectView` support for unit-structs is unimplemented.",
+            ))
+        }
+    };
+
+    Ok(fields
+        .named
+        .iter()
+        .map(|field| field.ident.as_ref().expect("Fields are named."))
+        .collect())
+}
diff --git a/crates/derive/src/value_view.rs b/crates/derive/src/value_view.rs
new file mode 100644
index 0000000..5ea7786
--- /dev/null
+++ b/crates/derive/src/value_view.rs
@@ -0,0 +1,45 @@
+use proc_macro2::*;
+use proc_quote::*;
+use syn::*;
+
+pub fn derive(input: &DeriveInput) -> TokenStream {
+    let DeriveInput {
+        ident, generics, ..
+    } = input;
+
+    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
+
+    quote! {
+        impl #impl_generics ::liquid::ValueView for #ident #ty_generics #where_clause {
+            fn render(&self) -> ::liquid::value::DisplayCow<'_> {
+                ::liquid::value::DisplayCow::Owned(Box::new(::liquid::value::ObjectRender::new(self)))
+            }
+            fn source(&self) -> ::liquid::value::DisplayCow<'_> {
+                ::liquid::value::DisplayCow::Owned(Box::new(::liquid::value::ObjectSource::new(self)))
+            }
+            fn type_name(&self) -> &'static str {
+                "object"
+            }
+            fn query_state(&self, state: ::liquid::value::State) -> bool {
+                match state {
+                    ::liquid::value::State::Truthy => true,
+                    ::liquid::value::State::DefaultValue |
+                    ::liquid::value::State::Empty |
+                    ::liquid::value::State::Blank => self.size() == 0,
+                }
+            }
+
+            fn to_kstr(&self) -> ::kstring::KStringCow<'_> {
+                let s = ::liquid::value::ObjectRender::new(self).to_string();
+                ::kstring::KStringCow::from_string(s)
+            }
+            fn to_value(&self) -> ::liquid::value::Value {
+                ::liquid::value::to_value(self).unwrap()
+            }
+
+            fn as_object(&self) -> Option<&dyn ::liquid::ObjectView> {
+                Some(self)
+            }
+        }
+    }
+}
diff --git a/crates/value/src/object.rs b/crates/value/src/object.rs
index 8d10a3d..42dbafe 100644
--- a/crates/value/src/object.rs
+++ b/crates/value/src/object.rs
@@ -322,10 +322,18 @@ fn as_value<T: ValueView>(value: &T) -> &dyn ValueView {
 }
 
 #[derive(Debug)]
-struct ObjectSource<'s, O: ObjectView> {
+#[doc(hidden)]
+pub struct ObjectSource<'s, O: ObjectView> {
     s: &'s O,
 }
 
+impl<'s, O: ObjectView> ObjectSource<'s, O> {
+    #[doc(hidden)]
+    pub fn new(other: &'s O) -> Self {
+        Self { s: other }
+    }
+}
+
 impl<'s, O: ObjectView> fmt::Display for ObjectSource<'s, O> {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
         write!(f, "{{")?;
@@ -338,10 +346,18 @@ impl<'s, O: ObjectView> fmt::Display for ObjectSource<'s, O> {
 }
 
 #[derive(Debug)]
-struct ObjectRender<'s, O: ObjectView> {
+#[doc(hidden)]
+pub struct ObjectRender<'s, O: ObjectView> {
     s: &'s O,
 }
 
+impl<'s, O: ObjectView> ObjectRender<'s, O> {
+    #[doc(hidden)]
+    pub fn new(other: &'s O) -> Self {
+        Self { s: other }
+    }
+}
+
 impl<'s, O: ObjectView> fmt::Display for ObjectRender<'s, O> {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
         for (k, v) in self.s.iter() {
diff --git a/src/lib.rs b/src/lib.rs
index 6f93539..26767fa 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -29,6 +29,9 @@ pub mod filters;
 pub mod partials;
 pub mod tags;
 
+#[doc(hidden)]
+pub use liquid_core::value;
+
 pub use crate::parser::*;
 pub use crate::reflection::*;
 pub use crate::template::*;
@@ -37,6 +40,7 @@ pub use liquid_core::to_object;
 pub use liquid_core::Error;
 pub use liquid_core::Object;
 pub use liquid_core::{ObjectView, ValueView};
+pub use liquid_derive::{ObjectView, ValueView};
 
 #[macro_use]
 extern crate doc_comment;


Test patch:
diff --git a/tests/derive_object.rs b/tests/derive_object.rs
new file mode 100644
index 0000000..a6e64b6
--- /dev/null
+++ b/tests/derive_object.rs
@@ -0,0 +1,118 @@
+use liquid::ObjectView;
+use liquid::ValueView;
+
+#[derive(ObjectView, ValueView, serde::Serialize, serde::Deserialize, Debug)]
+struct TestEmpty {}
+
+#[test]
+fn test_empty_value() {
+    let uut = TestEmpty {};
+
+    assert_eq!(uut.render().to_string(), "");
+    assert_eq!(uut.source().to_string(), "{}");
+    assert_eq!(uut.type_name(), "object");
+    assert_eq!(uut.query_state(liquid::value::State::Truthy), true);
+    assert_eq!(uut.query_state(liquid::value::State::DefaultValue), true);
+    assert_eq!(uut.query_state(liquid::value::State::Empty), true);
+    assert_eq!(uut.query_state(liquid::value::State::Blank), true);
+    assert_eq!(uut.to_kstr(), "");
+    assert_eq!(uut.to_value(), liquid::value::value!({}));
+    assert!(uut.as_object().is_some());
+}
+
+#[test]
+fn test_empty_object() {
+    let uut = TestEmpty {};
+
+    assert_eq!(uut.size(), 0);
+    assert!(uut.keys().collect::<Vec<_>>().is_empty());
+    assert!(uut.values().collect::<Vec<_>>().is_empty());
+    assert!(uut.iter().collect::<Vec<_>>().is_empty());
+    assert_eq!(uut.contains_key("non-existent"), false);
+    assert!(uut.get("non-existent").is_none());
+}
+
+#[derive(ObjectView, ValueView, serde::Serialize, serde::Deserialize, Debug)]
+struct TestStatic {
+    boolean: bool,
+    int: i32,
+    float: f64,
+    static_str: &'static str,
+    string: String,
+    kstring: kstring::KString,
+    array: Vec<i32>,
+}
+
+impl TestStatic {
+    fn non_default() -> Self {
+        Self {
+            boolean: true,
+            int: 5,
+            float: 4.2,
+            static_str: "Hello world",
+            string: String::from("Goodbye world"),
+            kstring: kstring::KString::from_static("foo"),
+            array: vec![1, 2, 3],
+        }
+    }
+}
+
+#[test]
+fn test_static_value() {
+    let uut = TestStatic::non_default();
+
+    assert_ne!(uut.render().to_string(), "");
+    assert_ne!(uut.source().to_string(), "{}");
+    assert_eq!(uut.type_name(), "object");
+    assert_eq!(uut.query_state(liquid::value::State::Truthy), true);
+    assert_eq!(uut.query_state(liquid::value::State::DefaultValue), false);
+    assert_eq!(uut.query_state(liquid::value::State::Empty), false);
+    assert_eq!(uut.query_state(liquid::value::State::Blank), false);
+    assert_ne!(uut.to_kstr(), "");
+    assert_ne!(uut.to_value(), liquid::value::value!({}));
+    assert!(uut.as_object().is_some());
+}
+
+#[test]
+fn test_static_object() {
+    let uut = TestStatic::non_default();
+
+    assert_eq!(uut.size(), 7i32);
+    assert!(!uut.keys().collect::<Vec<_>>().is_empty());
+    assert!(!uut.values().collect::<Vec<_>>().is_empty());
+    assert!(!uut.iter().collect::<Vec<_>>().is_empty());
+    assert_eq!(uut.contains_key("non-existent"), false);
+    assert_eq!(uut.contains_key("boolean"), true);
+    assert!(uut.get("boolean").is_some());
+}
+
+#[derive(ObjectView, ValueView, serde::Serialize, serde::Deserialize, Debug)]
+struct TestBorrow<'s> {
+    s: &'s str,
+}
+
+#[test]
+fn test_borrow_value() {
+    let fixture = String::from("foo");
+    let uut = TestBorrow {
+        s: fixture.as_str(),
+    };
+
+    assert!(uut.as_object().is_some());
+}
+
+#[test]
+fn test_borrow_object() {
+    let fixture = String::from("foo");
+    let uut = TestBorrow {
+        s: fixture.as_str(),
+    };
+
+    assert_eq!(uut.size(), 1i32);
+    assert!(!uut.keys().collect::<Vec<_>>().is_empty());
+    assert!(!uut.values().collect::<Vec<_>>().is_empty());
+    assert!(!uut.iter().collect::<Vec<_>>().is_empty());
+    assert_eq!(uut.contains_key("non-existent"), false);
+    assert_eq!(uut.contains_key("s"), true);
+    assert!(uut.get("s").is_some());
+}