Repository: cobalt-org/liquid-rust
Pull request #91 (2017-05-02 03:04:35)

feat(value): Add serde support
This should open it up for clients to easily serialize to/from the formats
they care about.


Patch:
diff --git a/Cargo.toml b/Cargo.toml
index c0d91d1..1c3f597 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -19,6 +19,8 @@ lazy_static = "0.2"
 chrono = "0.3"
 unicode-segmentation = "1.1"
 itertools = "0.6.0"
+serde = "1.0"
+serde_derive = "1.0"
 
 [build-dependencies]
 skeptic = "0.9"
@@ -26,6 +28,7 @@ skeptic = "0.9"
 [dev-dependencies]
 difference = "1.0"
 skeptic = "0.9"
+serde_yaml = "0.7"
 
 [features]
 default = ["extra-filters"]
diff --git a/src/context.rs b/src/context.rs
index 1c47584..a6c4634 100644
--- a/src/context.rs
+++ b/src/context.rs
@@ -2,7 +2,7 @@ use error::{Result, Error};
 use filters::Filter;
 use std::collections::HashMap;
 use token::Token::{self, Identifier, StringLiteral, NumberLiteral, BooleanLiteral};
-use value::Value;
+use value::{Value, Object};
 
 
 #[derive(Clone)]
@@ -11,12 +11,10 @@ pub enum Interrupt {
     Break,
 }
 
