From cd0c9a1fd8bcfae29a9a8e67721d25a12812e309 Mon Sep 17 00:00:00 2001 From: oonxt Date: Tue, 11 Feb 2025 16:28:10 +0800 Subject: [PATCH 1/6] add operator support in `filter` filter --- src/builtins/filters/array.rs | 498 +++++++++++++++++++++++++++++++++- 1 file changed, 496 insertions(+), 2 deletions(-) diff --git a/src/builtins/filters/array.rs b/src/builtins/filters/array.rs index f050a6a1..56b4bf08 100644 --- a/src/builtins/filters/array.rs +++ b/src/builtins/filters/array.rs @@ -1,6 +1,9 @@ +use std::cmp::Ordering; /// Filters operating on array use std::collections::HashMap; - +use std::str::FromStr; +use regex::Regex; +use serde_json::Number; use crate::context::{dotted_pointer, ValueRender}; use crate::errors::{Error, Result}; use crate::filter_utils::{get_sort_strategy_for_type, get_unique_strategy_for_type}; @@ -188,6 +191,176 @@ pub fn group_by(value: &Value, args: &HashMap) -> Result { /// Filter the array values, returning only the values where the `attribute` is equal to the `value` /// Values without the `attribute` or with a null `attribute` are discarded /// If the `value` is not passed, discard all elements where the attribute is null. +enum Operator { + Eq, + Ne, + Gt, + Lt, + Gte, + Lte, + In, + NotIn, + Contains, + NotContains, + Regex, + NotRegex, + IsNull, + IsNotNull, +} +impl std::fmt::Display for Operator { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Operator::Eq => f.write_fmt(::core::format_args!("eq")), + Operator::Ne => f.write_fmt(::core::format_args!("ne")), + Operator::Gt => f.write_fmt(::core::format_args!("gt")), + Operator::Lt => f.write_fmt(::core::format_args!("lt")), + Operator::Gte => f.write_fmt(::core::format_args!("gte")), + Operator::Lte => f.write_fmt(::core::format_args!("lte")), + Operator::In => f.write_fmt(::core::format_args!("in")), + Operator::NotIn => f.write_fmt(::core::format_args!("not_in")), + Operator::Contains => f.write_fmt(::core::format_args!("contains")), + Operator::NotContains => f.write_fmt(::core::format_args!("not_contains")), + Operator::Regex => f.write_fmt(::core::format_args!("regex")), + Operator::NotRegex => f.write_fmt(::core::format_args!("not_regex")), + Operator::IsNull => f.write_fmt(::core::format_args!("is_null")), + Operator::IsNotNull => f.write_fmt(::core::format_args!("is_not_null")), + } + } +} +impl FromStr for Operator { + type Err = Error; + fn from_str(s: &str) -> Result { + match s { + "eq" => Ok(Operator::Eq), + "ne" => Ok(Operator::Ne), + "gt" => Ok(Operator::Gt), + "lt" => Ok(Operator::Lt), + "gte" => Ok(Operator::Gte), + "lte" => Ok(Operator::Lte), + "in" => Ok(Operator::In), + "not_in" => Ok(Operator::NotIn), + "contains" => Ok(Operator::Contains), + "not_contains" => Ok(Operator::NotContains), + "regex" => Ok(Operator::Regex), + "not_regex" => Ok(Operator::NotRegex), + "is_null" => Ok(Operator::IsNull), + "is_not_null" => Ok(Operator::IsNotNull), + s => Err(Error::msg(format!( + "Unkown Value. Found `{}`, Expected `{}`", + s, + stringify!( "eq" , "ne" , "gt" , "lt" , "gte" , "lte" , "in" , "not_in" , "contains" , "not_contains" , "regex" , "not_regex" , "is_null" , "is_not_null" , ) + ))) + } + } +} + +impl Operator { + pub fn is_match(&self, value: &Value, value_to_compare: &Value) -> Result { + match (self, value_to_compare) { + (Operator::Eq , _) => Ok(value == value_to_compare), + (Operator::Ne , _) => Ok(value != value_to_compare), + (Operator::Gt, Value::Number(_)) => { + if value.is_number() { + Ok(Self::compare(value.as_number().unwrap(), value_to_compare.as_number().unwrap()).is_gt()) + } else { + Ok(false) + } + }, + (Operator::Gt, _) => Err(Error::msg("The `filter` operator Gt can only be used with numbers")), + (Operator::Lt, Value::Number(_)) => { + if value.is_number() { + Ok(Self::compare(value.as_number().unwrap(), value_to_compare.as_number().unwrap()).is_lt()) + } else { + Ok(false) + } + }, + (Operator::Lt, _) => Err(Error::msg("The `filter` operator Lt can only be used with numbers")), + (Operator::Gte, Value::Number(_)) => { + if value.is_number() { + Ok(Self::compare(value.as_number().unwrap(), value_to_compare.as_number().unwrap()).is_ge()) + } else { + Ok(false) + } + }, + (Operator::Gte, _) => Err(Error::msg("The `filter` operator Gte can only be used with numbers")), + (Operator::Lte, Value::Number(_)) => { + if value.is_number() { + Ok(Self::compare(value.as_number().unwrap(), value_to_compare.as_number().unwrap()).is_le()) + } else { + Ok(false) + } + }, + (Operator::Lte, _) => Err(Error::msg("The `filter` operator Lte can only be used with numbers")), + (Operator::In, Value::Array(_)) => Ok(value_to_compare.as_array().unwrap().iter().any(|v| v == value)), + (Operator::In, _) => Err(Error::msg("The `filter` operator In can only be used with arrays")), + (Operator::NotIn, Value::Array(_)) => Ok(value_to_compare.as_array().unwrap().iter().all(|v| v != value)), + (Operator::NotIn, _) => Err(Error::msg("The `filter` operator NotIn can only be used with arrays")), + (Operator::Contains, Value::String(_)) => { + if value.is_string() { + Ok(value.as_str().unwrap().contains(value_to_compare.as_str().unwrap())) + } else { + Ok(false) + } + }, + (Operator::Contains, _) => Err(Error::msg("The `filter` operator Contains can only be used with strings")), + (Operator::NotContains, Value::String(_)) => { + if value.is_string() { + Ok(!value.as_str().unwrap().contains(value_to_compare.as_str().unwrap())) + } else { + Ok(true) + } + }, + (Operator::NotContains, _) => Err(Error::msg("The `filter` operator NotContains can only be used with strings")), + (Operator::Regex, Value::String(_)) => { + if value.is_string() { + let re = Regex::new(value_to_compare.as_str().unwrap()).map_err(|err| Error::msg(err))?; + Ok(re.is_match(value.as_str().unwrap())) + } else { + Ok(false) + } + }, + (Operator::Regex, _) => Err(Error::msg("The `filter` operator Regex can only be used with strings")), + (Operator::NotRegex, Value::String(_)) => { + if value.is_string() { + let re = Regex::new(value_to_compare.as_str().unwrap()).map_err(|err| Error::msg(err))?; + Ok(!re.is_match(value.as_str().unwrap())) + } else { + Ok(true) + } + }, + (Operator::NotRegex, _) => Err(Error::msg("The `filter` operator NotRegex can only be used with strings")), + (Operator::IsNull, Value::Bool(b)) => Ok(if *b {value == &Value::Null } else {value != &Value::Null }), + (Operator::IsNull, _) => Err(Error::msg("The `filter` operator IsNull can only be used with booleans")), + (Operator::IsNotNull, Value::Bool(b)) => Ok(if *b {value != &Value::Null } else {value == &Value::Null }), + (Operator::IsNotNull, _) => Err(Error::msg("The `filter` operator IsNotNull can only be used with booleans")), + } + } + + fn compare(v1: &Number, v2: &Number) -> Ordering { + if v1.is_f64() && v2.is_f64() { + v1.as_f64().unwrap().partial_cmp(&v2.as_f64().unwrap()).unwrap() + } else if v1.is_i64() && v2.is_i64() { + v1.as_i64().unwrap().partial_cmp(&v2.as_i64().unwrap()).unwrap() + } else if v1.is_u64() && v2.is_u64() { + v1.as_u64().unwrap().partial_cmp(&v2.as_u64().unwrap()).unwrap() + } else if v1.is_f64() && v2.is_i64() { + v1.as_f64().unwrap().partial_cmp(&(v2.as_i64().unwrap() as f64)).unwrap() + } else if v1.is_i64() && v2.is_f64() { + (v1.as_i64().unwrap() as f64).partial_cmp(&v2.as_f64().unwrap()).unwrap() + } else if v1.is_f64() && v2.is_u64() { + v1.as_f64().unwrap().partial_cmp(&(v2.as_u64().unwrap() as f64)).unwrap() + } else if v1.is_i64() && v2.is_u64() { + (v1.as_i64().unwrap() as f64).partial_cmp(&(v2.as_u64().unwrap() as f64)).unwrap() + } else if v1.is_u64() && v2.is_f64() { + (v1.as_u64().unwrap() as f64).partial_cmp(&v2.as_f64().unwrap()).unwrap() + } else if v1.is_u64() && v2.is_i64() { + (v1.as_u64().unwrap() as f64).partial_cmp(&(v2.as_i64().unwrap() as f64)).unwrap() + } else { + Ordering::Equal + } + } +} + pub fn filter(value: &Value, args: &HashMap) -> Result { let mut arr = try_get_value!("filter", "value", Vec, value); if arr.is_empty() { @@ -198,6 +371,10 @@ pub fn filter(value: &Value, args: &HashMap) -> Result { Some(val) => try_get_value!("filter", "attribute", String, val), None => return Err(Error::msg("The `filter` filter has to have an `attribute` argument")), }; + let operator = match args.get("operator") { + Some(val) => Operator::from_str(&try_get_value!("filter", "operator", String, val)), + None => Ok(Operator::Eq), + }?; let value = args.get("value").unwrap_or(&Value::Null); arr = arr @@ -207,7 +384,7 @@ pub fn filter(value: &Value, args: &HashMap) -> Result { if value.is_null() { !val.is_null() } else { - val == value + operator.is_match(val, value).unwrap() } }) .collect::>(); @@ -782,6 +959,323 @@ mod tests { assert_eq!(res.unwrap(), to_value(expected).unwrap()); } + #[test] + fn test_filter_compare() { + let input = json!([ + {"id": 1, "year": 2015}, + {"id": 2, "year": 2015}, + {"id": 3, "year": 2016}, + {"id": 4, "year": 2017}, + {"id": 5, "year": 2017}, + {"id": 6, "year": 2017}, + {"id": 7, "year": 2018}, + {"id": 8}, + {"id": 9, "year": null}, + ]); + //eq + let mut args = HashMap::new(); + args.insert("attribute".to_string(), to_value("year").unwrap()); + args.insert("operator".to_string(), to_value("eq").unwrap()); + args.insert("value".to_string(), to_value(2015).unwrap()); + + let expected = json!([ + {"id": 1, "year": 2015}, + {"id": 2, "year": 2015}, + ]); + + let res = filter(&input, &args); + assert!(res.is_ok()); + assert_eq!(res.unwrap(), to_value(expected).unwrap()); + + //ne + let mut args = HashMap::new(); + args.insert("attribute".to_string(), to_value("year").unwrap()); + args.insert("operator".to_string(), to_value("ne").unwrap()); + args.insert("value".to_string(), to_value(2015).unwrap()); + + let expected = json!([ + {"id": 3, "year": 2016}, + {"id": 4, "year": 2017}, + {"id": 5, "year": 2017}, + {"id": 6, "year": 2017}, + {"id": 7, "year": 2018}, + {"id": 8}, + {"id": 9, "year": null}, + ]); + + let res = filter(&input, &args); + assert!(res.is_ok()); + assert_eq!(res.unwrap(), to_value(expected).unwrap()); + + //gt + let mut args = HashMap::new(); + args.insert("attribute".to_string(), to_value("year").unwrap()); + args.insert("operator".to_string(), to_value("gt").unwrap()); + args.insert("value".to_string(), to_value(2015).unwrap()); + + let expected = json!([ + {"id": 3, "year": 2016}, + {"id": 4, "year": 2017}, + {"id": 5, "year": 2017}, + {"id": 6, "year": 2017}, + {"id": 7, "year": 2018}, + ]); + + let res = filter(&input, &args); + assert!(res.is_ok()); + assert_eq!(res.unwrap(), to_value(expected).unwrap()); + + //lt + let mut args = HashMap::new(); + args.insert("attribute".to_string(), to_value("year").unwrap()); + args.insert("operator".to_string(), to_value("lt").unwrap()); + args.insert("value".to_string(), to_value(2016).unwrap()); + + let expected = json!([ + {"id": 1, "year": 2015}, + {"id": 2, "year": 2015}, + ]); + + let res = filter(&input, &args); + assert!(res.is_ok()); + assert_eq!(res.unwrap(), to_value(expected).unwrap()); + + //gte + let mut args = HashMap::new(); + args.insert("attribute".to_string(), to_value("year").unwrap()); + args.insert("operator".to_string(), to_value("gte").unwrap()); + args.insert("value".to_string(), to_value(2016).unwrap()); + + let expected = json!([ + {"id": 3, "year": 2016}, + {"id": 4, "year": 2017}, + {"id": 5, "year": 2017}, + {"id": 6, "year": 2017}, + {"id": 7, "year": 2018}, + ]); + + let res = filter(&input, &args); + assert!(res.is_ok()); + assert_eq!(res.unwrap(), to_value(expected).unwrap()); + + + //lte + let mut args = HashMap::new(); + args.insert("attribute".to_string(), to_value("year").unwrap()); + args.insert("operator".to_string(), to_value("lt").unwrap()); + args.insert("value".to_string(), to_value(2016).unwrap()); + + let expected = json!([ + {"id": 1, "year": 2015}, + {"id": 2, "year": 2015}, + {"id": 3, "year": 2016}, + ]); + } + + #[test] + fn test_filter_in() { + let input = json!([ + {"id": 1, "year": 2015}, + {"id": 2, "year": 2015}, + {"id": 3, "year": 2016}, + {"id": 4, "year": 2017}, + {"id": 5, "year": 2017}, + {"id": 6, "year": 2017}, + {"id": 7, "year": 2018}, + {"id": 8}, + {"id": 9, "year": null}, + ]); + //in + let mut args = HashMap::new(); + args.insert("attribute".to_string(), to_value("year").unwrap()); + args.insert("operator".to_string(), to_value("in").unwrap()); + args.insert("value".to_string(), to_value([2015, 2016]).unwrap()); + + let expected = json!([ + {"id": 1, "year": 2015}, + {"id": 2, "year": 2015}, + {"id": 3, "year": 2016}, + ]); + + let res = filter(&input, &args); + assert!(res.is_ok()); + assert_eq!(res.unwrap(), to_value(expected).unwrap()); + + + //not in + let mut args = HashMap::new(); + args.insert("attribute".to_string(), to_value("year").unwrap()); + args.insert("operator".to_string(), to_value("not_in").unwrap()); + args.insert("value".to_string(), to_value([2015, 2016]).unwrap()); + + let expected = json!([ + {"id": 4, "year": 2017}, + {"id": 5, "year": 2017}, + {"id": 6, "year": 2017}, + {"id": 7, "year": 2018}, + {"id": 8}, + {"id": 9, "year": null}, + ]); + + let res = filter(&input, &args); + assert!(res.is_ok()); + assert_eq!(res.unwrap(), to_value(expected).unwrap()); + } + + #[test] + fn test_filter_contain() { + let input = json!([ + {"id": 1, "year": "2015"}, + {"id": 2, "year": "2015"}, + {"id": 3, "year": "2016"}, + {"id": 4, "year": "2017"}, + {"id": 5, "year": "2017"}, + {"id": 6, "year": "2017"}, + {"id": 7, "year": "2018"}, + {"id": 8}, + {"id": 9, "year": null}, + ]); + //contains + let mut args = HashMap::new(); + args.insert("attribute".to_string(), to_value("year").unwrap()); + args.insert("operator".to_string(), to_value("contains").unwrap()); + args.insert("value".to_string(), to_value("15").unwrap()); + + let expected = json!([ + {"id": 1, "year": "2015"}, + {"id": 2, "year": "2015"}, + ]); + + let res = filter(&input, &args); + assert!(res.is_ok()); + assert_eq!(res.unwrap(), to_value(expected).unwrap()); + + //not contains + let mut args = HashMap::new(); + args.insert("attribute".to_string(), to_value("year").unwrap()); + args.insert("operator".to_string(), to_value("not_contains").unwrap()); + args.insert("value".to_string(), to_value("15").unwrap()); + + let expected = json!([ + {"id": 3, "year": "2016"}, + {"id": 4, "year": "2017"}, + {"id": 5, "year": "2017"}, + {"id": 6, "year": "2017"}, + {"id": 7, "year": "2018"}, + {"id": 8}, + {"id": 9, "year": null}, + ]); + + let res = filter(&input, &args); + assert!(res.is_ok()); + assert_eq!(res.unwrap(), to_value(expected).unwrap()); + } + + #[test] + fn test_filter_regex() { + let input = json!([ + {"id": 1, "year": "2015"}, + {"id": 2, "year": "2015"}, + {"id": 3, "year": "2016"}, + {"id": 4, "year": "2017"}, + {"id": 5, "year": "2017"}, + {"id": 6, "year": "2017"}, + {"id": 7, "year": "2018"}, + {"id": 8}, + {"id": 9, "year": null}, + ]); + //regex + let mut args = HashMap::new(); + args.insert("attribute".to_string(), to_value("year").unwrap()); + args.insert("operator".to_string(), to_value("regex").unwrap()); + args.insert("value".to_string(), to_value(r"\d01\d").unwrap()); + + let expected = json!([ + {"id": 1, "year": "2015"}, + {"id": 2, "year": "2015"}, + {"id": 3, "year": "2016"}, + {"id": 4, "year": "2017"}, + {"id": 5, "year": "2017"}, + {"id": 6, "year": "2017"}, + {"id": 7, "year": "2018"}, + ]); + + let res = filter(&input, &args); + assert!(res.is_ok()); + assert_eq!(res.unwrap(), to_value(expected).unwrap()); + + + //not regex + let mut args = HashMap::new(); + args.insert("attribute".to_string(), to_value("year").unwrap()); + args.insert("operator".to_string(), to_value("not_regex").unwrap()); + args.insert("value".to_string(), to_value(r"\d015").unwrap()); + + let expected = json!([ + {"id": 3, "year": "2016"}, + {"id": 4, "year": "2017"}, + {"id": 5, "year": "2017"}, + {"id": 6, "year": "2017"}, + {"id": 7, "year": "2018"}, + {"id": 8}, + {"id": 9, "year": null}, + ]); + + let res = filter(&input, &args); + assert!(res.is_ok()); + assert_eq!(res.unwrap(), to_value(expected).unwrap()); + } + + #[test] + fn test_filter_null() { + let input = json!([ + {"id": 1, "year": "2015"}, + {"id": 2, "year": "2015"}, + {"id": 3, "year": "2016"}, + {"id": 4, "year": "2017"}, + {"id": 5, "year": "2017"}, + {"id": 6, "year": "2017"}, + {"id": 7, "year": "2018"}, + {"id": 8}, + {"id": 9, "year": null}, + ]); + //is null + let mut args = HashMap::new(); + args.insert("attribute".to_string(), to_value("year").unwrap()); + args.insert("operator".to_string(), to_value("is_null").unwrap()); + args.insert("value".to_string(), to_value(true).unwrap()); + + let expected = json!([ + {"id": 8}, + {"id": 9, "year": null}, + ]); + + let res = filter(&input, &args); + assert!(res.is_ok()); + assert_eq!(res.unwrap(), to_value(expected).unwrap()); + + + //is not null + let mut args = HashMap::new(); + args.insert("attribute".to_string(), to_value("year").unwrap()); + args.insert("operator".to_string(), to_value("is_not_null").unwrap()); + args.insert("value".to_string(), to_value(true).unwrap()); + + let expected = json!([ + {"id": 1, "year": "2015"}, + {"id": 2, "year": "2015"}, + {"id": 3, "year": "2016"}, + {"id": 4, "year": "2017"}, + {"id": 5, "year": "2017"}, + {"id": 6, "year": "2017"}, + {"id": 7, "year": "2018"}, + ]); + + let res = filter(&input, &args); + assert!(res.is_ok()); + assert_eq!(res.unwrap(), to_value(expected).unwrap()); + } + #[test] fn test_map_empty() { let res = map(&json!([]), &HashMap::new()); From 46ac3af6cd9ed914d875fb355d84c6113af79937 Mon Sep 17 00:00:00 2001 From: oonxt Date: Tue, 11 Feb 2025 16:35:15 +0800 Subject: [PATCH 2/6] make builtins pub --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 151e530d..c1dd3c8c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -71,7 +71,7 @@ mod tera; mod utils; // Library exports. - +pub use crate::builtins::*; pub use crate::builtins::filters::Filter; pub use crate::builtins::functions::Function; pub use crate::builtins::testers::Test; From 2196c02a2036c0a0c4b11a9f7e1529dceda5efa3 Mon Sep 17 00:00:00 2001 From: oonxt Date: Wed, 12 Feb 2025 10:31:08 +0800 Subject: [PATCH 3/6] add batch filter --- src/builtins/filters/array.rs | 67 +++++++++++++++++++++++++++++++++++ src/tera.rs | 1 + 2 files changed, 68 insertions(+) diff --git a/src/builtins/filters/array.rs b/src/builtins/filters/array.rs index 56b4bf08..554a20d4 100644 --- a/src/builtins/filters/array.rs +++ b/src/builtins/filters/array.rs @@ -483,6 +483,34 @@ pub fn concat(value: &Value, args: &HashMap) -> Result { Ok(to_value(arr).unwrap()) } +/// Split the array with `size` +/// and fill the empty slots with the `default` argument +pub fn batch(value: &Value, args: &HashMap) -> Result { + let arr = try_get_value!("batch", "value", Vec, value); + if arr.is_empty() { + return Ok(arr.into()); + } + + let size = match args.get("size") { + Some(val) => get_index(try_get_value!("batch", "size", f64, val), &arr), + None => 0, + }; + let value = args.get("default").unwrap_or(&Value::Null); + + let arr = arr.chunks(size) + .map(|chunk| { + let mut chunk = chunk.to_vec(); + if chunk.len() < size { + for _ in 0..(size - chunk.len()) { + chunk.push(value.clone()); + } + } + chunk + }) + .collect::>>(); + Ok(to_value(arr).unwrap()) +} + #[cfg(test)] mod tests { use super::*; @@ -1331,4 +1359,43 @@ mod tests { assert!(res.is_ok()); assert_eq!(res.unwrap(), to_value(expected).unwrap()); } + + #[test] + fn test_batch() { + let input = json!([ + {"id": 1, "year": 2015}, + {"id": 2, "year": true}, + {"id": 3, "year": 2016.5}, + {"id": 4, "year": "2017"}, + {"id": 5, "year": 2017}, + {"id": 6, "year": 2017}, + {"id": 7, "year": [1900, 1901]}, + ]); + let mut args = HashMap::new(); + args.insert("size".to_string(), to_value(3).unwrap()); + args.insert("default".to_string(), json!({"id": 0})); + + let expected = + json!([ + [ + {"id": 1, "year": 2015}, + {"id": 2, "year": true}, + {"id": 3, "year": 2016.5}, + ], + [ + {"id": 4, "year": "2017"}, + {"id": 5, "year": 2017}, + {"id": 6, "year": 2017}, + ], + [ + {"id": 7, "year": [1900, 1901]}, + {"id": 0}, + {"id": 0} + ] + ]); + + let res = batch(&input, &args); + assert!(res.is_ok()); + assert_eq!(res.unwrap(), to_value(expected).unwrap()); + } } diff --git a/src/tera.rs b/src/tera.rs index 0c694f5c..24acdc2b 100644 --- a/src/tera.rs +++ b/src/tera.rs @@ -710,6 +710,7 @@ impl Tera { self.register_filter("filter", array::filter); self.register_filter("map", array::map); self.register_filter("concat", array::concat); + self.register_filter("batch", array::batch); self.register_filter("abs", number::abs); self.register_filter("pluralize", number::pluralize); From 2f1faf8fc7f9108583e04dc8db2d75cfc66a9e50 Mon Sep 17 00:00:00 2001 From: oonxt Date: Wed, 12 Feb 2025 10:31:25 +0800 Subject: [PATCH 4/6] remove #![deny(missing_docs)] lint --- src/lib.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index c1dd3c8c..11cda5c8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -56,8 +56,6 @@ //! [Jinja2]: http://jinja.pocoo.org/ //! [Django]: https://docs.djangoproject.com/en/3.1/topics/templates/ -#![deny(missing_docs)] - #[macro_use] mod macros; mod builtins; From a419cb0c4861cfefcebabcf34d7cccf004c5235f Mon Sep 17 00:00:00 2001 From: oonxt Date: Wed, 12 Feb 2025 11:45:11 +0800 Subject: [PATCH 5/6] add `key` path support --- src/builtins/filters/object.rs | 63 ++++++++++++++++++++++++++++++---- 1 file changed, 56 insertions(+), 7 deletions(-) diff --git a/src/builtins/filters/object.rs b/src/builtins/filters/object.rs index f9981450..99cc30b2 100644 --- a/src/builtins/filters/object.rs +++ b/src/builtins/filters/object.rs @@ -1,9 +1,10 @@ /// Filters operating on numbers use std::collections::HashMap; -use serde_json::value::Value; +use crate::dotted_pointer; use crate::errors::{Error, Result}; +use serde_json::value::Value; /// Returns a value by a `key` argument from a given object pub fn get(value: &Value, args: &HashMap) -> Result { @@ -13,10 +14,8 @@ pub fn get(value: &Value, args: &HashMap) -> Result { None => return Err(Error::msg("The `get` filter has to have an `key` argument")), }; - match value.as_object() { - Some(o) => match o.get(&key) { - Some(val) => Ok(val.clone()), - // If the value is not present, allow for an optional default value + if value.is_object() { + match dotted_pointer(&value, &key) { None => match default { Some(def) => Ok(def.clone()), None => Err(Error::msg(format!( @@ -24,8 +23,10 @@ pub fn get(value: &Value, args: &HashMap) -> Result { &key ))), }, - }, - None => Err(Error::msg("Filter `get` was used on a value that isn't an object")), + Some(val) => Ok(val.clone()), + } + } else { + Err(Error::msg("Filter `get` was used on a value that isn't an object")) } } @@ -34,6 +35,8 @@ mod tests { use super::*; use serde_json::value::to_value; use std::collections::HashMap; + use serde_json::json; + use crate::filters::array::batch; #[test] fn test_get_filter_exists() { @@ -87,4 +90,50 @@ mod tests { assert!(result.is_ok()); assert_eq!(result.unwrap(), to_value("default").unwrap()); } + + #[test] + fn test_get_attribute() { + let input = json!({"id": 7, "year": [1900, 1901], "children": [{"id": 0}]}); + let mut args = HashMap::new(); + args.insert("key".to_string(), to_value("id").unwrap()); + args.insert("default".to_string(), to_value(3).unwrap()); + + let expected = json!(7); + + let res = get(&input, &args); + assert!(res.is_ok()); + assert_eq!(res.unwrap(), to_value(expected).unwrap()); + + + let mut args = HashMap::new(); + args.insert("key".to_string(), to_value("id2").unwrap()); + args.insert("default".to_string(), to_value(3).unwrap()); + + let expected = json!(3); + + let res = get(&input, &args); + assert!(res.is_ok()); + assert_eq!(res.unwrap(), to_value(expected).unwrap()); + + + let mut args = HashMap::new(); + args.insert("key".to_string(), to_value("year.3").unwrap()); + args.insert("default".to_string(), to_value(3).unwrap()); + + let expected = json!(3); + + let res = get(&input, &args); + assert!(res.is_ok()); + assert_eq!(res.unwrap(), to_value(expected).unwrap()); + + let mut args = HashMap::new(); + args.insert("key".to_string(), to_value("children.0.id").unwrap()); + args.insert("default".to_string(), to_value(3).unwrap()); + + let expected = json!(0); + + let res = get(&input, &args); + assert!(res.is_ok()); + assert_eq!(res.unwrap(), to_value(expected).unwrap()); + } } From 9e4462a4415b8547aaffdaad2cf46e9258057314 Mon Sep 17 00:00:00 2001 From: oonxt Date: Thu, 13 Feb 2025 12:00:58 +0800 Subject: [PATCH 6/6] batch filter must have size argument --- src/builtins/filters/array.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/builtins/filters/array.rs b/src/builtins/filters/array.rs index 554a20d4..f56a888b 100644 --- a/src/builtins/filters/array.rs +++ b/src/builtins/filters/array.rs @@ -493,7 +493,7 @@ pub fn batch(value: &Value, args: &HashMap) -> Result { let size = match args.get("size") { Some(val) => get_index(try_get_value!("batch", "size", f64, val), &arr), - None => 0, + None => return Err(Error::msg("The `batch` filter has to have a `size` argument")), }; let value = args.get("default").unwrap_or(&Value::Null);