From 2d194b40c50e4cfd7965974a9860240599d7b1bd Mon Sep 17 00:00:00 2001 From: meir Date: Tue, 3 May 2022 18:18:27 +0300 Subject: [PATCH 1/2] Added jsonpath functionalities 1. Pop last token from json path 2. Check if the json path is static path (lead to single result) 3. Check the amount of tokens on the json path --- src/json_path.rs | 147 ++++++++++++++++++++++++++++++++++++++++++++--- src/lib.rs | 12 ++-- 2 files changed, 146 insertions(+), 13 deletions(-) diff --git a/src/json_path.rs b/src/json_path.rs index fd07b22..cde10a1 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,67 @@ pub struct QueryCompilationError { message: String, } +impl<'i> Query<'i> { + 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, + } + } + + pub fn size(&mut self) -> usize { + if self.size.is_some() { + return *self.size.as_ref().unwrap(); + } + self.is_static(); + self.size() + } + + pub fn is_static(&mut self) -> bool { + if self.is_static.is_some() { + return *self.is_static.as_ref().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 +119,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 { @@ -199,7 +275,10 @@ const fn create_empty_trucker<'i, 'j>() -> PathTracker<'i, 'j> { } } -const fn create_str_trucker<'i, 'j>(s: &'i str, father: &'j PathTracker<'i, 'j>) -> PathTracker<'i, 'j> { +const fn create_str_trucker<'i, 'j>( + s: &'i str, + father: &'j PathTracker<'i, 'j>, +) -> PathTracker<'i, 'j> { PathTracker { father: Some(father), element: PathTrackerElement::Key(s), @@ -883,7 +962,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 +970,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()), &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 +986,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> { @@ -924,3 +1003,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..66ec7af 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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), From 36bc8ab1cf69fdfd90453ad3f1e38c945428b028 Mon Sep 17 00:00:00 2001 From: oshadmi Date: Wed, 17 Aug 2022 16:14:59 +0300 Subject: [PATCH 2/2] jsonpath_rs code review --- README.md | 2 +- src/grammer.pest | 2 +- src/json_node.rs | 15 +++++----- src/json_path.rs | 71 +++++++++++++++++++++----------------------- src/lib.rs | 2 +- tests/filter.rs | 77 ++++++++++++++++++++++++++++++++++++++++++++++++ tests/paths.rs | 17 +++++++++++ 7 files changed, 139 insertions(+), 47 deletions(-) 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 cde10a1..aa60d29 100644 --- a/src/json_path.rs +++ b/src/json_path.rs @@ -30,6 +30,7 @@ pub struct QueryCompilationError { } impl<'i> Query<'i> { + #[allow(dead_code)] pub fn pop_last(&mut self) -> Option<(String, JsonPathToken)> { let last = self.root.next_back(); match last { @@ -56,17 +57,19 @@ impl<'i> Query<'i> { } } + #[allow(dead_code)] pub fn size(&mut self) -> usize { if self.size.is_some() { - return *self.size.as_ref().unwrap(); + 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.as_ref().unwrap(); + return self.is_static.unwrap(); } let mut size = 0; let mut is_static = true; @@ -264,33 +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>( +const fn create_str_tracker<'i, 'j>( s: &'i str, - father: &'j PathTracker<'i, 'j>, + 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), } } @@ -343,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, } } @@ -391,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(), @@ -444,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, @@ -469,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, ); } @@ -495,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, ); } @@ -529,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 { @@ -543,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 { @@ -567,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); @@ -597,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); } } @@ -646,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); } } @@ -724,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); } } @@ -863,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 { @@ -920,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, @@ -972,7 +968,7 @@ impl<'i, UPTG: UserPathTrackerGenerator> PathCalculator<'i, UPTG> { self.calc_internal( root, json, - Some(create_empty_trucker()), + Some(create_empty_tracker()), &mut calc_data, true, ); @@ -996,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() diff --git a/src/lib.rs b/src/lib.rs index 66ec7af..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) } 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"])); +}