From 6db5f79743344c1392181b170dd998a640746e3c Mon Sep 17 00:00:00 2001 From: JMARyA Date: Fri, 9 Feb 2024 14:34:12 +0100 Subject: [PATCH] refactor; add order() fn; add examples --- Cargo.lock | 2 +- Cargo.toml | 2 +- README.md | 32 ++++++++- examples/array.rs | 38 +++++++++++ examples/comparison.rs | 16 +++++ examples/error_handling.rs | 28 ++++++++ examples/filter.rs | 17 +++++ examples/nested_filters.rs | 53 +++++++++++++++ examples/range.rs | 38 +++++++++++ examples/regex.rs | 39 +++++++++++ src/lib.rs | 134 ++++++++++++++++++++++++++++++++++--- 11 files changed, 388 insertions(+), 11 deletions(-) create mode 100644 examples/array.rs create mode 100644 examples/comparison.rs create mode 100644 examples/error_handling.rs create mode 100644 examples/filter.rs create mode 100644 examples/nested_filters.rs create mode 100644 examples/range.rs create mode 100644 examples/regex.rs diff --git a/Cargo.lock b/Cargo.lock index d7bf1c1..21b3ce3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,7 +19,7 @@ checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" [[package]] name = "jsonfilter" -version = "0.1.0" +version = "0.2.0" dependencies = [ "regex", "serde", diff --git a/Cargo.toml b/Cargo.toml index e91208e..aa5ca05 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "jsonfilter" -version = "0.1.0" +version = "0.2.0" edition = "2021" [dependencies] diff --git a/README.md b/README.md index aff371a..198bf9f 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,32 @@ # JSONFilter -JSONFilter is a rust crate letting you filter JSON objects based on another json object as a filter. Think of MongoDBs `find()` function but as a filter. \ No newline at end of file +`jsonfilter` is a Rust crate designed to facilitate filtering and comparing JSON values based on specified criteria. It provides functions for comparing JSON values, applying filters to JSON objects, and determining if a filter matches a given JSON object. Think of MongoDBs `find()` function but as a filter function. + +## Usage +To use `jsonfilter`, add it to your `Cargo.toml` and add the following to your Rust code: +```rust +use jsonfilter::{order, matches}; +``` + +### Comparing JSON Values +You can compare two JSON values using the `order` function: +```rust +use serde_json::json; +use std::cmp::Ordering; +use jsonfilter::order; + +let a = json!(10); +let b = json!(5); +assert_eq!(order(&a, &b), Ordering::Greater); +``` + +### Matching Filters +To check if a JSON object matches a filter, use the `matches` function: +```rust +use serde_json::json; +use jsonfilter::matches; + +let filter = json!({"name": "John", "age": 30}); +let obj = json!({"name": "John", "age": 30, "city": "New York"}); + +assert!(matches(&filter, &obj)); +``` diff --git a/examples/array.rs b/examples/array.rs new file mode 100644 index 0000000..f2dd3fe --- /dev/null +++ b/examples/array.rs @@ -0,0 +1,38 @@ +use jsonfilter::{try_matches, FilterError}; +use serde_json::{json, Value}; + +fn main() { + let filter = json!({"$or": [{"tags": {"$in": "programming"}},{"tags": {"$in": "rust"}}]}); + + let obj1 = json!({"name": "John Doe", "tags": ["rust", "programming", "development"]}); + let obj2 = json!({"name": "Alice Smith", "tags": ["web", "development", "javascript"]}); + let obj3 = json!({"name": "Bob Brown", "tags": ["python", "programming", "machine learning"]}); + + println!("Filter:"); + println!("{}", filter); + println!("Objects:"); + println!("Object 1: {}", obj1); + println!("Object 2: {}", obj2); + println!("Object 3: {}", obj3); + + match_objects(&filter, &obj1); + match_objects(&filter, &obj2); + match_objects(&filter, &obj3); +} + +fn match_objects(filter: &Value, obj: &Value) { + match try_matches(filter, obj) { + Ok(result) => { + if result { + println!("Filter matches the object"); + } else { + println!("Filter does not match the object"); + } + } + Err(err) => match err { + FilterError::InvalidFilter => println!("Invalid filter"), + FilterError::UnknownOperator => println!("Unknown operator in filter"), + FilterError::KeyNotFound => println!("Key not found in object"), + }, + } +} diff --git a/examples/comparison.rs b/examples/comparison.rs new file mode 100644 index 0000000..18db310 --- /dev/null +++ b/examples/comparison.rs @@ -0,0 +1,16 @@ +use jsonfilter::order; +use serde_json::json; +use std::cmp::Ordering; + +fn main() { + let a = json!(10); + let b = json!(5); + println!("Comparing JSON values:"); + println!("a: {}", a); + println!("b: {}", b); + match order(&a, &b) { + Ordering::Greater => println!("a is greater than b"), + Ordering::Less => println!("a is less than b"), + Ordering::Equal => println!("a is equal to b"), + } +} diff --git a/examples/error_handling.rs b/examples/error_handling.rs new file mode 100644 index 0000000..538bf13 --- /dev/null +++ b/examples/error_handling.rs @@ -0,0 +1,28 @@ +use jsonfilter::{try_matches, FilterError}; +use serde_json::json; + +fn main() { + let filter = json!({"name": "John", "other": "key"}); + let obj = json!({"name": "John", "age": 30}); + + println!("Applying filter:"); + println!("{}", filter); + println!("To object:"); + println!("{}", obj); + + match try_matches(&filter, &obj) { + Ok(result) => { + if result { + println!("Filter matches the object"); + } else { + println!("Filter does not match the object"); + } + } + // FilterError::KeyNotFound mostly means the object did not pass the filter, but it's still an error though + Err(err) => match err { + FilterError::InvalidFilter => println!("Invalid filter"), + FilterError::UnknownOperator => println!("Unknown operator in filter"), + FilterError::KeyNotFound => println!("Key not found in object"), + }, + } +} diff --git a/examples/filter.rs b/examples/filter.rs new file mode 100644 index 0000000..e7f98bb --- /dev/null +++ b/examples/filter.rs @@ -0,0 +1,17 @@ +use jsonfilter::matches; +use serde_json::json; + +fn main() { + let filter = json!({"name": "John", "age": 30}); + let obj = json!({"name": "John", "age": 30, "city": "New York"}); + + println!("Applying filter:"); + println!("{}", filter); + println!("To object:"); + println!("{}", obj); + if matches(&filter, &obj) { + println!("Filter matches the object"); + } else { + println!("Filter does not match the object"); + } +} diff --git a/examples/nested_filters.rs b/examples/nested_filters.rs new file mode 100644 index 0000000..dce5095 --- /dev/null +++ b/examples/nested_filters.rs @@ -0,0 +1,53 @@ +use jsonfilter::{try_matches, FilterError}; +use serde_json::{json, Value}; + +fn main() { + // Filter can be as complex as you want + let filter = json!({ + "$and": [ + {"$or": [ + {"age": {"$gte": 18}}, + {"is_student": true} + ]}, + {"$not": { + "$or": [ + {"city": "New York"}, + {"city": "Los Angeles"} + ] + }} + ] + }); + + let obj1 = json!({"age": 25, "is_student": false, "city": "Chicago"}); + let obj2 = json!({"age": 16, "is_student": true, "city": "Miami"}); + let obj3 = json!({"age": 30, "is_student": false, "city": "New York"}); + + println!("Filter:"); + println!("{}", filter); + println!("Objects:"); + println!("Object 1: {}", obj1); + println!("Object 2: {}", obj2); + println!("Object 3: {}", obj3); + + // Matching objects against the filter + match_objects(&filter, &obj1); + match_objects(&filter, &obj2); + match_objects(&filter, &obj3); +} + +fn match_objects(filter: &Value, obj: &Value) { + match try_matches(filter, obj) { + Ok(result) => { + if result { + println!("Filter matches the object"); + } else { + println!("Filter does not match the object"); + } + } + Err(err) => match err { + FilterError::InvalidFilter => println!("Invalid filter"), + FilterError::UnknownOperator => println!("Unknown operator in filter"), + FilterError::KeyNotFound => println!("Key not found in object"), + }, + } +} diff --git a/examples/range.rs b/examples/range.rs new file mode 100644 index 0000000..01c9a64 --- /dev/null +++ b/examples/range.rs @@ -0,0 +1,38 @@ +use jsonfilter::{try_matches, FilterError}; +use serde_json::{json, Value}; + +fn main() { + let filter = json!({"$and": [{"age": {"$gte": 18}}, {"age": {"$lte": 30}}]}); + + let obj1 = json!({"name": "John Doe", "age": 25}); + let obj2 = json!({"name": "Alice Smith", "age": 35}); + let obj3 = json!({"name": "Bob Brown", "age": 20}); + + println!("Filter:"); + println!("{}", filter); + println!("Objects:"); + println!("Object 1: {}", obj1); + println!("Object 2: {}", obj2); + println!("Object 3: {}", obj3); + + match_objects(&filter, &obj1); + match_objects(&filter, &obj2); + match_objects(&filter, &obj3); +} + +fn match_objects(filter: &Value, obj: &Value) { + match try_matches(filter, obj) { + Ok(result) => { + if result { + println!("Filter matches the object"); + } else { + println!("Filter does not match the object"); + } + } + Err(err) => match err { + FilterError::InvalidFilter => println!("Invalid filter"), + FilterError::UnknownOperator => println!("Unknown operator in filter"), + FilterError::KeyNotFound => println!("Key not found in object"), + }, + } +} diff --git a/examples/regex.rs b/examples/regex.rs new file mode 100644 index 0000000..7245aa1 --- /dev/null +++ b/examples/regex.rs @@ -0,0 +1,39 @@ +use jsonfilter::{try_matches, FilterError}; +use serde_json::{json, Value}; + +fn main() { + // You can filter text with regex + let filter = json!({"description": {"$regex": "(Rust|Python)"}}); + + let obj1 = json!({"name": "John Doe", "description": "Enthusiastic about programming in Rust"}); + let obj2 = json!({"name": "Alice Smith", "description": "Experienced in web development"}); + let obj3 = json!({"name": "Bob Brown", "description": "Loves to code in Python"}); + + println!("Filter:"); + println!("{}", filter); + println!("Objects:"); + println!("Object 1: {}", obj1); + println!("Object 2: {}", obj2); + println!("Object 3: {}", obj3); + + match_objects(&filter, &obj1); + match_objects(&filter, &obj2); + match_objects(&filter, &obj3); +} + +fn match_objects(filter: &Value, obj: &Value) { + match try_matches(filter, obj) { + Ok(result) => { + if result { + println!("Filter matches the object"); + } else { + println!("Filter does not match the object"); + } + } + Err(err) => match err { + FilterError::InvalidFilter => println!("Invalid filter"), + FilterError::UnknownOperator => println!("Unknown operator in filter"), + FilterError::KeyNotFound => println!("Key not found in object"), + }, + } +} diff --git a/src/lib.rs b/src/lib.rs index e9b8338..a317432 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,73 @@ mod test; +use std::cmp::Ordering; + use serde_json::json; +/// Compares two JSON values `a` and `b` based on their ordering. +/// +/// This function evaluates the relationship between the JSON values `a` and `b` using the following criteria: +/// - If `a` is greater than `b`, returns `std::cmp::Ordering::Greater`. +/// - If `a` is less than `b`, returns `std::cmp::Ordering::Less`. +/// - If `a` is equal to `b`, returns `std::cmp::Ordering::Equal`. +/// +/// # Arguments +/// +/// * `a` - A reference to a `serde_json::Value` representing the first JSON value. +/// * `b` - A reference to a `serde_json::Value` representing the second JSON value. +/// +/// # Returns +/// +/// Returns `std::cmp::Ordering` indicating the relationship between `a` and `b`. +/// +/// # Panics +/// +/// This function panics if the values cant be compared. +/// +/// # Examples +/// +/// ``` +/// use serde_json::json; +/// use std::cmp::Ordering; +/// use jsonfilter::order; +/// +/// let a = json!(10); +/// let b = json!(5); +/// assert_eq!(order(&a, &b), Ordering::Greater); +/// ``` +#[must_use] +pub fn order(a: &serde_json::Value, b: &serde_json::Value) -> std::cmp::Ordering { + if matches( + &serde_json::json!({ + "a": { "$gt": b} + }), + &serde_json::json!({ + "a": a + }), + ) { + Ordering::Greater + } else if matches( + &serde_json::json!({ + "a": { "$lt": b} + }), + &serde_json::json!({ + "a": a + }), + ) { + Ordering::Less + } else if matches( + &serde_json::json!({ + "a": b + }), + &serde_json::json!({ + "a": a + }), + ) { + Ordering::Equal + } else { + unreachable!() + } +} + fn less_than_num(a: &serde_json::Value, b: &serde_json::Value) -> bool { if a.is_f64() { let a = a.as_f64().unwrap(); @@ -14,8 +81,12 @@ fn less_than_num(a: &serde_json::Value, b: &serde_json::Value) -> bool { let a = a.as_u64().unwrap(); let b = b.as_u64().unwrap(); a < b + } else if a.is_string() { + let a = a.as_str().unwrap(); + let b = b.as_str().unwrap(); + a < b } else { - false + unreachable!() } } @@ -32,8 +103,12 @@ fn greater_than_num(a: &serde_json::Value, b: &serde_json::Value) -> bool { let a = a.as_u64().unwrap(); let b = b.as_u64().unwrap(); a > b + } else if a.is_string() { + let a = a.as_str().unwrap(); + let b = b.as_str().unwrap(); + a > b } else { - false + unreachable!() } } @@ -89,21 +164,34 @@ pub fn try_matches( match op.as_str() { "$and" => { if let serde_json::Value::Array(and_list) = op_arg { - let and_list_bool: Vec = and_list + let and_list_bool: Vec> = and_list .iter() - .map(|sub_filter| matches(sub_filter, obj)) + .map(|sub_filter| try_matches(sub_filter, obj)) .collect(); - return Ok(!and_list_bool.iter().any(|x| !x)); + if let Some(err) = and_list_bool.iter().find(|x| x.is_err()) { + return *err; + } + return Ok(!and_list_bool.iter().map(|x| x.unwrap()).any(|x| !x)); } return Err(FilterError::InvalidFilter); } "$or" => { if let serde_json::Value::Array(or_list) = op_arg { - let or_list_bool: Vec = or_list + let or_list_bool: Vec> = or_list .iter() - .map(|sub_filter| matches(sub_filter, obj)) + .map(|sub_filter| try_matches(sub_filter, obj)) .collect(); - return Ok(or_list_bool.iter().any(|x| *x)); + if let Some(err) = or_list_bool.iter().find(|x| x.is_err()) { + return *err; + } + return Ok(or_list_bool.iter().map(|x| x.unwrap()).any(|x| x)); + } + return Err(FilterError::InvalidFilter); + } + "$not" => { + if let Some(inner) = filter.get("$not") { + let new_filter = inner; + return Ok(!try_matches(new_filter, obj)?); } return Err(FilterError::InvalidFilter); } @@ -145,6 +233,10 @@ pub fn try_matches( } } + check(&conditions) +} + +fn check(conditions: &[Result]) -> Result { conditions.iter().find(|x| x.is_err()).map_or_else( || Ok(!conditions.iter().map(|x| x.unwrap()).any(|x| !x)), |possible_error| *possible_error, @@ -163,6 +255,32 @@ fn match_operator( let op = keys.first().unwrap().as_str(); let op_arg = val.get(op).unwrap(); match op { + "$and" => { + if let serde_json::Value::Array(and_list) = op_arg { + let and_list_bool: Vec> = and_list + .iter() + .map(|sub_filter| try_matches(sub_filter, raw_obj)) + .collect(); + if let Some(err) = and_list_bool.iter().find(|x| x.is_err()) { + return *err; + } + return Ok(!and_list_bool.iter().map(|x| x.unwrap()).any(|x| !x)); + } + return Err(FilterError::InvalidFilter); + } + "$or" => { + if let serde_json::Value::Array(or_list) = op_arg { + let or_list_bool: Vec> = or_list + .iter() + .map(|sub_filter| try_matches(sub_filter, raw_obj)) + .collect(); + if let Some(err) = or_list_bool.iter().find(|x| x.is_err()) { + return *err; + } + return Ok(or_list_bool.iter().map(|x| x.unwrap()).any(|x| x)); + } + return Err(FilterError::InvalidFilter); + } "$lt" => { if let Some(a) = obj.get(key) { return Ok(less_than_num(a, op_arg));