From 89517943f21737be0958dc0f0dabe5ea10d87013 Mon Sep 17 00:00:00 2001 From: JMARyA Date: Wed, 7 May 2025 09:03:34 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20any=20+=20all?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/filter.md | 12 ++++++++ src/lib.rs | 33 +++++++++++++++++++++- src/test.rs | 74 ++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 118 insertions(+), 1 deletion(-) diff --git a/docs/filter.md b/docs/filter.md index e022b98..f4d4be6 100644 --- a/docs/filter.md +++ b/docs/filter.md @@ -132,6 +132,18 @@ Evaluates to `true` if the value contains the text. { "key": { "$contains": "text" }} ``` +### `$any` +Evaluates to `true` if any value matches the filter. +```json +{ "array": { "$any": { "$regex": "text" }}} +``` + +### `$all` +Evaluates to `true` if all value matches the filter. +```json +{ "array": { "$all": { "$regex": "text" }}} +``` + ## Misc Operators ### `$type` Evaluates to `true` if the value matches the specified type. diff --git a/src/lib.rs b/src/lib.rs index bfc3c02..22bdbaf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -197,7 +197,6 @@ pub fn try_matches( obj: &serde_json::Value, ) -> Result { let filter = filter.as_object().unwrap(); - let obj_map = obj.as_object().unwrap(); // Handle the case where the filter has a single key, such as top level $and, $or, $not if filter.len() == 1 { @@ -238,6 +237,25 @@ pub fn try_matches( } return Err(FilterError::InvalidFilter); } + "$contains" | "$in" => match obj { + serde_json::Value::String(str) => return Ok(str.contains(op_arg.as_str().unwrap())), + serde_json::Value::Array(values) => { + return Ok(values.into_iter().any(|x| x == op_arg)) + } + _ => {} + }, + "$any" => { + let elem_filter = op_arg; + let list = obj.as_array().unwrap(); + return Ok(list.into_iter().any(|x| matches(&elem_filter, x))); + } + "$all" => { + let elem_filter = json!({ "key": op_arg }); + let list = obj.as_array().unwrap(); + return Ok(!list + .into_iter() + .any(|x| !matches(&elem_filter, &json!({"key": x})))); + } _ => { if op.starts_with('$') { return Err(FilterError::UnknownOperator); @@ -246,6 +264,7 @@ pub fn try_matches( } } + let obj_map = obj.as_object().unwrap(); let mut conditions = vec![]; for (key, val) in filter { @@ -509,6 +528,18 @@ fn match_operator( key, ); } + "$any" => { + let elem_filter = op_arg; + let list = raw_obj.get(key).unwrap().as_array().unwrap(); + return Ok(list.into_iter().any(|x| matches(&elem_filter, x))); + } + "$all" => { + let elem_filter = json!({ "key": op_arg }); + let list = raw_obj.get(key).unwrap().as_array().unwrap(); + return Ok(!list + .into_iter() + .any(|x| !matches(&elem_filter, &json!({"key": x})))); + } _ => { if op.starts_with('$') { return Err(FilterError::UnknownOperator); diff --git a/src/test.rs b/src/test.rs index 941b2a8..c62ea59 100644 --- a/src/test.rs +++ b/src/test.rs @@ -3,6 +3,80 @@ mod tests { use crate::matches; use serde_json::json; + #[test] + fn array_element_match() { + assert!(matches( + &json!( + { "a": { "$contains": 3 }} + ), + &json!({ "a": [1,2,3,4]}) + )); + } + + #[test] + fn array_any_str() { + assert!(matches( + &json!( + { "a": { "$any": { "$contains": "world"} }} + ), + &json!({ "a": ["hello", "world"]}) + )); + } + + #[test] + fn array_any_sub() { + assert!(matches( + &json!( + { "a": { "$any": { "key": { "$contains": "world"} }}} + ), + &json!({ "a": [{ "key": "hello" }, {"key": "world"}]}) + )); + } + + #[test] + fn array_all_nested() { + assert!(!matches( + &json!( + { "key": { "$all": { "$gt": 10} }} + ), + &json!({ "key": [1,2,3,4,5]}) + )); + + assert!(matches( + &json!( + { "key": { "$all": { "$gt": 5} }} + ), + &json!({ "key": [6,7,8,9]}) + )); + } + + #[test] + fn array_all_direct() { + assert!(!matches( + &json!( + { "$all": { "$gt": 10} } + ), + &json!([1, 2, 3, 4, 5]) + )); + + assert!(matches( + &json!( + { "$all": { "$gt": 5} } + ), + &json!([6, 7, 8, 9]) + )); + } + + #[test] + fn array_any_direct() { + assert!(matches( + &json!( + { "$any": { "$contains": "world"} } + ), + &json!(["hello", "world"]) + )); + } + #[test] fn simple_mask() { assert!(matches(