From aa3ed99fcaaa540d16ced6c1a63fea985712ef33 Mon Sep 17 00:00:00 2001 From: Tom Alexander Date: Tue, 29 Dec 2020 17:51:46 -0500 Subject: [PATCH 1/3] Create a json-integration feature flag and disable building the CLI program without it. --- Cargo.toml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 189129c..3814777 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,10 @@ version = "0.1.0" authors = ["Tom Alexander "] edition = "2018" +[features] +default = [] +json-integration = [] + [lib] name = "duster" path = "src/lib.rs" @@ -11,6 +15,7 @@ path = "src/lib.rs" [[bin]] name = "duster-cli" path = "src/bin.rs" +required-features = ["json-integration"] [dependencies] nom = { git = "https://github.com/tomalexander/nom.git", branch = "take_until_parser_matches" } From a9a83d1b4ad9964b6ce5d9289590e93ab95dd1ae Mon Sep 17 00:00:00 2001 From: Tom Alexander Date: Tue, 29 Dec 2020 18:07:49 -0500 Subject: [PATCH 2/3] Update for the latest nom and make serde an optional dep. --- Cargo.toml | 6 +++--- src/integrations/json.rs | 6 ++++++ src/parser/parser.rs | 27 ++++++++++++++++++--------- 3 files changed, 27 insertions(+), 12 deletions(-) create mode 100644 src/integrations/json.rs diff --git a/Cargo.toml b/Cargo.toml index 3814777..848c1a5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ edition = "2018" [features] default = [] -json-integration = [] +json-integration = ["serde", "serde_json"] [lib] name = "duster" @@ -19,5 +19,5 @@ required-features = ["json-integration"] [dependencies] nom = { git = "https://github.com/tomalexander/nom.git", branch = "take_until_parser_matches" } -serde = "1.0.106" -serde_json = "1.0.51" +serde = { version = "1.0.106", optional = true } +serde_json = { version = "1.0.51", optional = true } diff --git a/src/integrations/json.rs b/src/integrations/json.rs new file mode 100644 index 0000000..8242fb2 --- /dev/null +++ b/src/integrations/json.rs @@ -0,0 +1,6 @@ +//! This file contains an integration for duster that implements +//! support for using serde_json values for the render context. This +//! is in its own separate file to avoid requiring serde as a +//! dependency since ContextElement can be implemented for any +//! type. Disable the json-integration feature to avoid compiling this +//! file and adding the serde and serde_json dependencies. diff --git a/src/parser/parser.rs b/src/parser/parser.rs index e35a44b..880f722 100644 --- a/src/parser/parser.rs +++ b/src/parser/parser.rs @@ -807,7 +807,10 @@ mod tests { assert_eq!(super::special("{~rb}"), Ok(("", Special::RightCurlyBrace))); assert_eq!( super::special("{~zzz}"), - Err(Error(("zzz}", ErrorKind::Tag))) + Err(Error(nom::error::Error { + input: "zzz}", + code: ErrorKind::Tag + })) ); } @@ -824,10 +827,10 @@ mod tests { ); assert_eq!( super::special("{! this is a comment without a close"), - Err(Error(( - "{! this is a comment without a close", - ErrorKind::Tag - ))) + Err(Error(nom::error::Error { + input: "{! this is a comment without a close", + code: ErrorKind::Tag + })) ); } @@ -861,7 +864,10 @@ mod tests { ); assert_eq!( super::span("{~lb}"), - Err(Error(("{~lb}", ErrorKind::Verify))) + Err(Error(nom::error::Error { + input: "{~lb}", + code: ErrorKind::Verify + })) ); assert_eq!( super::body("this is \t \n\n \t \n \t multiline text\n {foo}"), @@ -911,7 +917,10 @@ mod tests { fn test_section_mismatched_paths() { assert_eq!( super::dust_tag("{#foo.bar}{/baz}"), - Err(Error(("{#foo.bar}{/baz}", ErrorKind::Tag))) + Err(Error(nom::error::Error { + input: "{#foo.bar}{/baz}", + code: ErrorKind::Tag + })) ); } @@ -1539,7 +1548,7 @@ mod tests { {.} {/names}" ), - Ok::<_, nom::Err<(&str, ErrorKind)>>(( + Ok::<_, nom::Err>>(( "", Template { contents: Body { @@ -1613,7 +1622,7 @@ mod tests { super::template( r#"{#level3.level4}{>partialtwo v1="b" v2="b" v3="b" v4="b" v5="b" /}{/level3.level4}"# ), - Ok::<_, nom::Err<(&str, ErrorKind)>>(( + Ok::<_, nom::Err>>(( "", Template { contents: Body { From 900d929869276a39cb1046b1e41d394cff75d19a Mon Sep 17 00:00:00 2001 From: Tom Alexander Date: Tue, 29 Dec 2020 18:21:12 -0500 Subject: [PATCH 3/3] Move the json integration to its own file to keep the serde stuff separate. --- Cargo.toml | 2 +- src/bin.rs | 697 +------------------------------------- src/integrations/json.rs | 700 +++++++++++++++++++++++++++++++++++++++ src/integrations/mod.rs | 2 + 4 files changed, 704 insertions(+), 697 deletions(-) create mode 100644 src/integrations/mod.rs diff --git a/Cargo.toml b/Cargo.toml index 848c1a5..909e12f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ authors = ["Tom Alexander "] edition = "2018" [features] -default = [] +default = ["json-integration"] json-integration = ["serde", "serde_json"] [lib] diff --git a/src/bin.rs b/src/bin.rs index a614f88..465188c 100644 --- a/src/bin.rs +++ b/src/bin.rs @@ -1,33 +1,15 @@ extern crate nom; -use crate::renderer::CompareContextElement; -use parser::Filter; -use parser::OwnedLiteral; use parser::Template; -use renderer::compare_json_numbers; use renderer::compile_template; -use renderer::Castable; -use renderer::ComparisonNumber; use renderer::CompileError; -use renderer::ContextElement; use renderer::DustRenderer; -use renderer::IceResult; -use renderer::IntoContextElement; -use renderer::Loopable; -use renderer::MathNumber; -use renderer::RenderError; -use renderer::Renderable; -use renderer::Sizable; -use renderer::Truthiness; -use renderer::WalkError; -use renderer::Walkable; -use std::cmp::Ordering; -use std::convert::TryInto; use std::env; use std::fs; use std::io::{self, Read}; use std::path::Path; +mod integrations; mod parser; mod renderer; @@ -90,680 +72,3 @@ fn read_context_from_stdin() -> serde_json::Value { serde_json::from_str(&buffer).expect("Failed to parse json") } - -fn html_escape(inp: &str) -> String { - // Adding 10% space from the original to avoid re-allocations by - // leaving room for escaped sequences. - let mut output = String::with_capacity(((inp.len() as f64) * 1.1) as usize); - inp.chars().for_each(|c| match c { - '<' => output.push_str("<"), - '>' => output.push_str(">"), - '"' => output.push_str("""), - '\'' => output.push_str("'"), - '&' => output.push_str("&"), - _ => output.push(c), - }); - output -} - -fn javascript_escape(inp: &str) -> String { - // Adding 10% space from the original to avoid re-allocations by - // leaving room for escaped sequences. - let mut output = String::with_capacity(((inp.len() as f64) * 1.1) as usize); - inp.chars().for_each(|c| match c { - '"' => output.push_str(r#"\""#), - '\'' => output.push_str(r#"\'"#), - '\t' => output.push_str(r#"\t"#), - '\x0C' => output.push_str(r#"\f"#), - '\n' => output.push_str(r#"\n"#), - '\r' => output.push_str(r#"\r"#), - '\\' => output.push_str(r#"\\"#), - '/' => output.push_str(r#"\/"#), - _ => output.push(c), - }); - output -} - -fn get_utf8_hex(inp: char) -> String { - let num_bytes = inp.len_utf8(); - let mut byte_buffer = [0; 4]; // UTF-8 supports up to 4 bytes per codepoint - let mut output = String::with_capacity(num_bytes * 2); - - inp.encode_utf8(&mut byte_buffer); - - for b in &byte_buffer[..num_bytes] { - output.push_str(&format!("{:02X}", b)); - } - - output -} - -fn encode_uri(inp: &str) -> String { - // Adding 10% space from the original to avoid re-allocations by - // leaving room for escaped sequences. - let mut output = String::with_capacity(((inp.len() as f64) * 1.1) as usize); - inp.chars().for_each(|c| match c { - '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | 'a' | 'b' | 'c' | 'd' | 'e' - | 'f' | 'g' | 'h' | 'i' | 'j' | 'k' | 'l' | 'm' | 'n' | 'o' | 'p' | 'q' | 'r' | 's' - | 't' | 'u' | 'v' | 'w' | 'x' | 'y' | 'z' | 'A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'G' - | 'H' | 'I' | 'J' | 'K' | 'L' | 'M' | 'N' | 'O' | 'P' | 'Q' | 'R' | 'S' | 'T' | 'U' - | 'V' | 'W' | 'X' | 'Y' | 'Z' | ';' | ',' | '/' | '?' | ':' | '@' | '&' | '=' | '+' - | '$' | '-' | '_' | '.' | '!' | '~' | '*' | '\'' | '(' | ')' | '#' => output.push(c), - _ => { - output.push('%'); - output.push_str(&get_utf8_hex(c)); - } - }); - output -} - -fn encode_uri_component(inp: &str) -> String { - // Adding 10% space from the original to avoid re-allocations by - // leaving room for escaped sequences. - let mut output = String::with_capacity(((inp.len() as f64) * 1.1) as usize); - inp.chars().for_each(|c| match c { - '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | 'a' | 'b' | 'c' | 'd' | 'e' - | 'f' | 'g' | 'h' | 'i' | 'j' | 'k' | 'l' | 'm' | 'n' | 'o' | 'p' | 'q' | 'r' | 's' - | 't' | 'u' | 'v' | 'w' | 'x' | 'y' | 'z' | 'A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'G' - | 'H' | 'I' | 'J' | 'K' | 'L' | 'M' | 'N' | 'O' | 'P' | 'Q' | 'R' | 'S' | 'T' | 'U' - | 'V' | 'W' | 'X' | 'Y' | 'Z' | '-' | '_' | '.' | '!' | '~' | '*' | '\'' | '(' | ')' => { - output.push(c) - } - _ => { - output.push('%'); - output.push_str(&get_utf8_hex(c)); - } - }); - output -} - -fn apply_filter( - json_value: &serde_json::Value, - filter: &Filter, -) -> Result { - match (json_value, filter) { - // Html escape filter - (serde_json::Value::String(string), Filter::HtmlEncode) => { - Ok(serde_json::Value::String(html_escape(string))) - } - (_, Filter::HtmlEncode) => Ok(serde_json::Value::String(html_escape( - &json_value.render(&Vec::new())?, - ))), - // Disable html escape filter - (_, Filter::DisableHtmlEncode) => panic!("The |s filter is automatically removed by the renderer since it is a no-op during rendering."), - // Parse JSON filter - (serde_json::Value::String(string), Filter::JsonParse) => { - serde_json::from_str(&string).or(Err(RenderError::InvalidJson(string.to_owned()))) - } - (_, Filter::JsonParse) => { - let rendered_value = json_value.render(&Vec::new())?; - serde_json::from_str(&rendered_value).or(Err(RenderError::InvalidJson(rendered_value))) - } - // Json Stringify filter - (_, Filter::JsonStringify) => { - Ok(serde_json::Value::String(json_value.to_string())) - } - // Javascript escape filter - (serde_json::Value::String(string), Filter::JavascriptStringEncode) => { - Ok(serde_json::Value::String(javascript_escape(string))) - } - (serde_json::Value::Bool(boolean), Filter::JavascriptStringEncode) => { - Ok(serde_json::Value::Bool(*boolean)) - } - (serde_json::Value::Number(number), Filter::JavascriptStringEncode) => { - Ok(serde_json::Value::Number(number.clone())) - } - (serde_json::Value::Array(arr), Filter::JavascriptStringEncode) => { - Ok(serde_json::Value::Array(arr.clone())) - } - (serde_json::Value::Object(obj), Filter::JavascriptStringEncode) => { - Ok(serde_json::Value::Object(obj.clone())) - } - (_, Filter::JavascriptStringEncode) => Ok(serde_json::Value::String(javascript_escape( - &json_value.render(&Vec::new())?, - ))), - // EncodeURI filter - (serde_json::Value::String(string), Filter::EncodeUri) => { - Ok(serde_json::Value::String(encode_uri(string))) - } - (_, Filter::EncodeUri) => Ok(serde_json::Value::String(encode_uri( - &json_value.render(&Vec::new())?, - ))), - // EncodeURIComponent filter - (serde_json::Value::String(string), Filter::EncodeUriComponent) => { - Ok(serde_json::Value::String(encode_uri_component(string))) - } - (_, Filter::EncodeUriComponent) => Ok(serde_json::Value::String(encode_uri_component( - &json_value.render(&Vec::new())?, - ))), - } -} - -fn apply_filters( - json_value: &serde_json::Value, - filters: &[Filter], -) -> Result { - let mut final_value: serde_json::Value = apply_filter(json_value, &filters[0])?; - - for filter in &filters[1..] { - final_value = apply_filter(&final_value, filter)?; - } - - Ok(final_value) -} - -impl ContextElement for serde_json::Value {} - -impl Truthiness for serde_json::Value { - fn is_truthy(&self) -> bool { - match self { - serde_json::Value::Null => false, - serde_json::Value::Bool(boolean) => *boolean, - serde_json::Value::Number(_num) => true, - serde_json::Value::String(string_value) => !string_value.is_empty(), - serde_json::Value::Array(array_value) => !array_value.is_empty(), - serde_json::Value::Object(_obj) => true, - } - } -} - -impl Renderable for serde_json::Value { - fn render(&self, _filters: &Vec) -> Result { - let after_apply = if _filters.is_empty() { - None - } else { - Some(apply_filters(self, _filters)?) - }; - - match after_apply.as_ref().unwrap_or(self) { - serde_json::Value::Null => Ok("".to_owned()), - serde_json::Value::Bool(boolean) => Ok(boolean.to_string()), - serde_json::Value::Number(num) => Ok(num.to_string()), - serde_json::Value::String(string) => Ok(string.to_string()), - serde_json::Value::Array(arr) => { - let rendered: Result, RenderError> = - arr.iter().map(|val| val.render(&Vec::new())).collect(); - let rendered_slice: &[String] = &rendered?; - Ok(rendered_slice.join(",")) - } - serde_json::Value::Object(_obj) => Ok("[object Object]".to_owned()), - } - } -} - -impl Walkable for serde_json::Value { - fn walk(&self, segment: &str) -> Result<&dyn IntoContextElement, WalkError> { - match self { - serde_json::Value::Null => Err(WalkError::CantWalk), - serde_json::Value::Bool(_boolean) => Err(WalkError::CantWalk), - serde_json::Value::Number(_num) => Err(WalkError::CantWalk), - serde_json::Value::String(_string) => Err(WalkError::CantWalk), - serde_json::Value::Array(_arr) => Err(WalkError::CantWalk), - serde_json::Value::Object(obj) => obj - .get(segment) - .map(|val| val as _) - .ok_or(WalkError::CantWalk), - } - } -} - -impl Loopable for serde_json::Value { - fn get_loop_elements(&self) -> Vec<&dyn ContextElement> { - match self { - serde_json::Value::Array(array_value) => array_value.iter().map(|x| x as _).collect(), - _ => Vec::new(), - } - } -} - -impl Sizable for serde_json::Value { - fn is_castable(&self) -> bool { - match self { - serde_json::Value::Null => true, - serde_json::Value::Bool(_) => false, - serde_json::Value::Number(_) => true, - serde_json::Value::String(_) => true, - serde_json::Value::Array(_) => true, - serde_json::Value::Object(_) => true, - } - } - - fn get_size<'a>(&'a self) -> Option> { - match self { - serde_json::Value::Null => { - Some(IceResult::from_owned(OwnedLiteral::LPositiveInteger(0))) - } - serde_json::Value::Bool(_boolean) => { - Some(IceResult::from_owned(OwnedLiteral::LPositiveInteger(0))) - } - serde_json::Value::Number(_num) => Some(IceResult::from_borrowed(self)), - serde_json::Value::String(text) => Some(IceResult::from_owned( - OwnedLiteral::LPositiveInteger(text.len().try_into().unwrap()), - )), - serde_json::Value::Array(arr) => Some(IceResult::from_owned( - OwnedLiteral::LPositiveInteger(arr.len().try_into().unwrap()), - )), - serde_json::Value::Object(obj) => Some(IceResult::from_owned( - OwnedLiteral::LPositiveInteger(obj.len().try_into().unwrap()), - )), - } - } -} - -impl Castable for serde_json::Value { - fn cast_to_type<'a>(&'a self, target: &str) -> Option> { - match (self, target) { - (serde_json::Value::String(text), "number") => text - .parse::() - .map(|num| IceResult::from_owned(OwnedLiteral::LPositiveInteger(num))) - .or_else(|_| { - text.parse::() - .map(|num| IceResult::from_owned(OwnedLiteral::LNegativeInteger(num))) - }) - .or_else(|_| { - text.parse::() - .map(|num| IceResult::from_owned(OwnedLiteral::LFloat(num))) - }) - .ok(), - (serde_json::Value::Number(_), "number") => Some(IceResult::from_borrowed(self)), - (serde_json::Value::Null, "number") => { - Some(IceResult::from_owned(serde_json::Value::Number(0.into()))) - } - (serde_json::Value::Bool(boolean), "number") => { - if *boolean { - Some(IceResult::from_owned(serde_json::Value::Number(1.into()))) - } else { - Some(IceResult::from_owned(serde_json::Value::Number(0.into()))) - } - } - (serde_json::Value::Array(_), "number") => None, - (serde_json::Value::Object(_), "number") => None, - - (serde_json::Value::String(_), "string") => Some(IceResult::from_borrowed(self)), - (serde_json::Value::Number(num), "string") => Some(IceResult::from_owned( - serde_json::Value::String(num.to_string()), - )), - (serde_json::Value::Null, "string") => Some(IceResult::from_owned( - serde_json::Value::String("null".to_owned()), - )), - (serde_json::Value::Bool(boolean), "string") => Some(IceResult::from_owned( - serde_json::Value::String(boolean.to_string()), - )), - (serde_json::Value::Array(_), "string") => Some(IceResult::from_owned( - serde_json::Value::String(self.render(&Vec::new()).unwrap_or("".to_owned())), - )), - (serde_json::Value::Object(_), "string") => Some(IceResult::from_owned( - serde_json::Value::String(self.render(&Vec::new()).unwrap_or("".to_owned())), - )), - - (serde_json::Value::String(text), "boolean") => { - if text.is_empty() { - Some(IceResult::from_owned(serde_json::Value::Bool(false))) - } else { - Some(IceResult::from_owned(serde_json::Value::Bool(true))) - } - } - (serde_json::Value::Number(json_num), "boolean") => { - Some(IceResult::from_owned(serde_json::Value::Bool( - match (json_num.as_u64(), json_num.as_i64(), json_num.as_f64()) { - (Some(num), _, _) => num != 0, - (_, Some(num), _) => num != 0, - (_, _, Some(num)) => num != 0.0 && !num.is_nan(), - _ => false, - }, - ))) - } - (serde_json::Value::Null, "boolean") => { - Some(IceResult::from_owned(serde_json::Value::Bool(false))) - } - (serde_json::Value::Bool(_), "boolean") => Some(IceResult::from_borrowed(self)), - (serde_json::Value::Array(_), "boolean") => { - Some(IceResult::from_owned(serde_json::Value::Bool(true))) - } - (serde_json::Value::Object(_), "boolean") => { - Some(IceResult::from_owned(serde_json::Value::Bool(true))) - } - - (_, _) => panic!("Unimplemented cast"), - } - } -} - -impl CompareContextElement for serde_json::Value { - fn equals(&self, other: &dyn ContextElement) -> bool { - // println!("Json equality check {:?} == {:?}", self, other); - // Handle other serde_json::Value - match other.to_any().downcast_ref::() { - None => (), - Some(other_json_value) => match (self, other_json_value) { - // Non-scalar values not caught in the renderer by the - // identical-path shortcut are always not equal. - (serde_json::Value::Array(_), _) - | (_, serde_json::Value::Array(_)) - | (serde_json::Value::Object(_), _) - | (_, serde_json::Value::Object(_)) => return false, - _ => return self == other_json_value, - }, - } - // Handle literals - match other.to_any().downcast_ref::() { - None => (), - Some(OwnedLiteral::LString(other_string)) => { - return self.as_str().map_or(false, |s| s == other_string) - } - Some(OwnedLiteral::LBoolean(boolean)) => { - return self.equals(&serde_json::Value::Bool(*boolean) as &dyn ContextElement); - } - Some(OwnedLiteral::LPositiveInteger(other_num)) => { - let other_json_num: serde_json::Number = std::convert::From::from(*other_num); - return self - .equals(&serde_json::Value::Number(other_json_num) as &dyn ContextElement); - } - Some(OwnedLiteral::LNegativeInteger(other_num)) => { - let other_json_num: serde_json::Number = std::convert::From::from(*other_num); - return self - .equals(&serde_json::Value::Number(other_json_num) as &dyn ContextElement); - } - Some(OwnedLiteral::LFloat(other_num)) => match self.as_f64() { - None => return false, - Some(self_float) => return self_float == *other_num, - }, - } - false - } - - fn partial_compare(&self, other: &dyn ContextElement) -> Option { - // Handle type coerced objects - - // When doing a greater than or less than comparison, - // javascript coerces objects into "[object Object]". - if let serde_json::Value::Object(_) = self { - return OwnedLiteral::LString(self.render(&Vec::new()).unwrap_or("".to_owned())) - .partial_compare(other); - } - - // When doing a greater than or less than comparison - // javascript turns arrays into strings. - if let serde_json::Value::Array(_) = self { - return OwnedLiteral::LString(self.render(&Vec::new()).unwrap_or("".to_owned())) - .partial_compare(other); - } - - let maybe_json_other = other.to_any().downcast_ref::(); - let maybe_literal_other = other.to_any().downcast_ref::(); - - // If they're both strings, compare them directly - match (self, maybe_json_other, maybe_literal_other) { - // If they're both strings, compare them directly - ( - serde_json::Value::String(self_string), - Some(serde_json::Value::String(other_string)), - _, - ) => return self_string.partial_cmp(&other_string), - ( - serde_json::Value::String(self_string), - _, - Some(OwnedLiteral::LString(other_string)), - ) => return self_string.partial_cmp(&other_string), - // Otherwise, convert to numbers and compare them that way - (_, Some(json_other), _) => return compare_json_numbers(self, json_other), - (_, _, Some(literal_other)) => return compare_json_numbers(self, literal_other), - _ => panic!("Unimplemented comparison type."), - } - } - - fn math_add<'a>(&self, other: &dyn ContextElement) -> Option> { - let other_json = other.to_any().downcast_ref::(); - let other_literal = other.to_any().downcast_ref::(); - match (self, other_json, other_literal) { - // If its neither of those types, then it is unimplemented - (_, None, None) => panic!("Math operation on unimplemented type"), - // Since this is specifically for the math helper, non-primitives are not supported - (serde_json::Value::Array(_), _, _) - | (serde_json::Value::Object(_), _, _) - | (_, Some(serde_json::Value::Array(_)), _) - | (_, Some(serde_json::Value::Object(_)), _) => None, - // Strings also are ignored because this is specifically a math function - (serde_json::Value::String(_), _, _) - | (_, Some(serde_json::Value::String(_)), _) - | (_, _, Some(OwnedLiteral::LString(_))) => None, - // Handle other serde_json::Value - (_, Some(other_json_value), _) => (std::convert::Into::::into(self) - + std::convert::Into::::into(other_json_value)) - .map(IceResult::from_owned), - // Handle literals - (_, _, Some(other_literal)) => (std::convert::Into::::into(self) - + std::convert::Into::::into(other_literal)) - .map(IceResult::from_owned), - } - } - - fn math_subtract<'a>(&self, other: &dyn ContextElement) -> Option> { - let other_json = other.to_any().downcast_ref::(); - let other_literal = other.to_any().downcast_ref::(); - match (self, other_json, other_literal) { - // If its neither of those types, then it is unimplemented - (_, None, None) => panic!("Math operation on unimplemented type"), - // Since this is specifically for the math helper, non-primitives are not supported - (serde_json::Value::Array(_), _, _) - | (serde_json::Value::Object(_), _, _) - | (_, Some(serde_json::Value::Array(_)), _) - | (_, Some(serde_json::Value::Object(_)), _) => None, - // Strings also are ignored because this is specifically a math function - (serde_json::Value::String(_), _, _) - | (_, Some(serde_json::Value::String(_)), _) - | (_, _, Some(OwnedLiteral::LString(_))) => None, - // Handle other serde_json::Value - (_, Some(other_json_value), _) => (std::convert::Into::::into(self) - - std::convert::Into::::into(other_json_value)) - .map(IceResult::from_owned), - // Handle literals - (_, _, Some(other_literal)) => (std::convert::Into::::into(self) - - std::convert::Into::::into(other_literal)) - .map(IceResult::from_owned), - } - } - - fn math_multiply<'a>(&self, other: &dyn ContextElement) -> Option> { - let other_json = other.to_any().downcast_ref::(); - let other_literal = other.to_any().downcast_ref::(); - match (self, other_json, other_literal) { - // If its neither of those types, then it is unimplemented - (_, None, None) => panic!("Math operation on unimplemented type"), - // Since this is specifically for the math helper, non-primitives are not supported - (serde_json::Value::Array(_), _, _) - | (serde_json::Value::Object(_), _, _) - | (_, Some(serde_json::Value::Array(_)), _) - | (_, Some(serde_json::Value::Object(_)), _) => None, - // Strings also are ignored because this is specifically a math function - (serde_json::Value::String(_), _, _) - | (_, Some(serde_json::Value::String(_)), _) - | (_, _, Some(OwnedLiteral::LString(_))) => None, - // Handle other serde_json::Value - (_, Some(other_json_value), _) => (std::convert::Into::::into(self) - * std::convert::Into::::into(other_json_value)) - .map(IceResult::from_owned), - // Handle literals - (_, _, Some(other_literal)) => (std::convert::Into::::into(self) - * std::convert::Into::::into(other_literal)) - .map(IceResult::from_owned), - } - } - - fn math_divide<'a>(&self, other: &dyn ContextElement) -> Option> { - let other_json = other.to_any().downcast_ref::(); - let other_literal = other.to_any().downcast_ref::(); - match (self, other_json, other_literal) { - // If its neither of those types, then it is unimplemented - (_, None, None) => panic!("Math operation on unimplemented type"), - // Since this is specifically for the math helper, non-primitives are not supported - (serde_json::Value::Array(_), _, _) - | (serde_json::Value::Object(_), _, _) - | (_, Some(serde_json::Value::Array(_)), _) - | (_, Some(serde_json::Value::Object(_)), _) => None, - // Strings also are ignored because this is specifically a math function - (serde_json::Value::String(_), _, _) - | (_, Some(serde_json::Value::String(_)), _) - | (_, _, Some(OwnedLiteral::LString(_))) => None, - // Handle other serde_json::Value - (_, Some(other_json_value), _) => (std::convert::Into::::into(self) - / std::convert::Into::::into(other_json_value)) - .map(IceResult::from_owned), - // Handle literals - (_, _, Some(other_literal)) => (std::convert::Into::::into(self) - / std::convert::Into::::into(other_literal)) - .map(IceResult::from_owned), - } - } - - fn math_modulus<'a>(&self, other: &dyn ContextElement) -> Option> { - let other_json = other.to_any().downcast_ref::(); - let other_literal = other.to_any().downcast_ref::(); - match (self, other_json, other_literal) { - // If its neither of those types, then it is unimplemented - (_, None, None) => panic!("Math operation on unimplemented type"), - // Since this is specifically for the math helper, non-primitives are not supported - (serde_json::Value::Array(_), _, _) - | (serde_json::Value::Object(_), _, _) - | (_, Some(serde_json::Value::Array(_)), _) - | (_, Some(serde_json::Value::Object(_)), _) => None, - // Strings also are ignored because this is specifically a math function - (serde_json::Value::String(_), _, _) - | (_, Some(serde_json::Value::String(_)), _) - | (_, _, Some(OwnedLiteral::LString(_))) => None, - // Handle other serde_json::Value - (_, Some(other_json_value), _) => (std::convert::Into::::into(self) - % std::convert::Into::::into(other_json_value)) - .map(IceResult::from_owned), - // Handle literals - (_, _, Some(other_literal)) => (std::convert::Into::::into(self) - % std::convert::Into::::into(other_literal)) - .map(IceResult::from_owned), - } - } - - fn math_abs<'a>(&self) -> Option> { - std::convert::Into::::into(self) - .math_abs() - .map(IceResult::from_owned) - } - - fn math_floor<'a>(&self) -> Option> { - std::convert::Into::::into(self) - .math_floor() - .map(IceResult::from_owned) - } - - fn math_ceil<'a>(&self) -> Option> { - std::convert::Into::::into(self) - .math_ceil() - .map(IceResult::from_owned) - } -} - -impl From<&serde_json::Value> for ComparisonNumber { - /// Convert from a JSON value to a number for comparison based on - /// the logic described at - /// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Greater_than - fn from(original: &serde_json::Value) -> Self { - match original { - serde_json::Value::Null => ComparisonNumber::UnsignedInteger(0), - serde_json::Value::Bool(boolean) => { - if *boolean { - ComparisonNumber::UnsignedInteger(1) - } else { - ComparisonNumber::UnsignedInteger(0) - } - } - serde_json::Value::Number(num) => num.into(), - serde_json::Value::String(text) => text.into(), - serde_json::Value::Array(_) => { - panic!("Only primitives should be cast to numbers for comparisons") - } - serde_json::Value::Object(_) => { - panic!("Only primitives should be cast to numbers for comparisons") - } - } - } -} - -impl From<&serde_json::Number> for ComparisonNumber { - fn from(original: &serde_json::Number) -> Self { - match original.as_u64() { - Some(num) => return ComparisonNumber::UnsignedInteger(num), - None => (), - }; - match original.as_i64() { - Some(num) => return ComparisonNumber::SignedInteger(num), - None => (), - }; - match original.as_f64() { - Some(num) => return ComparisonNumber::Decimal(num), - None => (), - }; - ComparisonNumber::Failure - } -} - -impl From<&serde_json::Value> for MathNumber { - fn from(original: &serde_json::Value) -> Self { - match original { - serde_json::Value::Null => MathNumber::Integer(0), - serde_json::Value::Bool(boolean) => { - if *boolean { - MathNumber::Integer(1) - } else { - MathNumber::Integer(0) - } - } - serde_json::Value::Number(num) => num.into(), - serde_json::Value::String(_) => { - panic!("Strings should not be cast to numbers for math") - } - serde_json::Value::Array(_) => { - panic!("Only primitives should be cast to numbers for comparisons") - } - serde_json::Value::Object(_) => { - panic!("Only primitives should be cast to numbers for comparisons") - } - } - } -} - -impl From<&serde_json::Number> for MathNumber { - fn from(original: &serde_json::Number) -> Self { - match original.as_u64() { - Some(num) => return MathNumber::Integer(num.try_into().unwrap()), - None => (), - }; - match original.as_i64() { - Some(num) => return MathNumber::Integer(num.into()), - None => (), - }; - match original.as_f64() { - Some(num) => return MathNumber::Decimal(num), - None => (), - }; - MathNumber::Failure - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_nested_array_render() { - let x: serde_json::Value = - serde_json::from_str(r#"[3,5,[7,9]]"#).expect("Failed to parse json"); - assert_eq!( - x.render(&Vec::new()), - Ok::<_, RenderError>("3,5,7,9".to_owned()) - ); - } - - #[test] - fn test_html_escape() { - assert_eq!(html_escape("<>&\"'"), "<>&"'".to_owned()) - } -} diff --git a/src/integrations/json.rs b/src/integrations/json.rs index 8242fb2..3327d06 100644 --- a/src/integrations/json.rs +++ b/src/integrations/json.rs @@ -4,3 +4,703 @@ //! dependency since ContextElement can be implemented for any //! type. Disable the json-integration feature to avoid compiling this //! file and adding the serde and serde_json dependencies. +//! +//! In order to recreate perfect compatibility with the original +//! dustjs implementation, the logic below needs to follow the "rules" +//! of javascript logic. +use crate::parser::Filter; +use crate::parser::OwnedLiteral; +use crate::renderer::compare_json_numbers; +use crate::renderer::Castable; +use crate::renderer::CompareContextElement; +use crate::renderer::ComparisonNumber; +use crate::renderer::ContextElement; +use crate::renderer::IceResult; +use crate::renderer::IntoContextElement; +use crate::renderer::Loopable; +use crate::renderer::MathNumber; +use crate::renderer::RenderError; +use crate::renderer::Renderable; +use crate::renderer::Sizable; +use crate::renderer::Truthiness; +use crate::renderer::WalkError; +use crate::renderer::Walkable; +use std::cmp::Ordering; +use std::convert::TryInto; + +fn html_escape(inp: &str) -> String { + // Adding 10% space from the original to avoid re-allocations by + // leaving room for escaped sequences. + let mut output = String::with_capacity(((inp.len() as f64) * 1.1) as usize); + inp.chars().for_each(|c| match c { + '<' => output.push_str("<"), + '>' => output.push_str(">"), + '"' => output.push_str("""), + '\'' => output.push_str("'"), + '&' => output.push_str("&"), + _ => output.push(c), + }); + output +} + +fn javascript_escape(inp: &str) -> String { + // Adding 10% space from the original to avoid re-allocations by + // leaving room for escaped sequences. + let mut output = String::with_capacity(((inp.len() as f64) * 1.1) as usize); + inp.chars().for_each(|c| match c { + '"' => output.push_str(r#"\""#), + '\'' => output.push_str(r#"\'"#), + '\t' => output.push_str(r#"\t"#), + '\x0C' => output.push_str(r#"\f"#), + '\n' => output.push_str(r#"\n"#), + '\r' => output.push_str(r#"\r"#), + '\\' => output.push_str(r#"\\"#), + '/' => output.push_str(r#"\/"#), + _ => output.push(c), + }); + output +} + +fn get_utf8_hex(inp: char) -> String { + let num_bytes = inp.len_utf8(); + let mut byte_buffer = [0; 4]; // UTF-8 supports up to 4 bytes per codepoint + let mut output = String::with_capacity(num_bytes * 2); + + inp.encode_utf8(&mut byte_buffer); + + for b in &byte_buffer[..num_bytes] { + output.push_str(&format!("{:02X}", b)); + } + + output +} + +fn encode_uri(inp: &str) -> String { + // Adding 10% space from the original to avoid re-allocations by + // leaving room for escaped sequences. + let mut output = String::with_capacity(((inp.len() as f64) * 1.1) as usize); + inp.chars().for_each(|c| match c { + '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | 'a' | 'b' | 'c' | 'd' | 'e' + | 'f' | 'g' | 'h' | 'i' | 'j' | 'k' | 'l' | 'm' | 'n' | 'o' | 'p' | 'q' | 'r' | 's' + | 't' | 'u' | 'v' | 'w' | 'x' | 'y' | 'z' | 'A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'G' + | 'H' | 'I' | 'J' | 'K' | 'L' | 'M' | 'N' | 'O' | 'P' | 'Q' | 'R' | 'S' | 'T' | 'U' + | 'V' | 'W' | 'X' | 'Y' | 'Z' | ';' | ',' | '/' | '?' | ':' | '@' | '&' | '=' | '+' + | '$' | '-' | '_' | '.' | '!' | '~' | '*' | '\'' | '(' | ')' | '#' => output.push(c), + _ => { + output.push('%'); + output.push_str(&get_utf8_hex(c)); + } + }); + output +} + +fn encode_uri_component(inp: &str) -> String { + // Adding 10% space from the original to avoid re-allocations by + // leaving room for escaped sequences. + let mut output = String::with_capacity(((inp.len() as f64) * 1.1) as usize); + inp.chars().for_each(|c| match c { + '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | 'a' | 'b' | 'c' | 'd' | 'e' + | 'f' | 'g' | 'h' | 'i' | 'j' | 'k' | 'l' | 'm' | 'n' | 'o' | 'p' | 'q' | 'r' | 's' + | 't' | 'u' | 'v' | 'w' | 'x' | 'y' | 'z' | 'A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'G' + | 'H' | 'I' | 'J' | 'K' | 'L' | 'M' | 'N' | 'O' | 'P' | 'Q' | 'R' | 'S' | 'T' | 'U' + | 'V' | 'W' | 'X' | 'Y' | 'Z' | '-' | '_' | '.' | '!' | '~' | '*' | '\'' | '(' | ')' => { + output.push(c) + } + _ => { + output.push('%'); + output.push_str(&get_utf8_hex(c)); + } + }); + output +} + +fn apply_filter( + json_value: &serde_json::Value, + filter: &Filter, +) -> Result { + match (json_value, filter) { + // Html escape filter + (serde_json::Value::String(string), Filter::HtmlEncode) => { + Ok(serde_json::Value::String(html_escape(string))) + } + (_, Filter::HtmlEncode) => Ok(serde_json::Value::String(html_escape( + &json_value.render(&Vec::new())?, + ))), + // Disable html escape filter + (_, Filter::DisableHtmlEncode) => panic!("The |s filter is automatically removed by the renderer since it is a no-op during rendering."), + // Parse JSON filter + (serde_json::Value::String(string), Filter::JsonParse) => { + serde_json::from_str(&string).or(Err(RenderError::InvalidJson(string.to_owned()))) + } + (_, Filter::JsonParse) => { + let rendered_value = json_value.render(&Vec::new())?; + serde_json::from_str(&rendered_value).or(Err(RenderError::InvalidJson(rendered_value))) + } + // Json Stringify filter + (_, Filter::JsonStringify) => { + Ok(serde_json::Value::String(json_value.to_string())) + } + // Javascript escape filter + (serde_json::Value::String(string), Filter::JavascriptStringEncode) => { + Ok(serde_json::Value::String(javascript_escape(string))) + } + (serde_json::Value::Bool(boolean), Filter::JavascriptStringEncode) => { + Ok(serde_json::Value::Bool(*boolean)) + } + (serde_json::Value::Number(number), Filter::JavascriptStringEncode) => { + Ok(serde_json::Value::Number(number.clone())) + } + (serde_json::Value::Array(arr), Filter::JavascriptStringEncode) => { + Ok(serde_json::Value::Array(arr.clone())) + } + (serde_json::Value::Object(obj), Filter::JavascriptStringEncode) => { + Ok(serde_json::Value::Object(obj.clone())) + } + (_, Filter::JavascriptStringEncode) => Ok(serde_json::Value::String(javascript_escape( + &json_value.render(&Vec::new())?, + ))), + // EncodeURI filter + (serde_json::Value::String(string), Filter::EncodeUri) => { + Ok(serde_json::Value::String(encode_uri(string))) + } + (_, Filter::EncodeUri) => Ok(serde_json::Value::String(encode_uri( + &json_value.render(&Vec::new())?, + ))), + // EncodeURIComponent filter + (serde_json::Value::String(string), Filter::EncodeUriComponent) => { + Ok(serde_json::Value::String(encode_uri_component(string))) + } + (_, Filter::EncodeUriComponent) => Ok(serde_json::Value::String(encode_uri_component( + &json_value.render(&Vec::new())?, + ))), + } +} + +fn apply_filters( + json_value: &serde_json::Value, + filters: &[Filter], +) -> Result { + let mut final_value: serde_json::Value = apply_filter(json_value, &filters[0])?; + + for filter in &filters[1..] { + final_value = apply_filter(&final_value, filter)?; + } + + Ok(final_value) +} + +impl ContextElement for serde_json::Value {} + +impl Truthiness for serde_json::Value { + fn is_truthy(&self) -> bool { + match self { + serde_json::Value::Null => false, + serde_json::Value::Bool(boolean) => *boolean, + serde_json::Value::Number(_num) => true, + serde_json::Value::String(string_value) => !string_value.is_empty(), + serde_json::Value::Array(array_value) => !array_value.is_empty(), + serde_json::Value::Object(_obj) => true, + } + } +} + +impl Renderable for serde_json::Value { + fn render(&self, _filters: &Vec) -> Result { + let after_apply = if _filters.is_empty() { + None + } else { + Some(apply_filters(self, _filters)?) + }; + + match after_apply.as_ref().unwrap_or(self) { + serde_json::Value::Null => Ok("".to_owned()), + serde_json::Value::Bool(boolean) => Ok(boolean.to_string()), + serde_json::Value::Number(num) => Ok(num.to_string()), + serde_json::Value::String(string) => Ok(string.to_string()), + serde_json::Value::Array(arr) => { + let rendered: Result, RenderError> = + arr.iter().map(|val| val.render(&Vec::new())).collect(); + let rendered_slice: &[String] = &rendered?; + Ok(rendered_slice.join(",")) + } + serde_json::Value::Object(_obj) => Ok("[object Object]".to_owned()), + } + } +} + +impl Walkable for serde_json::Value { + fn walk(&self, segment: &str) -> Result<&dyn IntoContextElement, WalkError> { + match self { + serde_json::Value::Null => Err(WalkError::CantWalk), + serde_json::Value::Bool(_boolean) => Err(WalkError::CantWalk), + serde_json::Value::Number(_num) => Err(WalkError::CantWalk), + serde_json::Value::String(_string) => Err(WalkError::CantWalk), + serde_json::Value::Array(_arr) => Err(WalkError::CantWalk), + serde_json::Value::Object(obj) => obj + .get(segment) + .map(|val| val as _) + .ok_or(WalkError::CantWalk), + } + } +} + +impl Loopable for serde_json::Value { + fn get_loop_elements(&self) -> Vec<&dyn ContextElement> { + match self { + serde_json::Value::Array(array_value) => array_value.iter().map(|x| x as _).collect(), + _ => Vec::new(), + } + } +} + +impl Sizable for serde_json::Value { + fn is_castable(&self) -> bool { + match self { + serde_json::Value::Null => true, + serde_json::Value::Bool(_) => false, + serde_json::Value::Number(_) => true, + serde_json::Value::String(_) => true, + serde_json::Value::Array(_) => true, + serde_json::Value::Object(_) => true, + } + } + + fn get_size<'a>(&'a self) -> Option> { + match self { + serde_json::Value::Null => { + Some(IceResult::from_owned(OwnedLiteral::LPositiveInteger(0))) + } + serde_json::Value::Bool(_boolean) => { + Some(IceResult::from_owned(OwnedLiteral::LPositiveInteger(0))) + } + serde_json::Value::Number(_num) => Some(IceResult::from_borrowed(self)), + serde_json::Value::String(text) => Some(IceResult::from_owned( + OwnedLiteral::LPositiveInteger(text.len().try_into().unwrap()), + )), + serde_json::Value::Array(arr) => Some(IceResult::from_owned( + OwnedLiteral::LPositiveInteger(arr.len().try_into().unwrap()), + )), + serde_json::Value::Object(obj) => Some(IceResult::from_owned( + OwnedLiteral::LPositiveInteger(obj.len().try_into().unwrap()), + )), + } + } +} + +impl Castable for serde_json::Value { + fn cast_to_type<'a>(&'a self, target: &str) -> Option> { + match (self, target) { + (serde_json::Value::String(text), "number") => text + .parse::() + .map(|num| IceResult::from_owned(OwnedLiteral::LPositiveInteger(num))) + .or_else(|_| { + text.parse::() + .map(|num| IceResult::from_owned(OwnedLiteral::LNegativeInteger(num))) + }) + .or_else(|_| { + text.parse::() + .map(|num| IceResult::from_owned(OwnedLiteral::LFloat(num))) + }) + .ok(), + (serde_json::Value::Number(_), "number") => Some(IceResult::from_borrowed(self)), + (serde_json::Value::Null, "number") => { + Some(IceResult::from_owned(serde_json::Value::Number(0.into()))) + } + (serde_json::Value::Bool(boolean), "number") => { + if *boolean { + Some(IceResult::from_owned(serde_json::Value::Number(1.into()))) + } else { + Some(IceResult::from_owned(serde_json::Value::Number(0.into()))) + } + } + (serde_json::Value::Array(_), "number") => None, + (serde_json::Value::Object(_), "number") => None, + + (serde_json::Value::String(_), "string") => Some(IceResult::from_borrowed(self)), + (serde_json::Value::Number(num), "string") => Some(IceResult::from_owned( + serde_json::Value::String(num.to_string()), + )), + (serde_json::Value::Null, "string") => Some(IceResult::from_owned( + serde_json::Value::String("null".to_owned()), + )), + (serde_json::Value::Bool(boolean), "string") => Some(IceResult::from_owned( + serde_json::Value::String(boolean.to_string()), + )), + (serde_json::Value::Array(_), "string") => Some(IceResult::from_owned( + serde_json::Value::String(self.render(&Vec::new()).unwrap_or("".to_owned())), + )), + (serde_json::Value::Object(_), "string") => Some(IceResult::from_owned( + serde_json::Value::String(self.render(&Vec::new()).unwrap_or("".to_owned())), + )), + + (serde_json::Value::String(text), "boolean") => { + if text.is_empty() { + Some(IceResult::from_owned(serde_json::Value::Bool(false))) + } else { + Some(IceResult::from_owned(serde_json::Value::Bool(true))) + } + } + (serde_json::Value::Number(json_num), "boolean") => { + Some(IceResult::from_owned(serde_json::Value::Bool( + match (json_num.as_u64(), json_num.as_i64(), json_num.as_f64()) { + (Some(num), _, _) => num != 0, + (_, Some(num), _) => num != 0, + (_, _, Some(num)) => num != 0.0 && !num.is_nan(), + _ => false, + }, + ))) + } + (serde_json::Value::Null, "boolean") => { + Some(IceResult::from_owned(serde_json::Value::Bool(false))) + } + (serde_json::Value::Bool(_), "boolean") => Some(IceResult::from_borrowed(self)), + (serde_json::Value::Array(_), "boolean") => { + Some(IceResult::from_owned(serde_json::Value::Bool(true))) + } + (serde_json::Value::Object(_), "boolean") => { + Some(IceResult::from_owned(serde_json::Value::Bool(true))) + } + + (_, _) => panic!("Unimplemented cast"), + } + } +} + +impl CompareContextElement for serde_json::Value { + fn equals(&self, other: &dyn ContextElement) -> bool { + // println!("Json equality check {:?} == {:?}", self, other); + // Handle other serde_json::Value + match other.to_any().downcast_ref::() { + None => (), + Some(other_json_value) => match (self, other_json_value) { + // Non-scalar values not caught in the renderer by the + // identical-path shortcut are always not equal. + (serde_json::Value::Array(_), _) + | (_, serde_json::Value::Array(_)) + | (serde_json::Value::Object(_), _) + | (_, serde_json::Value::Object(_)) => return false, + _ => return self == other_json_value, + }, + } + // Handle literals + match other.to_any().downcast_ref::() { + None => (), + Some(OwnedLiteral::LString(other_string)) => { + return self.as_str().map_or(false, |s| s == other_string) + } + Some(OwnedLiteral::LBoolean(boolean)) => { + return self.equals(&serde_json::Value::Bool(*boolean) as &dyn ContextElement); + } + Some(OwnedLiteral::LPositiveInteger(other_num)) => { + let other_json_num: serde_json::Number = std::convert::From::from(*other_num); + return self + .equals(&serde_json::Value::Number(other_json_num) as &dyn ContextElement); + } + Some(OwnedLiteral::LNegativeInteger(other_num)) => { + let other_json_num: serde_json::Number = std::convert::From::from(*other_num); + return self + .equals(&serde_json::Value::Number(other_json_num) as &dyn ContextElement); + } + Some(OwnedLiteral::LFloat(other_num)) => match self.as_f64() { + None => return false, + Some(self_float) => return self_float == *other_num, + }, + } + false + } + + fn partial_compare(&self, other: &dyn ContextElement) -> Option { + // Handle type coerced objects + + // When doing a greater than or less than comparison, + // javascript coerces objects into "[object Object]". + if let serde_json::Value::Object(_) = self { + return OwnedLiteral::LString(self.render(&Vec::new()).unwrap_or("".to_owned())) + .partial_compare(other); + } + + // When doing a greater than or less than comparison + // javascript turns arrays into strings. + if let serde_json::Value::Array(_) = self { + return OwnedLiteral::LString(self.render(&Vec::new()).unwrap_or("".to_owned())) + .partial_compare(other); + } + + let maybe_json_other = other.to_any().downcast_ref::(); + let maybe_literal_other = other.to_any().downcast_ref::(); + + // If they're both strings, compare them directly + match (self, maybe_json_other, maybe_literal_other) { + // If they're both strings, compare them directly + ( + serde_json::Value::String(self_string), + Some(serde_json::Value::String(other_string)), + _, + ) => return self_string.partial_cmp(&other_string), + ( + serde_json::Value::String(self_string), + _, + Some(OwnedLiteral::LString(other_string)), + ) => return self_string.partial_cmp(&other_string), + // Otherwise, convert to numbers and compare them that way + (_, Some(json_other), _) => return compare_json_numbers(self, json_other), + (_, _, Some(literal_other)) => return compare_json_numbers(self, literal_other), + _ => panic!("Unimplemented comparison type."), + } + } + + fn math_add<'a>(&self, other: &dyn ContextElement) -> Option> { + let other_json = other.to_any().downcast_ref::(); + let other_literal = other.to_any().downcast_ref::(); + match (self, other_json, other_literal) { + // If its neither of those types, then it is unimplemented + (_, None, None) => panic!("Math operation on unimplemented type"), + // Since this is specifically for the math helper, non-primitives are not supported + (serde_json::Value::Array(_), _, _) + | (serde_json::Value::Object(_), _, _) + | (_, Some(serde_json::Value::Array(_)), _) + | (_, Some(serde_json::Value::Object(_)), _) => None, + // Strings also are ignored because this is specifically a math function + (serde_json::Value::String(_), _, _) + | (_, Some(serde_json::Value::String(_)), _) + | (_, _, Some(OwnedLiteral::LString(_))) => None, + // Handle other serde_json::Value + (_, Some(other_json_value), _) => (std::convert::Into::::into(self) + + std::convert::Into::::into(other_json_value)) + .map(IceResult::from_owned), + // Handle literals + (_, _, Some(other_literal)) => (std::convert::Into::::into(self) + + std::convert::Into::::into(other_literal)) + .map(IceResult::from_owned), + } + } + + fn math_subtract<'a>(&self, other: &dyn ContextElement) -> Option> { + let other_json = other.to_any().downcast_ref::(); + let other_literal = other.to_any().downcast_ref::(); + match (self, other_json, other_literal) { + // If its neither of those types, then it is unimplemented + (_, None, None) => panic!("Math operation on unimplemented type"), + // Since this is specifically for the math helper, non-primitives are not supported + (serde_json::Value::Array(_), _, _) + | (serde_json::Value::Object(_), _, _) + | (_, Some(serde_json::Value::Array(_)), _) + | (_, Some(serde_json::Value::Object(_)), _) => None, + // Strings also are ignored because this is specifically a math function + (serde_json::Value::String(_), _, _) + | (_, Some(serde_json::Value::String(_)), _) + | (_, _, Some(OwnedLiteral::LString(_))) => None, + // Handle other serde_json::Value + (_, Some(other_json_value), _) => (std::convert::Into::::into(self) + - std::convert::Into::::into(other_json_value)) + .map(IceResult::from_owned), + // Handle literals + (_, _, Some(other_literal)) => (std::convert::Into::::into(self) + - std::convert::Into::::into(other_literal)) + .map(IceResult::from_owned), + } + } + + fn math_multiply<'a>(&self, other: &dyn ContextElement) -> Option> { + let other_json = other.to_any().downcast_ref::(); + let other_literal = other.to_any().downcast_ref::(); + match (self, other_json, other_literal) { + // If its neither of those types, then it is unimplemented + (_, None, None) => panic!("Math operation on unimplemented type"), + // Since this is specifically for the math helper, non-primitives are not supported + (serde_json::Value::Array(_), _, _) + | (serde_json::Value::Object(_), _, _) + | (_, Some(serde_json::Value::Array(_)), _) + | (_, Some(serde_json::Value::Object(_)), _) => None, + // Strings also are ignored because this is specifically a math function + (serde_json::Value::String(_), _, _) + | (_, Some(serde_json::Value::String(_)), _) + | (_, _, Some(OwnedLiteral::LString(_))) => None, + // Handle other serde_json::Value + (_, Some(other_json_value), _) => (std::convert::Into::::into(self) + * std::convert::Into::::into(other_json_value)) + .map(IceResult::from_owned), + // Handle literals + (_, _, Some(other_literal)) => (std::convert::Into::::into(self) + * std::convert::Into::::into(other_literal)) + .map(IceResult::from_owned), + } + } + + fn math_divide<'a>(&self, other: &dyn ContextElement) -> Option> { + let other_json = other.to_any().downcast_ref::(); + let other_literal = other.to_any().downcast_ref::(); + match (self, other_json, other_literal) { + // If its neither of those types, then it is unimplemented + (_, None, None) => panic!("Math operation on unimplemented type"), + // Since this is specifically for the math helper, non-primitives are not supported + (serde_json::Value::Array(_), _, _) + | (serde_json::Value::Object(_), _, _) + | (_, Some(serde_json::Value::Array(_)), _) + | (_, Some(serde_json::Value::Object(_)), _) => None, + // Strings also are ignored because this is specifically a math function + (serde_json::Value::String(_), _, _) + | (_, Some(serde_json::Value::String(_)), _) + | (_, _, Some(OwnedLiteral::LString(_))) => None, + // Handle other serde_json::Value + (_, Some(other_json_value), _) => (std::convert::Into::::into(self) + / std::convert::Into::::into(other_json_value)) + .map(IceResult::from_owned), + // Handle literals + (_, _, Some(other_literal)) => (std::convert::Into::::into(self) + / std::convert::Into::::into(other_literal)) + .map(IceResult::from_owned), + } + } + + fn math_modulus<'a>(&self, other: &dyn ContextElement) -> Option> { + let other_json = other.to_any().downcast_ref::(); + let other_literal = other.to_any().downcast_ref::(); + match (self, other_json, other_literal) { + // If its neither of those types, then it is unimplemented + (_, None, None) => panic!("Math operation on unimplemented type"), + // Since this is specifically for the math helper, non-primitives are not supported + (serde_json::Value::Array(_), _, _) + | (serde_json::Value::Object(_), _, _) + | (_, Some(serde_json::Value::Array(_)), _) + | (_, Some(serde_json::Value::Object(_)), _) => None, + // Strings also are ignored because this is specifically a math function + (serde_json::Value::String(_), _, _) + | (_, Some(serde_json::Value::String(_)), _) + | (_, _, Some(OwnedLiteral::LString(_))) => None, + // Handle other serde_json::Value + (_, Some(other_json_value), _) => (std::convert::Into::::into(self) + % std::convert::Into::::into(other_json_value)) + .map(IceResult::from_owned), + // Handle literals + (_, _, Some(other_literal)) => (std::convert::Into::::into(self) + % std::convert::Into::::into(other_literal)) + .map(IceResult::from_owned), + } + } + + fn math_abs<'a>(&self) -> Option> { + std::convert::Into::::into(self) + .math_abs() + .map(IceResult::from_owned) + } + + fn math_floor<'a>(&self) -> Option> { + std::convert::Into::::into(self) + .math_floor() + .map(IceResult::from_owned) + } + + fn math_ceil<'a>(&self) -> Option> { + std::convert::Into::::into(self) + .math_ceil() + .map(IceResult::from_owned) + } +} + +impl From<&serde_json::Value> for ComparisonNumber { + /// Convert from a JSON value to a number for comparison based on + /// the logic described at + /// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Greater_than + fn from(original: &serde_json::Value) -> Self { + match original { + serde_json::Value::Null => ComparisonNumber::UnsignedInteger(0), + serde_json::Value::Bool(boolean) => { + if *boolean { + ComparisonNumber::UnsignedInteger(1) + } else { + ComparisonNumber::UnsignedInteger(0) + } + } + serde_json::Value::Number(num) => num.into(), + serde_json::Value::String(text) => text.into(), + serde_json::Value::Array(_) => { + panic!("Only primitives should be cast to numbers for comparisons") + } + serde_json::Value::Object(_) => { + panic!("Only primitives should be cast to numbers for comparisons") + } + } + } +} + +impl From<&serde_json::Number> for ComparisonNumber { + fn from(original: &serde_json::Number) -> Self { + match original.as_u64() { + Some(num) => return ComparisonNumber::UnsignedInteger(num), + None => (), + }; + match original.as_i64() { + Some(num) => return ComparisonNumber::SignedInteger(num), + None => (), + }; + match original.as_f64() { + Some(num) => return ComparisonNumber::Decimal(num), + None => (), + }; + ComparisonNumber::Failure + } +} + +impl From<&serde_json::Value> for MathNumber { + fn from(original: &serde_json::Value) -> Self { + match original { + serde_json::Value::Null => MathNumber::Integer(0), + serde_json::Value::Bool(boolean) => { + if *boolean { + MathNumber::Integer(1) + } else { + MathNumber::Integer(0) + } + } + serde_json::Value::Number(num) => num.into(), + serde_json::Value::String(_) => { + panic!("Strings should not be cast to numbers for math") + } + serde_json::Value::Array(_) => { + panic!("Only primitives should be cast to numbers for comparisons") + } + serde_json::Value::Object(_) => { + panic!("Only primitives should be cast to numbers for comparisons") + } + } + } +} + +impl From<&serde_json::Number> for MathNumber { + fn from(original: &serde_json::Number) -> Self { + match original.as_u64() { + Some(num) => return MathNumber::Integer(num.try_into().unwrap()), + None => (), + }; + match original.as_i64() { + Some(num) => return MathNumber::Integer(num.into()), + None => (), + }; + match original.as_f64() { + Some(num) => return MathNumber::Decimal(num), + None => (), + }; + MathNumber::Failure + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_nested_array_render() { + let x: serde_json::Value = + serde_json::from_str(r#"[3,5,[7,9]]"#).expect("Failed to parse json"); + assert_eq!( + x.render(&Vec::new()), + Ok::<_, RenderError>("3,5,7,9".to_owned()) + ); + } + + #[test] + fn test_html_escape() { + assert_eq!(html_escape("<>&\"'"), "<>&"'".to_owned()) + } +} diff --git a/src/integrations/mod.rs b/src/integrations/mod.rs new file mode 100644 index 0000000..31178e7 --- /dev/null +++ b/src/integrations/mod.rs @@ -0,0 +1,2 @@ +#[cfg(feature = "json-integration")] +pub mod json;