From 7517d9e18442b4f91d1b52b75eea81c6bdc0a4a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Konsta=20H=C3=B6ltt=C3=A4?= Date: Tue, 21 Jul 2020 21:54:26 +0300 Subject: [PATCH] add take_while, take_until filters Add a take_while filter that mirrors Iterator::take_while in a similar fashion to the "filter" filter, comparing attributes against a value or against null if no value is given. Add also take_until to do the reverse, producing elements that do not match a given value. --- docs/content/docs/_index.md | 66 ++++++++++ src/builtins/filters/array.rs | 242 ++++++++++++++++++++++++++++++++++ src/renderer/tests/basic.rs | 111 ++++++++++++++++ src/tera.rs | 2 + 4 files changed, 421 insertions(+) diff --git a/docs/content/docs/_index.md b/docs/content/docs/_index.md index 1dae41cdc..95efe5e5d 100644 --- a/docs/content/docs/_index.md +++ b/docs/content/docs/_index.md @@ -996,6 +996,72 @@ or by author name: If `value` is not passed, it will drop any elements where the attribute is `null`. +#### take_while +Slice an array by a filter. Returns a slice of the array values that occur from the start before an +element for which `attribute` is not equal to `value`. Unlike the `filter` filter, this stops at the +first element that does not match. + +If `value` is not passed, produces elements while the attribute is not null. + +If the first element does not match, an empty array is returned. If all elements match, the whole array is returned. + +`attribute` is mandatory. + +Example: + +Given `sorted_orders` is an array of `Crate` + +```rust +struct Crate { + label: String, + destination: String, + // ... +} +``` + +```jinja2 +Next cargo ship to the happy place will be filled with +{{ sorted_orders | take_while(attribute="destination", value="Finland") | len }} +crates. +``` + +#### take_until +Slice an array by a filter. Returns a slice of the array values that occur from the start before an +element for which `attribute` is equal to `value`. + +If `value` is not passed, produces elements while the attribute is null. + +If the first element matches, an empty array is returned. If no elements match, the whole array is returned. + +`attribute` is mandatory. + +Example: + +Given `all_posts` is an array of `Post` and `current_page` is a single `Post` + +```rust +struct Post { + title: String, + // unique to the nanosecond level + timestamp: String, + // ... +} +``` + +The `attribute` argument with a `value` can be used to find the older and newer posts compared to a current post: + +```jinja2 +Number of posts older than this one: +{{ all_posts | sort(attribute="timestamp") + | take_until(attribute="timestamp", value=current_page.timestamp) | len }} +``` + +```jinja2 +You should read the next post titled: +{{ all_posts | sort(attribute="timestamp") | reverse + | take_until(attribute="timestamp", value=current_page.timestamp) | last }} +``` + #### map Retrieves an attribute from each object in an array. The `attribute` argument is mandatory and specifies what to extract. diff --git a/src/builtins/filters/array.rs b/src/builtins/filters/array.rs index a13e62ad4..87371e63c 100644 --- a/src/builtins/filters/array.rs +++ b/src/builtins/filters/array.rs @@ -215,6 +215,68 @@ pub fn filter(value: &Value, args: &HashMap) -> Result { Ok(to_value(arr).unwrap()) } +/// Slice an array by a filter. Returns a slice of the array values that occur from the start +/// before an element for which `attribute` is not equal to `value`. If `value` is not passed, +/// produces elements while the element attribute is not null. +pub fn take_while(value: &Value, args: &HashMap) -> Result { + let arr = try_get_value!("take_while", "value", Vec, value); + if arr.is_empty() { + return Ok(arr.into()); + } + + let key = match args.get("attribute") { + Some(val) => try_get_value!("take_while", "attribute", String, val), + None => return Err(Error::msg("The `take_while` filter has to have an `attribute` argument")), + }; + let boundary_value = args.get("value").unwrap_or(&Value::Null); + + let json_pointer = get_json_pointer(&key); + let output = arr + .into_iter() + .take_while(|v| { + let val = v.pointer(&json_pointer).unwrap_or(&Value::Null); + if boundary_value.is_null() { + !val.is_null() + } else { + val == boundary_value + } + }) + .collect::>(); + + Ok(to_value(output).unwrap()) +} + +/// Slice an array by a filter. Returns a slice of the array values that occur from the start +/// before an element for which `attribute` is equal to `value`. If `value` is not passed, +/// produces elements while the element attribute is null. +pub fn take_until(value: &Value, args: &HashMap) -> Result { + let arr = try_get_value!("take_until", "value", Vec, value); + if arr.is_empty() { + return Ok(arr.into()); + } + + let key = match args.get("attribute") { + Some(val) => try_get_value!("take_until", "attribute", String, val), + None => return Err(Error::msg("The `take_until` filter has to have an `attribute` argument")), + }; + let boundary_value = args.get("value").unwrap_or(&Value::Null); + + let json_pointer = get_json_pointer(&key); + let output = arr + .into_iter() + .take_while(|v| { + let val = v.pointer(&json_pointer).unwrap_or(&Value::Null); + if boundary_value.is_null() { + val.is_null() + } else { + val != boundary_value + } + }) + .collect::>(); + + Ok(to_value(output).unwrap()) +} + /// Map retrieves an attribute from a list of objects. /// The 'attribute' argument specifies what to retrieve. pub fn map(value: &Value, args: &HashMap) -> Result { @@ -760,6 +822,186 @@ mod tests { assert_eq!(res.unwrap(), to_value(expected).unwrap()); } + #[test] + fn test_take_while_empty() { + let res = take_while(&json!([]), &HashMap::new()); + assert!(res.is_ok()); + assert_eq!(res.unwrap(), json!([])); + } + + #[test] + fn test_take_while_value_match() { + let input = json!([ + {"id": 1, "year": 2015}, + {"id": 2, "year": 2015}, + {"id": 3, "year": 2016}, + {"id": 4, "year": 2015}, + ]); + let mut args = HashMap::new(); + args.insert("attribute".to_string(), to_value("year").unwrap()); + args.insert("value".to_string(), to_value(2015).unwrap()); + + let expected = json!([ + {"id": 1, "year": 2015}, + {"id": 2, "year": 2015}, + ]); + + let res = take_while(&input, &args); + assert!(res.is_ok()); + assert_eq!(res.unwrap(), to_value(expected).unwrap()); + } + + #[test] + fn test_take_while_value_nomatch() { + let input = json!([ + {"id": 1, "year": 2015}, + {"id": 2, "year": 2015}, + {"id": 3, "year": 2016}, + {"id": 4, "year": 2015}, + ]); + let mut args = HashMap::new(); + args.insert("attribute".to_string(), to_value("year").unwrap()); + args.insert("value".to_string(), to_value(2016).unwrap()); + + let expected = json!([ + ]); + + let res = take_while(&input, &args); + assert!(res.is_ok()); + assert_eq!(res.unwrap(), to_value(expected).unwrap()); + } + + #[test] + fn test_take_while_novalue_match() { + let input = json!([ + {"id": 1, "year": 2015}, + {"id": 2, "year": 2015}, + {"id": 3}, + {"id": 4, "year": 2016}, + ]); + let mut args = HashMap::new(); + args.insert("attribute".to_string(), to_value("year").unwrap()); + + let expected = json!([ + {"id": 1, "year": 2015}, + {"id": 2, "year": 2015}, + ]); + + let res = take_while(&input, &args); + assert!(res.is_ok()); + assert_eq!(res.unwrap(), to_value(expected).unwrap()); + } + + #[test] + fn test_take_while_novalue_nomatch() { + let input = json!([ + {"id": 1, "year": 2015}, + {"id": 2, "year": 2015}, + ]); + let mut args = HashMap::new(); + args.insert("attribute".to_string(), to_value("year").unwrap()); + + let expected = json!([ + {"id": 1, "year": 2015}, + {"id": 2, "year": 2015}, + ]); + + let res = take_while(&input, &args); + assert!(res.is_ok()); + assert_eq!(res.unwrap(), to_value(expected).unwrap()); + } + + #[test] + fn test_take_until_empty() { + let res = take_until(&json!([]), &HashMap::new()); + assert!(res.is_ok()); + assert_eq!(res.unwrap(), json!([])); + } + + #[test] + fn test_take_until_value_match() { + let input = json!([ + {"id": 1, "year": 2015}, + {"id": 2, "year": 2015}, + {"id": 3, "year": 2016}, + {"id": 4, "year": 2015}, + ]); + let mut args = HashMap::new(); + args.insert("attribute".to_string(), to_value("year").unwrap()); + args.insert("value".to_string(), to_value(2016).unwrap()); + + let expected = json!([ + {"id": 1, "year": 2015}, + {"id": 2, "year": 2015}, + ]); + + let res = take_until(&input, &args); + assert!(res.is_ok()); + assert_eq!(res.unwrap(), to_value(expected).unwrap()); + } + + #[test] + fn test_take_until_value_nomatch() { + let input = json!([ + {"id": 1, "year": 2015}, + {"id": 2, "year": 2015}, + {"id": 3, "year": 2016}, + {"id": 4, "year": 2015}, + ]); + let mut args = HashMap::new(); + args.insert("attribute".to_string(), to_value("year").unwrap()); + args.insert("value".to_string(), to_value(2014).unwrap()); + + let expected = json!([ + {"id": 1, "year": 2015}, + {"id": 2, "year": 2015}, + {"id": 3, "year": 2016}, + {"id": 4, "year": 2015}, + ]); + + let res = take_until(&input, &args); + assert!(res.is_ok()); + assert_eq!(res.unwrap(), to_value(expected).unwrap()); + } + + #[test] + fn test_take_until_novalue_match() { + let input = json!([ + {"id": 1}, + {"id": 2, "year": 2015}, + {"id": 3}, + ]); + let mut args = HashMap::new(); + args.insert("attribute".to_string(), to_value("year").unwrap()); + + let expected = json!([ + {"id": 1}, + ]); + + let res = take_until(&input, &args); + assert!(res.is_ok()); + assert_eq!(res.unwrap(), to_value(expected).unwrap()); + } + + #[test] + fn test_take_until_novalue_nomatch() { + let input = json!([ + {"id": 1}, + {"id": 2}, + ]); + let mut args = HashMap::new(); + args.insert("attribute".to_string(), to_value("year").unwrap()); + + let expected = json!([ + {"id": 1}, + {"id": 2}, + ]); + + let res = take_until(&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()); diff --git a/src/renderer/tests/basic.rs b/src/renderer/tests/basic.rs index dfba4c2fc..f56bccf60 100644 --- a/src/renderer/tests/basic.rs +++ b/src/renderer/tests/basic.rs @@ -633,6 +633,117 @@ fn filter_filter_works() { } } +#[test] +fn take_while_filter_with_value_works() { + #[derive(Debug, Serialize)] + struct Author { + id: u8, + age: u8, + }; + + let mut context = Context::new(); + context.insert("authors", &vec![ + Author { id: 1, age: 42 }, + Author { id: 2, age: 42 }, + Author { id: 3, age: 5 }, + ]); + + let inputs = vec![ + (r#"{{ authors | take_while(attribute="age", value=42) | first | get(key="id") }}"#, "1"), + (r#"{{ authors | take_while(attribute="age", value=42) | last | get(key="id") }}"#, "2"), + (r#"{{ authors | take_while(attribute="batman", value="bruce") }}"#, "[]"), + ]; + + for (input, expected) in inputs { + println!("{:?} -> {:?}", input, expected); + assert_eq!(render_template(input, &context).unwrap(), expected); + } +} + +#[test] +fn take_while_filter_without_value_works() { + #[derive(Debug, Serialize)] + struct Author { + id: u8, + publications: HashMap<&'static str, u8>, + }; + + let mut context = Context::new(); + context.insert("authors", &vec![ + Author { id: 1, publications: [("books", 1)].iter().cloned().collect() }, + Author { id: 2, publications: [("books", 2)].iter().cloned().collect() }, + Author { id: 3, publications: [("papers", 42)].iter().cloned().collect() }, + ]); + + let inputs = vec![ + (r#"{{ authors | take_while(attribute="publications.books") | first | get(key="id") }}"#, "1"), + (r#"{{ authors | take_while(attribute="publications.books") | last | get(key="id") }}"#, "2"), + (r#"{{ authors | take_while(attribute="publications.batman") }}"#, "[]"), + ]; + + for (input, expected) in inputs { + println!("{:?} -> {:?}", input, expected); + assert_eq!(render_template(input, &context).unwrap(), expected); + } +} + +#[test] +fn take_until_filter_with_value_works() { + #[derive(Debug, Serialize)] + struct Author { + id: u8, + age: u8, + }; + + let mut context = Context::new(); + context.insert("authors", &vec![ + Author { id: 1, age: 42 }, + Author { id: 2, age: 42 }, + Author { id: 3, age: 5 }, + Author { id: 4, age: 99 }, + Author { id: 5, age: 99 }, + ]); + + let inputs = vec![ + (r#"{{ authors | take_until(attribute="age", value=5) | first | get(key="id") }}"#, "1"), + (r#"{{ authors | take_until(attribute="age", value=5) | last | get(key="id") }}"#, "2"), + (r#"{{ authors | reverse | take_until(attribute="age", value=5) | last | get(key="id") }}"#, "4"), + (r#"{{ authors | reverse | take_until(attribute="age", value=5) | first | get(key="id") }}"#, "5"), + ]; + + for (input, expected) in inputs { + println!("{:?} -> {:?}", input, expected); + assert_eq!(render_template(input, &context).unwrap(), expected); + } +} + +#[test] +fn take_until_filter_without_value_works() { + #[derive(Debug, Serialize)] + struct Author { + id: u8, + publications: HashMap<&'static str, u8>, + }; + + let mut context = Context::new(); + context.insert("authors", &vec![ + Author { id: 1, publications: [("books", 1)].iter().cloned().collect() }, + Author { id: 2, publications: [("books", 2)].iter().cloned().collect() }, + Author { id: 3, publications: [("papers", 42)].iter().cloned().collect() }, + ]); + + let inputs = vec![ + (r#"{{ authors | take_until(attribute="publications.papers") | last | get(key="id") }}"#, "2"), + (r#"{{ authors | take_until(attribute="publications.batman") | length }}"#, "3"), + (r#"{{ authors | take_until(attribute="publications.books") }}"#, "[]"), + ]; + + for (input, expected) in inputs { + println!("{:?} -> {:?}", input, expected); + assert_eq!(render_template(input, &context).unwrap(), expected); + } +} + #[test] fn filter_on_array_literal_works() { let mut context = Context::new(); diff --git a/src/tera.rs b/src/tera.rs index a9f32b687..3790e3efa 100644 --- a/src/tera.rs +++ b/src/tera.rs @@ -549,6 +549,8 @@ impl Tera { self.register_filter("slice", array::slice); self.register_filter("group_by", array::group_by); self.register_filter("filter", array::filter); + self.register_filter("take_while", array::take_while); + self.register_filter("take_until", array::take_until); self.register_filter("map", array::map); self.register_filter("concat", array::concat);