From baa85e8a32acad6d73576497dba7ba8b533f441a Mon Sep 17 00:00:00 2001 From: Mike Taghavi Date: Sat, 15 Jul 2023 10:14:10 +0200 Subject: [PATCH] Add support for keyed recursive descendant search Update README Bump version --- Cargo.lock | 2 +- Cargo.toml | 2 +- README.md | 45 ++++++++++++++++--- src/context.rs | 112 +++++++++++++++++++++++++++++++++++------------ src/grammar.pest | 6 ++- src/parser.rs | 71 +++++++++++++++++++++++------- 6 files changed, 185 insertions(+), 53 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 557a602..bd59429 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -96,7 +96,7 @@ checksum = "62b02a5381cc465bd3041d84623d0fa3b66738b52b8e2fc3bab8ad63ab032f4a" [[package]] name = "jetro" -version = "0.2.2" +version = "0.2.3" dependencies = [ "dynfmt", "pest", diff --git a/Cargo.toml b/Cargo.toml index 7871597..13d7454 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "jetro" -version = "0.2.2" +version = "0.2.3" edition = "2021" authors = ["Milad (Mike) Taghavi "] license = "MIT" diff --git a/README.md b/README.md index 58b7809..7954544 100644 --- a/README.md +++ b/README.md @@ -174,7 +174,7 @@ Get value associated with `line_items`. "name": "chilli", "price": 0, "total": 1, - "type": "ingridient" + "type": "ingredient" }, { "ident": "ewq", @@ -207,6 +207,41 @@ Get value associated with first matching key which has a value and return its `i ``` +--- + +Recursively search for objects that has key with specified value. + +``` +>/..('type'='ingredient') +``` + +
+ See output + + ### result + +```json +[ + { + "ident": "ghi", + "is_gratis": false, + "name": "cheese", + "price": 2, + "total": 1, + "type": "ingredient" + }, + { + "ident": "def", + "is_gratis": false, + "name": "salami", + "price": 2.8, + "total": 10, + "type": "ingredient" + } +] +``` +
+ --- ``` >/..items/#tail @@ -241,7 +276,7 @@ Get value associated with first matching key which has a value and return its `i "name": "chilli", "price": 0, "total": 1, - "type": "ingridient" + "type": "ingredient" }, { "ident": "ewq", @@ -274,7 +309,7 @@ Get value associated with first matching key which has a value and return its `i "name": "chilli", "price": 0, "total": 1, - "type": "ingridient" + "type": "ingredient" } ] ``` @@ -293,7 +328,7 @@ Get value associated with first matching key which has a value and return its `i ```json [ - "ingridient" + "ingredient" ] ``` @@ -374,7 +409,7 @@ Select up to 4 items from index zero of array `items` "name": "chilli", "price": 0, "total": 1, - "type": "ingridient" + "type": "ingredient" } ] ``` diff --git a/src/context.rs b/src/context.rs index b9d2a3f..61a4b9d 100644 --- a/src/context.rs +++ b/src/context.rs @@ -211,6 +211,12 @@ pub struct FilterAST { pub right: Option>>, } +#[derive(Debug, PartialEq, Clone)] +pub enum FilterDescendant { + Single(String), + Pair(String, String), +} + /// Filter contains operations that transform, match /// or search the input based on structure generated /// the from parser. @@ -219,7 +225,7 @@ pub enum Filter { Root, AnyChild, Child(String), - Descendant(String), + DescendantChild(FilterDescendant), Pick(Vec), ArrayIndex(usize), ArrayFrom(usize), @@ -668,9 +674,14 @@ impl Filter { pub fn key(&self) -> Option { match &self { - Filter::Descendant(ref descendant) => { - return Some(descendant[2..].to_string()); - } + Filter::DescendantChild(entry) => match entry { + FilterDescendant::Single(ref descendant) => { + return Some(descendant[2..].to_string()); + } + _ => { + todo!("pair descendant not implemented"); + } + }, _ => None, } } @@ -1067,36 +1078,83 @@ impl<'a> Context<'a> { _ => {} }, - (Filter::Descendant(ref descendant), Some(tail)) => match *current.value { - Value::Object(ref obj) => { - for (k, v) in obj { - let filters: &[Filter] = if k == descendant { - tail - } else { - current.filters - }; - - if filters.len() == 0 { - self.results.borrow_mut().push(Rc::new(v.clone())); - } else { + (Filter::DescendantChild(ref entry), Some(tail)) => match entry { + FilterDescendant::Single(ref descendant) => match *current.value { + Value::Object(ref obj) => { + for (k, v) in obj { + let filters: &[Filter] = if k == descendant { + tail + } else { + current.filters + }; + + if filters.len() == 0 { + self.results.borrow_mut().push(Rc::new(v.clone())); + } else { + self.stack.borrow_mut().push(StackItem::new( + Rc::new(v.clone()), + filters, + self.stack.clone(), + )); + } + } + } + Value::Array(ref array) => { + for v in array { self.stack.borrow_mut().push(StackItem::new( Rc::new(v.clone()), - filters, + current.filters, self.stack.clone(), )); } } - } - Value::Array(ref array) => { - for v in array { - self.stack.borrow_mut().push(StackItem::new( - Rc::new(v.clone()), - current.filters, - self.stack.clone(), - )); + _ => {} + }, + FilterDescendant::Pair(ref descendant, ref target_value) => { + match *current.value { + Value::Object(ref obj) => { + let mut found_match = false; + for (k, v) in obj { + let filters: &[Filter] = if k == descendant { + match &v { + Value::String(ref current_value) => { + if *current_value == *target_value { + found_match = true; + tail + } else { + current.filters + } + } + _ => current.filters, + } + } else { + current.filters + }; + + if filters.len() == 0 && found_match { + self.results + .borrow_mut() + .insert(0, current.value.clone()); + } + self.stack.borrow_mut().push(StackItem::new( + Rc::new(v.clone()), + filters, + self.stack.clone(), + )); + } + } + Value::Array(ref array) => { + for v in array { + self.stack.borrow_mut().push(StackItem::new( + Rc::new(v.clone()), + current.filters, + self.stack.clone(), + )); + } + } + _ => {} } } - _ => {} }, _ => {} @@ -1177,7 +1235,7 @@ mod test { let filters = vec![ Filter::Root, Filter::Child("foo".to_string()), - Filter::Descendant("within".to_string()), + Filter::DescendantChild(FilterDescendant::Single("within".to_string())), ]; let mut ctx = Context::new(v, &filters); diff --git a/src/grammar.pest b/src/grammar.pest index a976ed7..592d867 100644 --- a/src/grammar.pest +++ b/src/grammar.pest @@ -47,7 +47,9 @@ or = { "or" } logical_cmp = { and | or } child = { slash ~ ident } any_child = { slash ~ asterisk } -descendant_child = { slash ~ double_dot ~ ident } +keyed_ident = { literal ~ whitespace ~ "=" ~ whitespace ~ literal } +ident_or_keyed = { ( lparen ~ keyed_ident ~ rparen )| ident } +descendant_child = { slash ~ double_dot ~ ident_or_keyed } grouped_any = { slash ~ grouped_literal } array_index = { slash ~ "[" ~ number ~ "]" } pure_index = {"[" ~ number ~ "]"} @@ -82,4 +84,4 @@ expression = { (path|reverse_path) ~ whitespace ~ (whitespace ~ pickFn ~ whitespace | filterFn ~ whitespace| grouped_any ~ whitespace | child ~ whitespace | any_child ~ whitespace | descendant_child ~ whitespace | array_index ~ whitespace | slice ~ whitespace | array_to ~ whitespace | array_from ~ whitespace | fn ~ whitespace)* ~ EOI -} \ No newline at end of file +} diff --git a/src/parser.rs b/src/parser.rs index cbffaad..68875d1 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -6,8 +6,8 @@ use std::rc::Rc; use pest::iterators::Pair; use crate::context::{ - Filter, FilterAST, FilterInner, FilterInnerRighthand, FilterLogicalOp, FilterOp, Func, FuncArg, - MapAST, MapBody, PickFilterInner, + Filter, FilterAST, FilterDescendant, FilterInner, FilterInnerRighthand, FilterLogicalOp, + FilterOp, Func, FuncArg, MapAST, MapBody, PickFilterInner, }; use crate::*; @@ -395,8 +395,26 @@ pub(crate) fn parse<'a>(input: &'a str) -> Result, pest::error::Erro actions.push(Filter::Pick(elems)); } Rule::descendant_child => { - let ident = token.into_inner().nth(1).unwrap().as_str().to_owned(); - actions.push(Filter::Descendant(ident)); + let entry = token.into_inner().nth(1).unwrap(); + let node = entry.into_inner().next().unwrap(); + let rule = node.as_rule(); + match &rule { + Rule::ident => { + let ident = node.as_str().to_owned(); + actions.push(Filter::DescendantChild(FilterDescendant::Single(ident))); + } + Rule::keyed_ident => { + let mut node = node.into_inner(); + let mut lnode = node.next().unwrap().into_inner(); + let mut rnode = node.next().unwrap().into_inner(); + let lstr = lnode.next().unwrap().as_str().to_owned(); + let rstr = rnode.next().unwrap().as_str().to_owned(); + actions.push(Filter::DescendantChild(FilterDescendant::Pair(lstr, rstr))); + } + _ => { + panic!("unreachable") + } + } } Rule::child => { let ident = token.into_inner().nth(1).unwrap().as_str().to_owned(); @@ -413,7 +431,7 @@ mod test { use super::*; #[test] - fn test_one() { + fn test_simple_path() { let actions = parse(">/obj/some/*/name").unwrap(); assert_eq!( actions, @@ -428,7 +446,7 @@ mod test { } #[test] - fn test_two() { + fn test_simple_path_with_recursive_descendant() { let actions = parse(">/obj/some/..descendant/name").unwrap(); assert_eq!( actions, @@ -436,14 +454,29 @@ mod test { Filter::Root, Filter::Child("obj".to_string()), Filter::Child("some".to_string()), - Filter::Descendant("descendant".to_string()), + Filter::DescendantChild(FilterDescendant::Single("descendant".to_string())), Filter::Child("name".to_string()), ] ); } #[test] - fn test_three() { + fn test_recursive_descendant_keyed() { + let actions = parse(">/..('key' = 'value')").unwrap(); + assert_eq!( + actions, + vec![ + Filter::Root, + Filter::DescendantChild(FilterDescendant::Pair( + "key".to_string(), + "value".to_string() + )), + ] + ); + } + + #[test] + fn test_simple_path_with_recursive_descendant_and_pick() { let actions = parse(">/obj/some/..descendant/#pick('a', 'b', 'c', 'd')").unwrap(); assert_eq!( actions, @@ -451,7 +484,7 @@ mod test { Filter::Root, Filter::Child("obj".to_string()), Filter::Child("some".to_string()), - Filter::Descendant("descendant".to_string()), + Filter::DescendantChild(FilterDescendant::Single("descendant".to_string())), Filter::Pick(vec![ PickFilterInner::Str("a".to_string()), PickFilterInner::Str("b".to_string()), @@ -471,7 +504,7 @@ mod test { Filter::Root, Filter::Child("obj".to_string()), Filter::Child("some".to_string()), - Filter::Descendant("descendant".to_string()), + Filter::DescendantChild(FilterDescendant::Single("descendant".to_string())), Filter::Pick(vec![ PickFilterInner::KeyedStr { key: "f".to_string(), @@ -496,7 +529,7 @@ mod test { Filter::Root, Filter::Child("obj".to_string()), Filter::Child("some".to_string()), - Filter::Descendant("descendant".to_string()), + Filter::DescendantChild(FilterDescendant::Single("descendant".to_string())), Filter::Pick(vec![ PickFilterInner::KeyedStr { key: "f".to_string(), @@ -526,7 +559,7 @@ mod test { Filter::Root, Filter::Child("obj".to_string()), Filter::Child("some".to_string()), - Filter::Descendant("descendant".to_string()), + Filter::DescendantChild(FilterDescendant::Single("descendant".to_string())), Filter::Pick(vec![ PickFilterInner::KeyedStr { key: "f".to_string(), @@ -580,7 +613,7 @@ mod test { actions, vec![ Filter::Root, - Filter::Descendant("meows".to_string()), + Filter::DescendantChild(FilterDescendant::Single("meows".to_string())), Filter::MultiFilter(Rc::new(RefCell::new(FilterAST { operator: FilterLogicalOp::None, left: Some(Rc::new(RefCell::new(FilterInner::Cond { @@ -908,7 +941,7 @@ mod test { Filter::Pick(vec![PickFilterInner::KeyedSubpath { subpath: vec![ Filter::Root, - Filter::Descendant("priority".to_string()), + Filter::DescendantChild(FilterDescendant::Single("priority".to_string())), Filter::Function(Func { name: "len".to_string(), args: vec![], @@ -930,7 +963,7 @@ mod test { actions, vec![ Filter::Root, - Filter::Descendant("values".to_string()), + Filter::DescendantChild(FilterDescendant::Single("values".to_string())), Filter::Function(Func { name: "map".to_string(), alias: None, @@ -979,7 +1012,9 @@ mod test { subpath: vec![ Filter::Root, Filter::Child("get".to_string()), - Filter::Descendant("recursive".to_string()), + Filter::DescendantChild(FilterDescendant::Single( + "recursive".to_string() + )), Filter::Child("value".to_string()), Filter::MultiFilter(Rc::new(RefCell::new(FilterAST { operator: FilterLogicalOp::None, @@ -997,7 +1032,9 @@ mod test { reverse: false, subpath: vec![ Filter::Root, - Filter::Descendant("preferences".to_string()), + Filter::DescendantChild(FilterDescendant::Single( + "preferences".to_string() + )), Filter::Function(Func { name: "map".to_string(), alias: None,