diff --git a/README.md b/README.md index 00f7f30..4f6fed6 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![Forum](https://img.shields.io/badge/Forum-RedisJSON-blue)](https://forum.redislabs.com/c/modules/redisjson) [![Discord](https://img.shields.io/discord/697882427875393627?style=flat-square)](https://discord.gg/QUkjSsk) -A JSONPath library for rust. The idea behind this library is that it can operate on any json representation as long as it implements the [`SelectValue`](src/select_value.rs) triat. The library has an implementation for [serde_json value](https://docs.serde.rs/serde_json/value/enum.Value.html) and [ivalue](https://docs.rs/tch/0.1.1/tch/enum.IValue.html). +A JSONPath library for rust. The idea behind this library is that it can operate on any json representation as long as it implements the [`SelectValue`](src/select_value.rs) trait. The library has an implementation for [serde_json value](https://docs.serde.rs/serde_json/value/enum.Value.html) and [ivalue](https://docs.rs/tch/0.1.1/tch/enum.IValue.html). ### Getting Started Add the following to your cargo.toml diff --git a/src/grammer.pest b/src/grammer.pest index 515102d..90e1be1 100644 --- a/src/grammer.pest +++ b/src/grammer.pest @@ -1,4 +1,4 @@ -literal = @{('a' .. 'z' | 'A' .. 'Z' | '0' .. '9' | "-" | "_" | "`" | "~" | "+" | "#" | "%" | "$" | "^" | "/")+} +literal = @{('a' .. 'z' | 'A' .. 'Z' | '0' .. '9' | "-" | "_" | "`" | "~" | "+" | "#" | "%" | "$" | "^" | "/" | ":")+} string = _{("'" ~ (string_value) ~ "'") | ("\"" ~ (string_value) ~ "\"") | ("\"" ~ (string_value_escape_1) ~ "\"") | ("'" ~ (string_value_escape_2) ~ "'")} string_escape = @{"\\"} diff --git a/src/json_node.rs b/src/json_node.rs index a5d0262..3fe81d2 100644 --- a/src/json_node.rs +++ b/src/json_node.rs @@ -107,8 +107,8 @@ impl SelectValue for Value { fn get_long(&self) -> i64 { match self { Value::Number(n) => { - if n.is_i64() || n.is_u64() { - n.as_i64().unwrap() + if let Some(n) = n.as_i64() { + n } else { panic!("not a long"); } @@ -155,9 +155,10 @@ impl SelectValue for IValue { } fn contains_key(&self, key: &str) -> bool { - match self.as_object() { - Some(o) => o.contains_key(key), - _ => false, + if let Some(o) = self.as_object() { + o.contains_key(key) + } else { + false } } @@ -247,7 +248,7 @@ impl SelectValue for IValue { } } _ => { - panic!("not a long"); + panic!("not a number"); } } } @@ -262,7 +263,7 @@ impl SelectValue for IValue { } } _ => { - panic!("not a double"); + panic!("not a number"); } } } diff --git a/src/json_path.rs b/src/json_path.rs index fd07b22..aa60d29 100644 --- a/src/json_path.rs +++ b/src/json_path.rs @@ -9,10 +9,18 @@ use std::fmt::Debug; #[grammar = "grammer.pest"] pub struct JsonPathParser; +#[derive(Debug, PartialEq)] +pub enum JsonPathToken { + String, + Number, +} + #[derive(Debug)] pub struct Query<'i> { // query: QueryElement<'i> - pub query: Pairs<'i, Rule>, + pub root: Pairs<'i, Rule>, + is_static: Option, + size: Option, } #[derive(Debug)] @@ -21,6 +29,70 @@ pub struct QueryCompilationError { message: String, } +impl<'i> Query<'i> { + #[allow(dead_code)] + pub fn pop_last(&mut self) -> Option<(String, JsonPathToken)> { + let last = self.root.next_back(); + match last { + Some(last) => match last.as_rule() { + Rule::literal => Some((last.as_str().to_string(), JsonPathToken::String)), + Rule::number => Some((last.as_str().to_string(), JsonPathToken::Number)), + Rule::numbers_list => { + let first_on_list = last.into_inner().next(); + match first_on_list { + Some(first) => Some((first.as_str().to_string(), JsonPathToken::Number)), + None => None, + } + } + Rule::string_list => { + let first_on_list = last.into_inner().next(); + match first_on_list { + Some(first) => Some((first.as_str().to_string(), JsonPathToken::String)), + None => None, + } + } + _ => panic!("pop last was used in a none static path"), + }, + None => None, + } + } + + #[allow(dead_code)] + pub fn size(&mut self) -> usize { + if self.size.is_some() { + return self.size.unwrap(); + } + self.is_static(); + self.size() + } + + #[allow(dead_code)] + pub fn is_static(&mut self) -> bool { + if self.is_static.is_some() { + return self.is_static.unwrap(); + } + let mut size = 0; + let mut is_static = true; + let mut root_copy = self.root.clone(); + while let Some(n) = root_copy.next() { + size = size + 1; + match n.as_rule() { + Rule::literal | Rule::number => continue, + Rule::numbers_list | Rule::string_list => { + let inner = n.into_inner(); + if inner.count() > 1 { + is_static = false; + } + } + _ => is_static = false, + } + } + self.size = Some(size); + self.is_static = Some(is_static); + self.is_static() + } +} + impl std::fmt::Display for QueryCompilationError { fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { write!( @@ -50,7 +122,14 @@ impl std::fmt::Display for Rule { pub(crate) fn compile(path: &str) -> Result { let query = JsonPathParser::parse(Rule::query, path); match query { - Ok(q) => Ok(Query { query: q }), + Ok(mut q) => { + let root = q.next().unwrap(); + Ok(Query { + root: root.into_inner(), + is_static: None, + size: None, + }) + } // pest::error::Error Err(e) => { let pos = match e.location { @@ -188,30 +267,33 @@ enum PathTrackerElement<'i> { #[derive(Clone)] struct PathTracker<'i, 'j> { - father: Option<&'j PathTracker<'i, 'j>>, + parent: Option<&'j PathTracker<'i, 'j>>, element: PathTrackerElement<'i>, } -const fn create_empty_trucker<'i, 'j>() -> PathTracker<'i, 'j> { +const fn create_empty_tracker<'i, 'j>() -> PathTracker<'i, 'j> { PathTracker { - father: None, + parent: None, element: PathTrackerElement::Root, } } -const fn create_str_trucker<'i, 'j>(s: &'i str, father: &'j PathTracker<'i, 'j>) -> PathTracker<'i, 'j> { +const fn create_str_tracker<'i, 'j>( + s: &'i str, + parent: &'j PathTracker<'i, 'j>, +) -> PathTracker<'i, 'j> { PathTracker { - father: Some(father), + parent: Some(parent), element: PathTrackerElement::Key(s), } } -const fn create_index_trucker<'i, 'j>( +const fn create_index_tracker<'i, 'j>( index: usize, - father: &'j PathTracker<'i, 'j>, + parent: &'j PathTracker<'i, 'j>, ) -> PathTracker<'i, 'j> { PathTracker { - father: Some(father), + parent: Some(parent), element: PathTrackerElement::Index(index), } } @@ -264,21 +346,23 @@ impl<'i, 'j, S: SelectValue> TermEvaluationResult<'i, 'j, S> { (TermEvaluationResult::String(s1), TermEvaluationResult::String(s2)) => { CmpResult::Ord(s1.cmp(s2)) } + (TermEvaluationResult::Bool(b1), TermEvaluationResult::Bool(b2)) => { + CmpResult::Ord(b1.cmp(b2)) + } (TermEvaluationResult::Value(v), _) => match v.get_type() { SelectValueType::Long => TermEvaluationResult::Integer(v.get_long()).cmp(s), SelectValueType::Double => TermEvaluationResult::Float(v.get_double()).cmp(s), SelectValueType::String => TermEvaluationResult::Str(v.as_str()).cmp(s), + SelectValueType::Bool => TermEvaluationResult::Bool(v.get_bool()).cmp(s), _ => CmpResult::NotCmparable, }, (_, TermEvaluationResult::Value(v)) => match v.get_type() { SelectValueType::Long => self.cmp(&TermEvaluationResult::Integer(v.get_long())), SelectValueType::Double => self.cmp(&TermEvaluationResult::Float(v.get_double())), SelectValueType::String => self.cmp(&TermEvaluationResult::Str(v.as_str())), + SelectValueType::Bool => self.cmp(&TermEvaluationResult::Bool(v.get_bool())), _ => CmpResult::NotCmparable, }, - (TermEvaluationResult::Invalid, _) | (_, TermEvaluationResult::Invalid) => { - CmpResult::NotCmparable - } (_, _) => CmpResult::NotCmparable, } } @@ -312,16 +396,6 @@ impl<'i, 'j, S: SelectValue> TermEvaluationResult<'i, 'j, S> { fn eq(&self, s: &Self) -> bool { match (self, s) { - (TermEvaluationResult::Bool(b1), TermEvaluationResult::Bool(b2)) => *b1 == *b2, - (TermEvaluationResult::Value(v1), TermEvaluationResult::Bool(b2)) => { - if v1.get_type() == SelectValueType::Bool { - let b1 = v1.get_bool(); - b1 == *b2 - } else { - false - } - } - (TermEvaluationResult::Bool(_), TermEvaluationResult::Value(_)) => s.eq(self), (TermEvaluationResult::Value(v1), TermEvaluationResult::Value(v2)) => v1 == v2, (_, _) => match self.cmp(s) { CmpResult::Ord(o) => o.is_eq(), @@ -365,6 +439,7 @@ impl<'i, UPTG: UserPathTrackerGenerator> PathCalculator<'i, UPTG> { } } + #[allow(dead_code)] pub fn create_with_generator( query: &'i Query<'i>, tracker_generator: UPTG, @@ -390,14 +465,14 @@ impl<'i, UPTG: UserPathTrackerGenerator> PathCalculator<'i, UPTG> { self.calc_internal( pairs.clone(), val, - Some(create_str_trucker(key, &pt)), + Some(create_str_tracker(key, &pt)), calc_data, false, ); self.calc_full_scan( pairs.clone(), val, - Some(create_str_trucker(key, &pt)), + Some(create_str_tracker(key, &pt)), calc_data, ); } @@ -416,14 +491,14 @@ impl<'i, UPTG: UserPathTrackerGenerator> PathCalculator<'i, UPTG> { self.calc_internal( pairs.clone(), v, - Some(create_index_trucker(i, &pt)), + Some(create_index_tracker(i, &pt)), calc_data, false, ); self.calc_full_scan( pairs.clone(), v, - Some(create_index_trucker(i, &pt)), + Some(create_index_tracker(i, &pt)), calc_data, ); } @@ -450,7 +525,7 @@ impl<'i, UPTG: UserPathTrackerGenerator> PathCalculator<'i, UPTG> { if let Some(pt) = path_tracker { let items = json.items().unwrap(); for (key, val) in items { - let new_tracker = Some(create_str_trucker(key, &pt)); + let new_tracker = Some(create_str_tracker(key, &pt)); self.calc_internal(pairs.clone(), val, new_tracker, calc_data, true); } } else { @@ -464,7 +539,7 @@ impl<'i, UPTG: UserPathTrackerGenerator> PathCalculator<'i, UPTG> { let values = json.values().unwrap(); if let Some(pt) = path_tracker { for (i, v) in values.enumerate() { - let new_tracker = Some(create_index_trucker(i, &pt)); + let new_tracker = Some(create_index_tracker(i, &pt)); self.calc_internal(pairs.clone(), v, new_tracker, calc_data, true); } } else { @@ -488,7 +563,7 @@ impl<'i, UPTG: UserPathTrackerGenerator> PathCalculator<'i, UPTG> { let curr_val = json.get_key(curr.as_str()); if let Some(e) = curr_val { if let Some(pt) = path_tracker { - let new_tracker = Some(create_str_trucker(curr.as_str(), &pt)); + let new_tracker = Some(create_str_tracker(curr.as_str(), &pt)); self.calc_internal(pairs, e, new_tracker, calc_data, true); } else { self.calc_internal(pairs, e, None, calc_data, true); @@ -518,7 +593,7 @@ impl<'i, UPTG: UserPathTrackerGenerator> PathCalculator<'i, UPTG> { _ => panic!("{}", format!("{:?}", c)), }; if let Some(e) = curr_val { - let new_tracker = Some(create_str_trucker(s, &pt)); + let new_tracker = Some(create_str_tracker(s, &pt)); self.calc_internal(pairs.clone(), e, new_tracker, calc_data, true); } } @@ -567,7 +642,7 @@ impl<'i, UPTG: UserPathTrackerGenerator> PathCalculator<'i, UPTG> { let i = self.calc_abs_index(c.as_str().parse::().unwrap(), n); let curr_val = json.get_index(i); if let Some(e) = curr_val { - let new_tracker = Some(create_index_trucker(i, &pt)); + let new_tracker = Some(create_index_tracker(i, &pt)); self.calc_internal(pairs.clone(), e, new_tracker, calc_data, true); } } @@ -645,7 +720,7 @@ impl<'i, UPTG: UserPathTrackerGenerator> PathCalculator<'i, UPTG> { for i in (start..end).step_by(step) { let curr_val = json.get_index(i); if let Some(e) = curr_val { - let new_tracker = Some(create_index_trucker(i, &pt)); + let new_tracker = Some(create_index_tracker(i, &pt)); self.calc_internal(pairs.clone(), e, new_tracker, calc_data, true); } } @@ -784,7 +859,7 @@ impl<'i, UPTG: UserPathTrackerGenerator> PathCalculator<'i, UPTG> { } fn populate_path_tracker<'k, 'l>(&self, pt: &PathTracker<'l, 'k>, upt: &mut UPTG::PT) { - if let Some(f) = pt.father { + if let Some(f) = pt.parent { self.populate_path_tracker(f, upt); } match pt.element { @@ -841,7 +916,7 @@ impl<'i, UPTG: UserPathTrackerGenerator> PathCalculator<'i, UPTG> { if let Some(pt) = path_tracker { for (i, v) in values.enumerate() { if self.evaluate_filter(curr.clone(), v, calc_data) { - let new_tracker = Some(create_index_trucker(i, &pt)); + let new_tracker = Some(create_index_tracker(i, &pt)); self.calc_internal( pairs.clone(), v, @@ -883,7 +958,7 @@ impl<'i, UPTG: UserPathTrackerGenerator> PathCalculator<'i, UPTG> { pub fn calc_with_paths_on_root<'j: 'i, S: SelectValue>( &self, json: &'j S, - root: Pair, + root: Pairs<'i, Rule>, ) -> Vec> { let mut calc_data = PathCalculatorData { results: Vec::new(), @@ -891,14 +966,14 @@ impl<'i, UPTG: UserPathTrackerGenerator> PathCalculator<'i, UPTG> { }; if self.tracker_generator.is_some() { self.calc_internal( - root.into_inner(), + root, json, - Some(create_empty_trucker()), + Some(create_empty_tracker()), &mut calc_data, true, ); } else { - self.calc_internal(root.into_inner(), json, None, &mut calc_data, true); + self.calc_internal(root, json, None, &mut calc_data, true); } calc_data.results.drain(..).collect() } @@ -907,7 +982,7 @@ impl<'i, UPTG: UserPathTrackerGenerator> PathCalculator<'i, UPTG> { &self, json: &'j S, ) -> Vec> { - self.calc_with_paths_on_root(json, self.query.unwrap().query.clone().next().unwrap()) + self.calc_with_paths_on_root(json, self.query.unwrap().root.clone()) } pub fn calc<'j: 'i, S: SelectValue>(&self, json: &'j S) -> Vec<&'j S> { @@ -917,6 +992,7 @@ impl<'i, UPTG: UserPathTrackerGenerator> PathCalculator<'i, UPTG> { .collect() } + #[allow(dead_code)] pub fn calc_paths<'j: 'i, S: SelectValue>(&self, json: &'j S) -> Vec> { self.calc_with_paths(json) .into_iter() @@ -924,3 +1000,57 @@ impl<'i, UPTG: UserPathTrackerGenerator> PathCalculator<'i, UPTG> { .collect() } } + +#[cfg(test)] +mod json_path_compiler_tests { + use crate::json_path::compile; + use crate::json_path::JsonPathToken; + + #[test] + fn test_compiler_pop_last() { + let query = compile("$.foo"); + assert_eq!( + query.unwrap().pop_last().unwrap(), + ("foo".to_string(), JsonPathToken::String) + ); + } + + #[test] + fn test_compiler_pop_last_number() { + let query = compile("$.[1]"); + assert_eq!( + query.unwrap().pop_last().unwrap(), + ("1".to_string(), JsonPathToken::Number) + ); + } + + #[test] + fn test_compiler_pop_last_string_brucket_notation() { + let query = compile("$.[\"foo\"]"); + assert_eq!( + query.unwrap().pop_last().unwrap(), + ("foo".to_string(), JsonPathToken::String) + ); + } + + #[test] + fn test_compiler_is_static() { + let query = compile("$.[\"foo\"]"); + assert!(query.unwrap().is_static()); + + let query = compile("$.[\"foo\", \"bar\"]"); + assert!(!query.unwrap().is_static()); + } + + #[test] + fn test_compiler_size() { + let query = compile("$.[\"foo\"]"); + assert_eq!(query.unwrap().size(), 1); + + let query = compile("$.[\"foo\"].bar"); + assert_eq!(query.unwrap().size(), 2); + + let query = compile("$.[\"foo\"].bar[1]"); + assert_eq!(query.unwrap().size(), 3); + } +} diff --git a/src/lib.rs b/src/lib.rs index 253ddfb..d368c3e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -48,7 +48,7 @@ pub fn create<'i>(query: &'i Query<'i>) -> PathCalculator<'i, DummyTrackerGenera /// Create a PathCalculator object. The path calculator can be re-used /// to calculate json paths on different jsons. /// Unlike create(), this function will return results with full path as PTracker object. -/// It is possible to create your own path tracker by implement the PTrackerGenerator triat. +/// It is possible to create your own path tracker by implement the PTrackerGenerator trait. pub fn create_with_generator<'i>(query: &'i Query<'i>) -> PathCalculator<'i, PTrackerGenerator> { PathCalculator::create_with_generator(query, PTrackerGenerator) } @@ -63,8 +63,8 @@ pub fn compile(s: &str) -> Result { /// The query ownership is taken so it can not be used after. This allows /// the get a better performance if there is a need to calculate the query /// only once. -pub fn calc_once<'j, 'p, S: SelectValue>(mut q: Query<'j>, json: &'p S) -> Vec<&'p S> { - let root = q.query.next().unwrap(); +pub fn calc_once<'j, 'p, S: SelectValue>(q: Query<'j>, json: &'p S) -> Vec<&'p S> { + let root = q.root; PathCalculator::<'p, DummyTrackerGenerator> { query: None, tracker_generator: None, @@ -77,10 +77,10 @@ pub fn calc_once<'j, 'p, S: SelectValue>(mut q: Query<'j>, json: &'p S) -> Vec<& /// A version of `calc_once` that returns also paths. pub fn calc_once_with_paths<'j, 'p, S: SelectValue>( - mut q: Query<'j>, + q: Query<'j>, json: &'p S, ) -> Vec> { - let root = q.query.next().unwrap(); + let root = q.root; PathCalculator { query: None, tracker_generator: Some(PTrackerGenerator), @@ -89,8 +89,8 @@ pub fn calc_once_with_paths<'j, 'p, S: SelectValue>( } /// A version of `calc_once` that returns only paths as Vec>. -pub fn calc_once_paths(mut q: Query, json: &S) -> Vec> { - let root = q.query.next().unwrap(); +pub fn calc_once_paths(q: Query, json: &S) -> Vec> { + let root = q.root; PathCalculator { query: None, tracker_generator: Some(PTrackerGenerator), diff --git a/tests/filter.rs b/tests/filter.rs index 7758d67..5f460c4 100644 --- a/tests/filter.rs +++ b/tests/filter.rs @@ -319,3 +319,80 @@ fn unimplemented_in_filter() { json!([]), ); } + +// #[test] +// fn filter_nested() { +// setup(); + +// select_and_then_compare( +// "$.store.book[?(@.authors[?(@.lastName == 'Rees')])].title", +// json!([ +// { +// "store": { +// "book": [ +// { +// "authors": [ +// { +// "firstName": "Nigel", +// "lastName": "Rees" +// }, +// { +// "firstName": "Evelyn", +// "lastName": "Waugh" +// } +// ], +// "title": "Sayings of the Century" +// }, +// { +// "authors": [ +// { +// "firstName": "Herman", +// "lastName": "Melville" +// }, +// { +// "firstName": "Somebody", +// "lastName": "Else" +// } +// ], +// "title": "Moby Dick" +// } +// ] +// } +// } +// ]), +// json!(["Sayings of the Century"]), +// ); +// } + +// #[test] +// fn filter_inner() { +// setup(); + +// select_and_then_compare( +// "$['a'][?(@.inner.for.inner=='u8')].id", +// json!([ +// { +// "a": { +// "id": "0:4", +// "inner": { +// "for": {"inner": "u8", "kind": "primitive"} +// } +// } +// } +// ]), +// json!(["0:4"]), +// ); +// } + +#[test] +fn op_object_or_nonexisting_default() { + setup(); + + select_and_then_compare( + "$.friends[?(@.id >= 2 || @.id == 4)]", + read_json("./json_examples/data_obj.json"), + json!([ + { "id" : 2, "name" : "Gray Berry" } + ]), + ); +} diff --git a/tests/paths.rs b/tests/paths.rs index fe2878a..d3029c9 100644 --- a/tests/paths.rs +++ b/tests/paths.rs @@ -113,3 +113,20 @@ fn dolla_token_in_path() { ]), ); } + +#[test] +fn colon_token_in_path() { + setup(); + + let payload = json!({ + "prod:id": "G637", + "prod_name": "coffee table", + "price": 194 + }); + + select_and_then_compare("$.price", payload.clone(), json!([194])); + + select_and_then_compare("$.prod_name", payload.clone(), json!(["coffee table"])); + + select_and_then_compare("$.prod:id", payload.clone(), json!(["G637"])); +}