commit c9d1c372c913fbb655f5bf84d45122be71b319a9 Author: JMARyA Date: Fri Feb 9 10:21:27 2024 +0100 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..d7bf1c1 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,134 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aho-corasick" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +dependencies = [ + "memchr", +] + +[[package]] +name = "itoa" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" + +[[package]] +name = "jsonfilter" +version = "0.1.0" +dependencies = [ + "regex", + "serde", + "serde_json", +] + +[[package]] +name = "memchr" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" + +[[package]] +name = "proc-macro2" +version = "1.0.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" + +[[package]] +name = "ryu" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" + +[[package]] +name = "serde" +version = "1.0.196" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.196" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.113" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69801b70b1c3dac963ecb03a364ba0ceda9cf60c71cfe475e99864759c8b8a79" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "syn" +version = "2.0.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..e91208e --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "jsonfilter" +version = "0.1.0" +edition = "2021" + +[dependencies] +regex = "1.10.3" +serde = "1.0.196" +serde_json = "1.0.113" diff --git a/README.md b/README.md new file mode 100644 index 0000000..aff371a --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# 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 diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..8fa8e9a --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,250 @@ +use serde_json::json; + +mod test; + +fn less_than_num(a: &serde_json::Value, b: &serde_json::Value) -> bool { + if a.is_f64() { + let a = a.as_f64().unwrap(); + let b = b.as_f64().unwrap(); + return a < b; + } else if a.is_i64() { + let a = a.as_i64().unwrap(); + let b = b.as_i64().unwrap(); + return a < b; + } else if a.is_u64() { + let a = a.as_u64().unwrap(); + let b = b.as_u64().unwrap(); + return a < b; + } else { + return false; + } +} + +fn greater_than_num(a: &serde_json::Value, b: &serde_json::Value) -> bool { + if a.is_f64() { + let a = a.as_f64().unwrap(); + let b = b.as_f64().unwrap(); + return a > b; + } else if a.is_i64() { + let a = a.as_i64().unwrap(); + let b = b.as_i64().unwrap(); + return a > b; + } else if a.is_u64() { + let a = a.as_u64().unwrap(); + let b = b.as_u64().unwrap(); + return a > b; + } else { + return false; + } +} + +pub fn matches(filter: &serde_json::Value, raw_obj: &serde_json::Value) -> bool { + let filter = filter.as_object().unwrap(); + let obj = raw_obj.as_object().unwrap(); + + if filter.len() == 1 { + let filter_keys: Vec<_> = filter.keys().collect(); + let op = filter_keys.first().unwrap(); + let op_arg = filter.get(op.as_str()).unwrap(); + match op.as_str() { + "$and" => { + if let serde_json::Value::Array(and_list) = op_arg { + let and_list_bool: Vec = and_list + .iter() + .map(|sub_filter| matches(sub_filter, raw_obj)) + .collect(); + return !and_list_bool.iter().any(|x| !x); + } else { + return false; + } + } + "$or" => { + if let serde_json::Value::Array(or_list) = op_arg { + let or_list_bool: Vec = or_list + .iter() + .map(|sub_filter| matches(sub_filter, raw_obj)) + .collect(); + return or_list_bool.iter().any(|x| *x); + } else { + return false; + } + } + _ => { + if op.starts_with("$") { + unimplemented!() + } + } + } + } + + for (key, val) in filter.iter() { + if val.is_object() { + let val_keys: Vec<_> = val.as_object().unwrap().keys().collect(); + if val_keys.first().unwrap().starts_with("$") { + return match_operator(val, raw_obj, key.as_str()); + } else { + // nested + for (_, _) in val.as_object().unwrap() { + let new_filter = filter.get(key).unwrap(); + if let Some(val) = obj.get(key) { + return matches(new_filter, val); + } else { + return false; + } + } + } + } + + if let Some(valb) = obj.get(key) { + if val != valb { + return false; + } + } else { + return false; + } + } + + true +} + +fn match_operator(val: &serde_json::Value, raw_obj: &serde_json::Value, key: &str) -> bool { + let obj = raw_obj.as_object().unwrap(); + let val = val.as_object().unwrap(); + if val.keys().len() == 1 { + let keys: Vec<_> = val.keys().collect(); + let op = keys.first().unwrap().as_str(); + let op_arg = val.get(op).unwrap(); + match op { + "$lt" => { + if let Some(a) = obj.get(key) { + return less_than_num(a, op_arg); + } else { + return false; + } + } + "$lte" => { + if let Some(a) = obj.get(key) { + return less_than_num(a, op_arg) || a == op_arg; + } else { + return false; + } + } + "$gt" => { + if let Some(valb) = obj.get(key) { + return greater_than_num(valb, op_arg); + } else { + return false; + } + } + "$gte" => { + if let Some(a) = obj.get(key) { + return greater_than_num(a, op_arg) || a == op_arg; + } else { + return false; + } + } + "$not" => { + if let Some(inner) = val.get("$not") { + if let serde_json::Value::Object(inner) = inner { + let new_filter = json!({ + key: inner + }); + return !matches(&new_filter, raw_obj); + } else { + return false; + } + } else { + return false; + } + } + "$ne" => { + if let Some(valb) = obj.get(key) { + return valb != op_arg; + } else { + return false; + } + } + "$in" => { + if let Some(valb) = obj.get(key) { + if let serde_json::Value::Array(list) = valb { + return list.iter().any(|x| x == op_arg); + } else { + return false; + } + } else { + return false; + } + } + "$nin" => { + if let Some(valb) = obj.get(key) { + if let serde_json::Value::Array(list) = valb { + return !list.iter().any(|x| x == op_arg); + } else { + return false; + } + } else { + return false; + } + } + "$exists" => { + if let serde_json::Value::Bool(exists) = op_arg { + let valb = obj.get(key).is_some(); + return *exists == valb; + } else { + return false; + } + } + "$size" => { + if let Some(serde_json::Value::Array(list)) = obj.get(key) { + let val_size = list.len() as u64; + if let serde_json::Value::Number(pref_size) = op_arg { + let pref_size = pref_size.as_u64().unwrap(); + return pref_size == val_size; + } else { + return false; + } + } else { + return false; + } + } + "$regex" => { + if let serde_json::Value::String(regex_pattern) = op_arg { + if let Some(serde_json::Value::String(valb)) = obj.get(key) { + let pattern = regex::Regex::new(regex_pattern).unwrap(); + return pattern.is_match(valb); + } else { + return false; + } + } else { + return false; + } + } + "$type" => { + if let Some(valb) = obj.get(key) { + if let serde_json::Value::String(type_str) = op_arg { + return match type_str.to_lowercase().as_str() { + "null" => valb.is_null(), + "string" => valb.is_string(), + "number" => valb.is_number(), + "object" => valb.is_object(), + "array" => valb.is_array(), + "boolean" => valb.is_boolean(), + _ => false, + }; + } else { + return false; + } + } else { + return false; + } + } + _ => { + if op.starts_with("$") { + unimplemented!() + } + } + } + } + + return false; +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..06e3aae --- /dev/null +++ b/src/main.rs @@ -0,0 +1,30 @@ +use jsonfilter::matches; +use serde_json::json; + +fn main() { + assert!(matches( + &json!({ + "$and": [ + { "key": "value" }, + { "num": 5 } + ] + }), + &json!({ + "key": "value", + "num": 5 + }) + )); + + assert!(!matches( + &json!({ + "$and": [ + { "key": "value" }, + { "num": 5 } + ] + }), + &json!({ + "key": "value", + "num": 3 + }) + )); +} diff --git a/src/test.rs b/src/test.rs new file mode 100644 index 0000000..4a33182 --- /dev/null +++ b/src/test.rs @@ -0,0 +1,390 @@ +#[cfg(test)] +mod tests { + use crate::matches; + use serde_json::json; + + #[test] + fn simple_mask() { + assert!(matches( + &json!( + { "key": "value" } + ), + &json!({ + "key": "value", + "num": 3 + }) + )); + assert!(!matches( + &json!( + { "key": "value" } + ), + &json!({ + "key": "not_value", + "num": 3 + }) + )); + } + + #[test] + fn nested_mask() { + assert!(matches( + &json!({ + "key": { + "nested": "value" + } + }), + &json!({ + "key": { + "nested": "value" + } + }) + )); + } + + #[test] + fn not_equal() { + assert!(matches( + &json!({ + "key": { + "$ne": "value" + } + }), + &json!({ "key": "not_value"}) + )); + } + + #[test] + fn greater_than() { + assert!(!matches( + &json!({ + "key": { + "$gt": 5 + } + }), + &json!({ "key": 4}) + )); + assert!(!matches( + &json!({ + "key": { + "$gt": 5 + } + }), + &json!({ "key": 5}) + )); + assert!(matches( + &json!({ + "key": { + "$gte": 5 + } + }), + &json!({ "key": 5}) + )); + } + + #[test] + fn less_than() { + assert!(!matches( + &json!({ + "key": { + "$lt": 5 + } + }), + &json!({ "key": 6}) + )); + assert!(!matches( + &json!({ + "key": { + "$lt": 5 + } + }), + &json!({ "key": 5}) + )); + assert!(matches( + &json!({ + "key": { + "$lte": 5 + } + }), + &json!({ "key": 5}) + )); + } + + #[test] + fn in_array() { + assert!(matches( + &json!({ + "key": { + "$in": 3 + } + }), + &json!({ + "key": [1,2,3,4,5] + }) + )); + + assert!(matches( + &json!({ + "key": { + "$nin": 3 + } + }), + &json!({ + "key": [1,2,4,5] + }) + )); + } + + #[test] + fn and_op() { + assert!(matches( + &json!({ + "$and": [ + { "key": "value" }, + { "num": { "$gt": 5 }} + ] + }), + &json!({ + "key": "value", + "num": 7 + }) + )); + + assert!(!matches( + &json!({ + "$and": [ + { "key": "value" }, + { "num": { "$gt": 5 }} + ] + }), + &json!({ + "key": "value", + "num": 3 + }) + )); + } + + #[test] + fn or_op() { + let filter = json!({ + "$or": [ + { "key": "value" }, + { "num": { "$gt": 5} } + ] + }); + assert!(matches( + &filter, + &json!({ + "key": "value", + "num": 6 + }) + )); + assert!(matches( + &filter, + &json!({ + "key": "value", + "num": 2 + }) + )); + assert!(matches( + &filter, + &json!({ + "key": "not_value", + "num": 6 + }) + )); + assert!(!matches( + &filter, + &json!({ + "key": "not_value", + "num": 2 + }) + )); + } + + #[test] + fn negation() { + assert!(matches( + &json!({ + "num": { "$not": { "$gt": 5 }} + }), + &json!({ + "num": 3 + }) + )); + } + + #[test] + fn exists() { + assert!(matches( + &json!({ + "key": { "$exists": true } + }), + &json!({ + "key": "value" + }) + )); + assert!(!matches( + &json!({ + "key": { "$exists": true } + }), + &json!({}) + )); + assert!(!matches( + &json!({ + "key": { "$exists": false } + }), + &json!({ + "key": "value" + }) + )); + } + + #[test] + fn size() { + assert!(matches( + &json!({ + "list": { "$size": 3} + }), + &json!({ + "list": [1,2,3] + }) + )); + assert!(!matches( + &json!({ + "list": { "$size": 5} + }), + &json!({ + "list": [1,2,3] + }) + )); + } + + #[test] + fn type_match() { + assert!(matches( + &json!({ + "key": { "$type": "number"} + }), + &json!({ + "key": 3 + }) + )); + assert!(matches( + &json!({ + "key": { "$type": "string"} + }), + &json!({ + "key": "value" + }) + )); + assert!(matches( + &json!({ + "key": { "$type": "array"} + }), + &json!({ + "key": [1,2,3] + }) + )); + assert!(matches( + &json!({ + "key": { "$type": "object"} + }), + &json!({ + "key": { + "nested": "value" + } + }) + )); + } + + #[test] + fn regex_match() { + assert!(matches( + &json!({ + "key": { "$regex": "hello (world|json)"} + }), + &json!({ + "key": "hello world" + }) + )); + assert!(matches( + &json!({ + "key": { "$regex": "hello (world|json)"} + }), + &json!({ + "key": "hello json" + }) + )); + assert!(!matches( + &json!({ + "key": { "$regex": "hello (world|json)"} + }), + &json!({ + "key": "hello rust" + }) + )); + assert!(matches( + &json!({ + "key": { "$regex": "hello (world|json)"} + }), + &json!({ + "key": "hello world!" + }) + )); + } + + #[test] + fn multiple_mask() { + assert!(matches( + &json!({ + "key": "value", + "num": 3 + }), + &json!({ + "key": "value", + "num": 3, + "extra": "value" + }) + )); + assert!(!matches( + &json!({ + "key": "value", + "num": 3 + }), + &json!({ + "key": "value", + "num": 5, + "extra": "value" + }) + )); + } + + #[test] + fn nested_modifier() { + assert!(matches( + &json!({ + "key": { + "nested": { + "$gt": 5 + } + } + }), + &json!({ + "key": { "nested": 7 } + }) + )); + + assert!(!matches( + &json!({ + "key": { + "nested": { + "$gt": 5 + } + } + }), + &json!({ + "key": { "nested": 2 } + }) + )); + } +}