-type ValueMap = HashMap<String, Value>;
-
 #[derive(Default)]
 pub struct Context {
-    stack: Vec<ValueMap>,
-    globals: ValueMap,
+    stack: Vec<Object>,
+    globals: Object,
 
     /// The current interrupt state. The interrupt state is used by
     /// the `break` and `continue` tags to halt template rendering
@@ -43,22 +41,22 @@ impl Context {
     /// assert_eq!(ctx.get_val("test"), None);
     /// ```
     pub fn new() -> Context {
-        Context::with_values_and_filters(HashMap::new(), HashMap::new())
+        Context::with_values_and_filters(Object::new(), HashMap::new())
     }
 
-    pub fn with_values(values: HashMap<String, Value>) -> Context {
+    pub fn with_values(values: Object) -> Context {
         Context::with_values_and_filters(values, HashMap::new())
     }
 
     pub fn with_filters(filters: HashMap<String, Box<Filter>>) -> Context {
-        Context::with_values_and_filters(HashMap::new(), filters)
+        Context::with_values_and_filters(Object::new(), filters)
     }
 
-    pub fn with_values_and_filters(values: HashMap<String, Value>,
+    pub fn with_values_and_filters(values: Object,
                                    filters: HashMap<String, Box<Filter>>)
                                    -> Context {
         Context {
-            stack: vec![HashMap::new()],
+            stack: vec![Object::new()],
             interrupt: None,
             cycles: HashMap::new(),
             globals: values,
@@ -116,7 +114,7 @@ impl Context {
 
     /// Creates a new variable scope chained to a parent scope.
     fn push_scope(&mut self) {
-        self.stack.push(HashMap::new());
+        self.stack.push(Object::new());
     }
 
     /// Removes the topmost stack frame from the local variable stack.
diff --git a/src/lib.rs b/src/lib.rs
index 28e2777..86fe6ba 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -31,13 +31,18 @@
 #![allow(unknown_lints)]
 #![allow(zero_ptr)]
 
-#[macro_use]
-extern crate lazy_static;
 extern crate regex;
 extern crate chrono;
 extern crate unicode_segmentation;
 extern crate itertools;
 
+#[macro_use]
+extern crate lazy_static;
+
+extern crate serde;
+#[macro_use]
+extern crate serde_derive;
+
 use std::collections::HashMap;
 use lexer::Element;
 use tags::{assign_tag, cycle_tag, include_tag, break_tag, continue_tag, comment_block, raw_block,
@@ -49,6 +54,8 @@ use std::path::{PathBuf, Path};
 use error::Result;
 
 pub use value::Value;
+pub use value::Object;
+pub use value::Array;
 pub use context::Context;
 pub use template::Template;
 pub use error::Error;
diff --git a/src/value.rs b/src/value.rs
index 5cb7c41..9261e6e 100644
--- a/src/value.rs
+++ b/src/value.rs
@@ -8,10 +8,12 @@ use token::Token::*;
 
 /// An enum to represent different value types
 #[derive(Clone, Debug)]
+#[derive(Serialize, Deserialize)]
+#[serde(untagged)]
 pub enum Value {
-    Str(String),
     Num(f32),
     Bool(bool),
+    Str(String),
     Array(Array),
     Object(Object),
 }
@@ -40,19 +42,6 @@ impl<'a> Value {
         }
     }
 
-    /// Extracts the str value if it is a str.
-    pub fn as_str(&'a self) -> Option<&'a str> {
-        match *self {
-            Value::Str(ref v) => Some(v),
-            _ => None,
-        }
-    }
-
-    /// Tests whether this value is a str
-    pub fn is_str(&self) -> bool {
-        self.as_str().is_some()
-    }
-
     /// Extracts the float value if it is a float.
     pub fn as_float(&self) -> Option<f32> {
         match *self {
@@ -79,6 +68,19 @@ impl<'a> Value {
         self.as_bool().is_some()
     }
 
+    /// Extracts the str value if it is a str.
+    pub fn as_str(&'a self) -> Option<&'a str> {
+        match *self {
+            Value::Str(ref v) => Some(v),
+            _ => None,
+        }
+    }
+
+    /// Tests whether this value is a str
+    pub fn is_str(&self) -> bool {
+        self.as_str().is_some()
+    }
+
     /// Extracts the array value if it is an array.
     pub fn as_array(&self) -> Option<&Vec<Value>> {
         match *self {
@@ -125,9 +127,9 @@ impl<'a> Value {
 impl PartialEq<Value> for Value {
     fn eq(&self, other: &Value) -> bool {
         match (self, other) {
-            (&Value::Str(ref x), &Value::Str(ref y)) => x == y,
             (&Value::Num(x), &Value::Num(y)) => x == y,
             (&Value::Bool(x), &Value::Bool(y)) => x == y,
+            (&Value::Str(ref x), &Value::Str(ref y)) => x == y,
             (&Value::Array(ref x), &Value::Array(ref y)) => x == y,
             (&Value::Object(ref x), &Value::Object(ref y)) => x == y,
 
@@ -146,9 +148,9 @@ impl Eq for Value {}
 impl PartialOrd<Value> for Value {
     fn partial_cmp(&self, other: &Value) -> Option<Ordering> {
         match (self, other) {
-            (&Value::Str(ref x), &Value::Str(ref y)) => x.partial_cmp(y),
             (&Value::Num(x), &Value::Num(y)) => x.partial_cmp(&y),
             (&Value::Bool(x), &Value::Bool(y)) => x.partial_cmp(&y),
+            (&Value::Str(ref x), &Value::Str(ref y)) => x.partial_cmp(y),
             (&Value::Array(ref x), &Value::Array(ref y)) => x.iter().partial_cmp(y.iter()),
             (&Value::Object(ref x), &Value::Object(ref y)) => x.iter().partial_cmp(y.iter()),
             _ => None,
@@ -159,9 +161,9 @@ impl PartialOrd<Value> for Value {
 impl ToString for Value {
     fn to_string(&self) -> String {
         match *self {
-            Value::Str(ref x) => x.to_owned(),
             Value::Num(ref x) => x.to_string(),
             Value::Bool(ref x) => x.to_string(),
+            Value::Str(ref x) => x.to_owned(),
             Value::Array(ref x) => {
                 let arr: Vec<String> = x.iter().map(|v| v.to_string()).collect();
                 arr.join(", ")
@@ -190,21 +192,21 @@ mod test {
     static FALSE: Value = Value::Bool(false);
 
     #[test]
-    fn test_as_str() {
+    fn test_num_to_string() {
         let val = Value::Num(42f32);
-        assert_eq!(val.as_str(), None);
+        assert_eq!(&val.to_string(), "42");
 
-        let val = Value::str("test");
-        assert_eq!(val.as_str(), Some("test"));
+        let val = Value::Num(42.34);
+        assert_eq!(&val.to_string(), "42.34");
     }
 
     #[test]
-    fn test_num_to_string() {
+    fn test_as_str() {
         let val = Value::Num(42f32);
-        assert_eq!(&val.to_string(), "42");
+        assert_eq!(val.as_str(), None);
 
-        let val = Value::Num(42.34);
-        assert_eq!(&val.to_string(), "42.34");
+        let val = Value::str("test");
+        assert_eq!(val.as_str(), Some("test"));
     }
 
     #[test]


Test patch:
diff --git a/tests/value.rs b/tests/value.rs
new file mode 100644
index 0000000..130a34c
--- /dev/null
+++ b/tests/value.rs
@@ -0,0 +1,131 @@
+extern crate liquid;
+extern crate serde_yaml;
+
+#[macro_use]
+extern crate difference;
+
+use std::f32;
+
+#[test]
+pub fn serialize_num() {
+    let actual = liquid::Value::Num(1f32);
+    let actual = serde_yaml::to_string(&actual).unwrap();
+    assert_diff!(&actual, "---\n1", "", 0);
+
+    let actual = liquid::Value::Num(-100f32);
+    let actual = serde_yaml::to_string(&actual).unwrap();
+    assert_diff!(&actual, "---\n-100", "", 0);
+
+    let actual = liquid::Value::Num(3.14e10f32);
+    let actual = serde_yaml::to_string(&actual).unwrap();
+    assert_diff!(&actual, "---\n31399999488", "", 0);
+
+    let actual = liquid::Value::Num(f32::NAN);
+    let actual = serde_yaml::to_string(&actual).unwrap();
+    assert_diff!(&actual, "---\nNaN", "", 0);
+
+    let actual = liquid::Value::Num(f32::INFINITY);
+    let actual = serde_yaml::to_string(&actual).unwrap();
+    assert_diff!(&actual, "---\ninf", "", 0);
+}
+
+#[test]
+pub fn deserialize_num() {
+    let actual: liquid::Value = serde_yaml::from_str("---\n1").unwrap();
+    assert_eq!(actual, liquid::Value::Num(1f32));
+
+    let actual: liquid::Value = serde_yaml::from_str("---\n-100").unwrap();
+    assert_eq!(actual, liquid::Value::Num(-100f32));
+
+    let actual: liquid::Value = serde_yaml::from_str("---\n31399999488").unwrap();
+    assert_eq!(actual, liquid::Value::Num(3.14e10f32));
+
+    // Skipping NaN since equality fails
+
+    let actual: liquid::Value = serde_yaml::from_str("---\ninf").unwrap();
+    assert_eq!(actual, liquid::Value::Num(f32::INFINITY));
+}
+
+#[test]
+pub fn serialize_bool() {
+    let actual = liquid::Value::Bool(true);
+    let actual = serde_yaml::to_string(&actual).unwrap();
+    assert_diff!(&actual, "---\ntrue", "", 0);
+
+    let actual = liquid::Value::Bool(false);
+    let actual = serde_yaml::to_string(&actual).unwrap();
+    assert_diff!(&actual, "---\nfalse", "", 0);
+}
+
+#[test]
+pub fn deserialize_bool() {
+    let actual: liquid::Value = serde_yaml::from_str("---\ntrue").unwrap();
+    assert_eq!(actual, liquid::Value::Bool(true));
+
+    let actual: liquid::Value = serde_yaml::from_str("---\nfalse").unwrap();
+    assert_eq!(actual, liquid::Value::Bool(false));
+}
+
+#[test]
+pub fn serialize_str() {
+    let actual = liquid::Value::str("Hello");
+    let actual = serde_yaml::to_string(&actual).unwrap();
+    assert_diff!(&actual, "---\nHello", "", 0);
+
+    let actual = liquid::Value::str("10");
+    let actual = serde_yaml::to_string(&actual).unwrap();
+    assert_diff!(&actual, "---\n\"10\"", "", 0);
+
+    let actual = liquid::Value::str("false");
+    let actual = serde_yaml::to_string(&actual).unwrap();
+    assert_diff!(&actual, "---\n\"false\"", "", 0);
+}
+
+#[test]
+pub fn deserialize_str() {
+    let actual: liquid::Value = serde_yaml::from_str("---\nHello").unwrap();
+    assert_eq!(actual, liquid::Value::str("Hello"));
+
+    let actual: liquid::Value = serde_yaml::from_str("\"10\"\n").unwrap();
+    assert_eq!(actual, liquid::Value::str("10"));
+
+    let actual: liquid::Value = serde_yaml::from_str("---\n\"false\"").unwrap();
+    assert_eq!(actual, liquid::Value::str("false"));
+}
+
+#[test]
+pub fn serialize_array() {
+    let actual =
+        vec![liquid::Value::Num(1f32), liquid::Value::Bool(true), liquid::Value::str("true")];
+    let actual = liquid::Value::Array(actual);
+    let actual = serde_yaml::to_string(&actual).unwrap();
+    assert_diff!(&actual, "---\n- 1\n- true\n- \"true\"", "", 0);
+}
+
+#[test]
+pub fn deserialize_array() {
+    let actual: liquid::Value = serde_yaml::from_str("---\n- 1\n- true\n- \"true\"").unwrap();
+    let expected =
+        vec![liquid::Value::Num(1f32), liquid::Value::Bool(true), liquid::Value::str("true")];
+    let expected = liquid::Value::Array(expected);
+    assert_eq!(actual, expected);
+}
+
+#[test]
+pub fn serialize_object() {
+    // Skipping due to HashMap ordering issues
+}
+
+#[test]
+pub fn deserialize_object() {
+    let actual: liquid::Value = serde_yaml::from_str("---\nNum: 1\nBool: true\nStr: \"true\"")
+        .unwrap();
+    let expected: liquid::Object = [("Num".to_owned(), liquid::Value::Num(1f32)),
+                                    ("Bool".to_owned(), liquid::Value::Bool(true)),
+                                    ("Str".to_owned(), liquid::Value::str("true"))]
+        .iter()
+        .cloned()
+        .collect();
+    let expected = liquid::Value::Object(expected);
+    assert_eq!(actual, expected);
+}