diff --git a/README.md b/README.md index 1e497ee..d87e52d 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,7 @@ following elements: | Expression sign | Description | Where to use | |-----------------|--------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------| +| `!` | Not | To negate the expression | | `==` | Equal | To compare numbers or string literals | | `!=` | Unequal | To compare numbers or string literals in opposite way to equals | | `<` | Less | To compare numbers | @@ -99,7 +100,7 @@ following elements: | `noneOf` | The left size has no intersection with right | | | `anyOf` | The left size has at least one intersection with right | | | `subsetOf` | The left is a subset of the right side | | -| | Exists operator. | The operator checks the existence of the field depicted on the left side like that `[?(@.key.isActive)]` | +| `?` | Exists operator. | The operator checks the existence of the field depicted on the left side like that `[?(@.key.isActive)]` | Filter expressions can be chained using `||` and `&&` (logical or and logical and correspondingly) in the following way: diff --git a/src/lib.rs b/src/lib.rs index 5d4e8e2..cb3be69 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1244,6 +1244,66 @@ mod tests { ); } + #[test] + fn logical_not_exp_test() { + let json: Box = Box::new(json!({"first":{"second":{"active":1}}})); + let path: Box = Box::from( + JsonPathInst::from_str("$.first[?(!@.does_not_exist >= 1.0)]") + .expect("the path is correct"), + ); + let finder = JsonPathFinder::new(json.clone(), path); + let v = finder.find_slice(); + assert_eq!( + v, + vec![Slice( + &json!({"second":{"active": 1}}), + "$.['first']".to_string() + )] + ); + + let path: Box = Box::from( + JsonPathInst::from_str("$.first[?(!(@.does_not_exist >= 1.0))]") + .expect("the path is correct"), + ); + let finder = JsonPathFinder::new(json.clone(), path); + let v = finder.find_slice(); + assert_eq!( + v, + vec![Slice( + &json!({"second":{"active": 1}}), + "$.['first']".to_string() + )] + ); + + let path: Box = Box::from( + JsonPathInst::from_str("$.first[?(!(@.second.active == 1) || @.second.active == 1)]") + .expect("the path is correct"), + ); + let finder = JsonPathFinder::new(json.clone(), path); + let v = finder.find_slice(); + assert_eq!( + v, + vec![Slice( + &json!({"second":{"active": 1}}), + "$.['first']".to_string() + )] + ); + + let path: Box = Box::from( + JsonPathInst::from_str("$.first[?(!@.second.active == 1 && !@.second.active == 1 || !@.second.active == 2)]") + .expect("the path is correct"), + ); + let finder = JsonPathFinder::new(json, path); + let v = finder.find_slice(); + assert_eq!( + v, + vec![Slice( + &json!({"second":{"active": 1}}), + "$.['first']".to_string() + )] + ); + } + // #[test] // fn no_value_len_field_test() { // let json: Box = diff --git a/src/parser/grammar/json_path.pest b/src/parser/grammar/json_path.pest index e6dffe8..90a960e 100644 --- a/src/parser/grammar/json_path.pest +++ b/src/parser/grammar/json_path.pest @@ -19,6 +19,7 @@ char = _{ } root = {"$"} sign = { "==" | "!=" | "~=" | ">=" | ">" | "<=" | "<" | "in" | "nin" | "size" | "noneOf" | "anyOf" | "subsetOf"} +not = {"!"} key_lim = {!"length()" ~ (word | ASCII_DIGIT | specs)+} key_unlim = {"[" ~ string_qt ~ "]"} key = ${key_lim | key_unlim} @@ -38,11 +39,12 @@ slice = {start_slice? ~ col ~ end_slice? ~ step_slice? } unit_keys = { string_qt ~ ("," ~ string_qt)+ } unit_indexes = { number ~ ("," ~ number)+ } -filter = {"?"~ "(" ~ logic ~ ")"} +filter = {"?"~ "(" ~ logic_or ~ ")"} -logic = {logic_and ~ ("||" ~ logic_and)*} -logic_and = {logic_atom ~ ("&&" ~ logic_atom)*} -logic_atom = {atom ~ (sign ~ atom)? | "(" ~ logic ~ ")"} +logic_or = {logic_and ~ ("||" ~ logic_and)*} +logic_and = {logic_not ~ ("&&" ~ logic_not)*} +logic_not = {not? ~ logic_atom} +logic_atom = {atom ~ (sign ~ atom)? | "(" ~ logic_or ~ ")"} atom = {chain | string_qt | number | boolean | null} diff --git a/src/parser/model.rs b/src/parser/model.rs index d60ecd3..5b55336 100644 --- a/src/parser/model.rs +++ b/src/parser/model.rs @@ -69,6 +69,8 @@ pub enum FilterExpression { And(Box, Box), /// or with || Or(Box, Box), + /// not with ! + Not(Box), } impl FilterExpression { diff --git a/src/parser/parser.rs b/src/parser/parser.rs index e95e01a..b7d5c51 100644 --- a/src/parser/parser.rs +++ b/src/parser/parser.rs @@ -1,6 +1,6 @@ use crate::parser::errors::JsonPathParserError::ParserError; use crate::parser::errors::{parser_err, JsonPathParserError}; -use crate::parser::model::FilterExpression::{And, Or}; +use crate::parser::model::FilterExpression::{And, Not, Or}; use crate::parser::model::{ FilterExpression, FilterSign, Function, JsonPath, JsonPathIndex, Operand, }; @@ -145,10 +145,10 @@ fn parse_chain_in_operand(rule: Pair) -> Result) -> Result { - Ok(JsonPathIndex::Filter(parse_logic(pair.into_inner())?)) + Ok(JsonPathIndex::Filter(parse_logic_or(pair.into_inner())?)) } -fn parse_logic(pairs: Pairs) -> Result { +fn parse_logic_or(pairs: Pairs) -> Result { let mut expr: Option = None; let error_message = format!("Failed to parse logical expression: {:?}", pairs); for pair in pairs { @@ -168,7 +168,7 @@ fn parse_logic_and(pairs: Pairs) -> Result = None; let error_message = format!("Failed to parse logical `and` expression: {:?}", pairs,); for pair in pairs { - let next_expr = parse_logic_atom(pair.into_inner())?; + let next_expr = parse_logic_not(pair.into_inner())?; match expr { None => expr = Some(next_expr), Some(e) => expr = Some(And(Box::new(e), Box::new(next_expr))), @@ -180,10 +180,26 @@ fn parse_logic_and(pairs: Pairs) -> Result) -> Result { + if let Some(rule) = pairs.peek().map(|x| x.as_rule()) { + match rule { + Rule::not => { + pairs.next().expect("unreachable in arithmetic: should have a value as pairs.peek() was Some(_)"); + parse_logic_not(pairs) + .map(|expr|Not(Box::new(expr))) + }, + Rule::logic_atom => parse_logic_atom(pairs.next().expect("unreachable in arithmetic: should have a value as pairs.peek() was Some(_)").into_inner()), + x => Err(JsonPathParserError::UnexpectedRuleLogicError(x, pairs)), + } + } else { + Err(JsonPathParserError::UnexpectedNoneLogicError(pairs)) + } +} + fn parse_logic_atom(mut pairs: Pairs) -> Result { if let Some(rule) = pairs.peek().map(|x| x.as_rule()) { match rule { - Rule::logic => parse_logic(pairs.next().expect("unreachable in arithmetic: should have a value as pairs.peek() was Some(_)").into_inner()), + Rule::logic_or => parse_logic_or(pairs.next().expect("unreachable in arithmetic: should have a value as pairs.peek() was Some(_)").into_inner()), Rule::atom => { let left: Operand = parse_atom(pairs.next().unwrap())?; if pairs.peek().is_none() { diff --git a/src/path/index.rs b/src/path/index.rs index f371788..cabf9d7 100644 --- a/src/path/index.rs +++ b/src/path/index.rs @@ -205,6 +205,9 @@ pub enum FilterPath<'a> { left: PathInstance<'a>, right: PathInstance<'a>, }, + Not { + exp: PathInstance<'a>, + }, } impl<'a> FilterPath<'a> { @@ -223,6 +226,9 @@ impl<'a> FilterPath<'a> { left: Box::new(FilterPath::new(l, root)), right: Box::new(FilterPath::new(r, root)), }, + FilterExpression::Not(exp) => FilterPath::Not { + exp: Box::new(FilterPath::new(exp, root)), + }, } } fn compound( @@ -307,6 +313,9 @@ impl<'a> FilterPath<'a> { !JsonPathValue::vec_as_data(right.find(Slice(curr_el, pref))).is_empty() } } + FilterPath::Not { exp } => { + JsonPathValue::vec_as_data(exp.find(Slice(curr_el, pref))).is_empty() + } } } }