diff --git a/examples/specific.rs b/examples/specific.rs index abc8fe2..fc5aac5 100644 --- a/examples/specific.rs +++ b/examples/specific.rs @@ -4,50 +4,27 @@ use serde_json::json; fn main() { let logic = JsonLogic::new(); - let rule = json!({ - "filter": [ - { - "var": "locales" - }, - { - "!==": [ - { - "var": "code" - }, - { - "var": "../../locale" - } - ] - } - ] - }); - let data = json!({ - "locale": "pl", - "locales": [ - { - "name": "Israel", - "code": "he", - "flag": "🇮🇱", - "iso": "he-IL", - "dir": "rtl" - }, - { - "name": "українська", - "code": "ue", - "flag": "🇺🇦", - "iso": "uk-UA", - "dir": "ltr" - }, - { - "name": "Polski", - "code": "pl", - "flag": "🇵🇱", - "iso": "pl-PL", - "dir": "ltr" - } - ] - }); - - let result = logic.apply(&rule, &data).unwrap(); - println!("Result: {}", result); + // Test both escaped dot and regular dot navigation + let test_cases = vec![ + // Test 1: Escaped dot should look up exact key + ( + json!({"var": "hello\\.world"}), + json!({"hello": {"world": "i'm here!"}, "hello.world": "ups!"}), + "ups!" + ), + // Test 2: Regular dot should navigate nested object + ( + json!({"var": "hello.world"}), + json!({"hello": {"world": "i'm here!"}, "hello.world": "ups!"}), + "i'm here!" + ) + ]; + + for (rule, data, expected) in test_cases { + let result = logic.apply(&rule, &data).unwrap(); + println!("Rule: {}", rule); + println!("Result: {}", result); + println!("Expected: {}", expected); + println!("---"); + } } \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 61c7ead..796ad2e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,6 +2,7 @@ mod error; mod operators; use error::Error; +use operators::preserve::PreserveOperator; use operators::{ operator::Operator, var::VarOperator, @@ -83,6 +84,7 @@ impl JsonLogic { self.operators.insert("none".into(), Arc::new(NoneOperator)); self.operators.insert("some".into(), Arc::new(SomeOperator)); + self.operators.insert("preserve".into(), Arc::new(PreserveOperator)); } diff --git a/src/operators/mod.rs b/src/operators/mod.rs index 68a0e2d..c4ec58a 100644 --- a/src/operators/mod.rs +++ b/src/operators/mod.rs @@ -6,4 +6,5 @@ pub mod arithmetic; pub mod string; pub mod array; pub mod missing; -pub mod array_ops; \ No newline at end of file +pub mod array_ops; +pub mod preserve; diff --git a/src/operators/preserve.rs b/src/operators/preserve.rs new file mode 100644 index 0000000..d63599c --- /dev/null +++ b/src/operators/preserve.rs @@ -0,0 +1,20 @@ +use crate::operators::operator::Operator; +use crate::{Error, JsonLogic, JsonLogicResult}; +use serde_json::Value; + +#[derive(Default)] +pub struct PreserveOperator; + +impl Operator for PreserveOperator { + fn apply(&self, _logic: &JsonLogic, args: &Value, _data: &Value) -> JsonLogicResult { + match args { + Value::Object(obj) => Ok(Value::Object(obj.clone())), + _ => Err(Error::InvalidArguments("preserve requires object argument".into())) + } + } + + // Optionally disable auto traversal since we want to preserve the raw value + fn auto_traverse(&self) -> bool { + false + } +} \ No newline at end of file diff --git a/src/operators/var.rs b/src/operators/var.rs index b85a0bc..9178571 100644 --- a/src/operators/var.rs +++ b/src/operators/var.rs @@ -11,11 +11,27 @@ impl VarOperator { return Some(data.clone()); } + // Handle escaped path first + if path.contains("\\.") { + let unescaped_key = path.replace("\\.", "."); + if let Value::Object(map) = data { + return map.get(&unescaped_key).cloned(); + } + return None; + } + + // Handle dot navigation for unescaped paths let mut current = data; for part in path.split('.') { - current = match (current, part.parse::()) { - (Value::Object(map), _) => map.get(part).unwrap_or(&Value::Null), - (Value::Array(arr), Ok(index)) => arr.get(index).unwrap_or(&Value::Null), + current = match current { + Value::Object(map) => map.get(part).unwrap_or(&Value::Null), + Value::Array(arr) => { + if let Ok(index) = part.parse::() { + arr.get(index).unwrap_or(&Value::Null) + } else { + return None; + } + }, _ => return None }; @@ -23,7 +39,7 @@ impl VarOperator { return None; } } - + Some(current.clone()) } @@ -42,26 +58,13 @@ impl VarOperator { } } - fn handle_string_path(path_str: &str, data: &Value, default: Option<&Value>) -> JsonLogicResult { - if path_str.is_empty() { - return Ok(data.clone()); + fn handle_string_path(path: &str, data: &Value, default: Option<&Value>) -> JsonLogicResult { + match Self::get_value_at_path(data, path) { + Some(value) => Ok(value), + None => Self::get_default(default) } - - let mut current = data; - for part in path_str.split('.') { - current = match (current, part.parse::()) { - (Value::Object(map), _) => map.get(part).unwrap_or(&Value::Null), - (Value::Array(arr), Ok(index)) => arr.get(index).unwrap_or(&Value::Null), - _ => return Self::get_default(default) - }; - - if current == &Value::Null { - return Self::get_default(default); - } - } - - Ok(current.clone()) } + } impl Operator for VarOperator { diff --git a/tests/alltests.rs b/tests/alltests.rs index c0478d8..a9241a7 100644 --- a/tests/alltests.rs +++ b/tests/alltests.rs @@ -78,10 +78,8 @@ fn test_jsonlogic_all_test_suites() { let test_sources = vec![ // Remote URLs "https://jsonlogic.com/tests.json", - // "https://raw.githubusercontent.com/TotalTechGeek/json-logic-engine/refs/heads/master/bench/tests.json", - // "https://raw.githubusercontent.com/TotalTechGeek/json-logic-engine/refs/heads/master/bench/compatible.json", // Local file - // "tests/custom_tests.json", // Add your local test file path here + "tests/custom_tests.json", // Add your local test file path here ]; let mut overall_passed = 0; diff --git a/tests/custom_tests.json b/tests/custom_tests.json index 7c38c0b..b077439 100644 --- a/tests/custom_tests.json +++ b/tests/custom_tests.json @@ -1,5 +1,15 @@ [ "Custom Tests", + [ + {"var": "hello\\.world"}, + {"hello": {"world": "i'm here!"}, "hello.world": "ups!"}, + "ups!" + ], + [ + {"var": "hello.world"}, + {"hello": {"world": "i'm here!"}, "hello.world": "ups!"}, + "i'm here!" + ], [ { "var": "hello\\.world" @@ -57,5 +67,24 @@ ] }, [{"name":"Israel","code":"he","flag":"🇮🇱","iso":"he-IL","dir":"rtl"},{"name":"українська","code":"ue","flag":"🇺🇦","iso":"uk-UA","dir":"ltr"}] + ], + [ + { + "if": [ + { + "and": [ + { "===": [ { "var": "first_name" }, true ] }, + { "===": [ { "var": "last_name" }, true ] } + ] + }, + { "preserve": { "first_name": "scott", "last_name": "wyatt" } }, + { "preserve": { "first_name": "no", "last_name": "idea" } } + ] + }, + { + "first_name": true, + "last_name": true + }, + {"first_name":"scott","last_name":"wyatt"} ] ] \ No newline at end of file