This commit is contained in:
JMARyA 2024-02-09 10:21:27 +01:00
commit c9d1c372c9
Signed by: jmarya
GPG key ID: 901B2ADDF27C2263
7 changed files with 816 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/target

134
Cargo.lock generated Normal file
View file

@ -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"

9
Cargo.toml Normal file
View file

@ -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"

2
README.md Normal file
View file

@ -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.

250
src/lib.rs Normal file
View file

@ -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<bool> = 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<bool> = 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;
}

30
src/main.rs Normal file
View file

@ -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
})
));
}

390
src/test.rs Normal file
View file

@ -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 }
})
));
}
}