From c01a942f4a7388d61188a2fa141ad7d886a65193 Mon Sep 17 00:00:00 2001 From: Trevor Hilton Date: Thu, 25 Jan 2024 21:44:57 -0500 Subject: [PATCH 01/21] feat: a start on normalized path support This commit introduces the NormalizedPath type to represent normalized paths from the JSONPath spec. The Queryable trait includes a query_paths method that is used to produce a list of normalized paths vs. the standard query method, which produces the nodes. A few of the spec types had this implemented in their impl for Queryable, but this is incomplete. --- serde_json_path_core/src/spec/mod.rs | 1 + serde_json_path_core/src/spec/path.rs | 83 +++++++++++++++++++ serde_json_path_core/src/spec/query.rs | 17 +++- serde_json_path_core/src/spec/segment.rs | 18 ++++ .../src/spec/selector/filter.rs | 18 ++++ .../src/spec/selector/index.rs | 27 +++++- serde_json_path_core/src/spec/selector/mod.rs | 41 ++++++++- .../src/spec/selector/name.rs | 16 +++- .../src/spec/selector/slice.rs | 9 ++ 9 files changed, 226 insertions(+), 4 deletions(-) create mode 100644 serde_json_path_core/src/spec/path.rs diff --git a/serde_json_path_core/src/spec/mod.rs b/serde_json_path_core/src/spec/mod.rs index 585baf5..2dc84e1 100644 --- a/serde_json_path_core/src/spec/mod.rs +++ b/serde_json_path_core/src/spec/mod.rs @@ -1,5 +1,6 @@ //! Types representing the IETF JSONPath Standard pub mod functions; +pub mod path; pub mod query; pub mod segment; pub mod selector; diff --git a/serde_json_path_core/src/spec/path.rs b/serde_json_path_core/src/spec/path.rs new file mode 100644 index 0000000..40792df --- /dev/null +++ b/serde_json_path_core/src/spec/path.rs @@ -0,0 +1,83 @@ +use std::ops::{Deref, DerefMut}; + +#[derive(Clone)] +pub struct NormalizedPath<'a>(Vec>); + +impl<'a> NormalizedPath<'a> { + pub fn as_json_pointer(&self) -> String { + self.0 + .iter() + .map(PathElement::as_json_pointer) + .fold(String::from(""), |mut acc, s| { + acc.push('/'); + acc.push_str(&s.replace('~', "~0").replace('/', "~1")); + acc + }) + } +} + +impl<'a> Deref for NormalizedPath<'a> { + type Target = Vec>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl<'a> DerefMut for NormalizedPath<'a> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +#[derive(Clone, Copy)] +pub enum PathElement<'a> { + Name(&'a str), + Index(usize), +} + +impl<'a> PathElement<'a> { + fn as_json_pointer(&self) -> String { + match self { + PathElement::Name(ref s) => format!("{s}"), + PathElement::Index(i) => format!("{i}"), + } + } +} + +impl<'a> From<&'a String> for PathElement<'a> { + fn from(s: &'a String) -> Self { + Self::Name(s.as_str()) + } +} + +impl<'a> From for PathElement<'a> { + fn from(index: usize) -> Self { + Self::Index(index) + } +} + +#[cfg(test)] +mod tests { + use super::{NormalizedPath, PathElement}; + + #[test] + fn normalized_path_to_json_pointer() { + let np = NormalizedPath(vec![ + PathElement::Name("foo"), + PathElement::Index(42), + PathElement::Name("bar"), + ]); + assert_eq!(np.as_json_pointer(), "/foo/42/bar",); + } + + #[test] + fn normalized_path_to_json_pointer_with_escapes() { + let np = NormalizedPath(vec![ + PathElement::Name("foo~bar"), + PathElement::Index(42), + PathElement::Name("baz/bop"), + ]); + assert_eq!(np.as_json_pointer(), "/foo~0bar/42/baz~1bop",); + } +} diff --git a/serde_json_path_core/src/spec/query.rs b/serde_json_path_core/src/spec/query.rs index 5ee171a..c3e73c2 100644 --- a/serde_json_path_core/src/spec/query.rs +++ b/serde_json_path_core/src/spec/query.rs @@ -1,7 +1,7 @@ //! Types representing queries in JSONPath use serde_json::Value; -use super::segment::QuerySegment; +use super::{path::NormalizedPath, segment::QuerySegment}; mod sealed { use crate::spec::{ @@ -33,6 +33,12 @@ mod sealed { pub trait Queryable: sealed::Sealed { /// Query `self` using a current node, and the root node fn query<'b>(&self, current: &'b Value, root: &'b Value) -> Vec<&'b Value>; + fn query_paths<'b>( + &self, + current: &'b Value, + _root: &'b Value, + parent: NormalizedPath<'b>, + ) -> Vec>; } /// Represents a JSONPath expression @@ -97,4 +103,13 @@ impl Queryable for Query { } query } + + fn query_paths<'b>( + &self, + current: &'b Value, + _root: &'b Value, + parent: NormalizedPath<'b>, + ) -> Vec> { + todo!() + } } diff --git a/serde_json_path_core/src/spec/segment.rs b/serde_json_path_core/src/spec/segment.rs index 5ce1b0b..5671c7b 100644 --- a/serde_json_path_core/src/spec/segment.rs +++ b/serde_json_path_core/src/spec/segment.rs @@ -55,6 +55,15 @@ impl Queryable for QuerySegment { } query } + + fn query_paths<'b>( + &self, + current: &'b Value, + _root: &'b Value, + parent: super::path::NormalizedPath<'b>, + ) -> Vec> { + todo!() + } } #[cfg_attr(feature = "trace", tracing::instrument(name = "Descend", level = "trace", parent = None, ret))] @@ -174,4 +183,13 @@ impl Queryable for Segment { } query } + + fn query_paths<'b>( + &self, + current: &'b Value, + _root: &'b Value, + parent: super::path::NormalizedPath<'b>, + ) -> Vec> { + todo!() + } } diff --git a/serde_json_path_core/src/spec/selector/filter.rs b/serde_json_path_core/src/spec/selector/filter.rs index 563a535..bf1394f 100644 --- a/serde_json_path_core/src/spec/selector/filter.rs +++ b/serde_json_path_core/src/spec/selector/filter.rs @@ -69,6 +69,15 @@ impl Queryable for Filter { vec![] } } + + fn query_paths<'b>( + &self, + current: &'b Value, + _root: &'b Value, + parent: crate::spec::path::NormalizedPath<'b>, + ) -> Vec> { + todo!() + } } /// The top level boolean expression type @@ -565,6 +574,15 @@ impl Queryable for SingularQuery { None => vec![], } } + + fn query_paths<'b>( + &self, + current: &'b Value, + _root: &'b Value, + parent: crate::spec::path::NormalizedPath<'b>, + ) -> Vec> { + todo!() + } } impl std::fmt::Display for SingularQuery { diff --git a/serde_json_path_core/src/spec/selector/index.rs b/serde_json_path_core/src/spec/selector/index.rs index ab369df..3a2c6a6 100644 --- a/serde_json_path_core/src/spec/selector/index.rs +++ b/serde_json_path_core/src/spec/selector/index.rs @@ -1,7 +1,7 @@ //! Index selectors in JSONPath use serde_json::Value; -use crate::spec::query::Queryable; +use crate::spec::{path::NormalizedPath, query::Queryable}; /// For selecting array elements by their index /// @@ -38,6 +38,31 @@ impl Queryable for Index { vec![] } } + + fn query_paths<'b>( + &self, + current: &'b Value, + _root: &'b Value, + mut parent: NormalizedPath<'b>, + ) -> Vec> { + if let Some(index) = current.as_array().and_then(|list| { + if self.0 < 0 { + self.0 + .checked_abs() + .and_then(|i| usize::try_from(i).ok()) + .and_then(|i| list.len().checked_sub(i)) + } else { + usize::try_from(self.0) + .ok() + .and_then(|i| (list.len() >= i).then_some(i)) + } + }) { + parent.push(index.into()); + vec![parent] + } else { + vec![] + } + } } impl From for Index { diff --git a/serde_json_path_core/src/spec/selector/mod.rs b/serde_json_path_core/src/spec/selector/mod.rs index b6bf82e..a88f7b1 100644 --- a/serde_json_path_core/src/spec/selector/mod.rs +++ b/serde_json_path_core/src/spec/selector/mod.rs @@ -8,7 +8,10 @@ use serde_json::Value; use self::{filter::Filter, index::Index, name::Name, slice::Slice}; -use super::query::Queryable; +use super::{ + path::{NormalizedPath, PathElement}, + query::Queryable, +}; /// A JSONPath selector #[derive(Debug, PartialEq, Eq, Clone)] @@ -70,4 +73,40 @@ impl Queryable for Selector { } query } + + fn query_paths<'b>( + &self, + current: &'b Value, + root: &'b Value, + parent: NormalizedPath<'b>, + ) -> Vec> { + match self { + Selector::Name(name) => name.query_paths(current, root, parent), + Selector::Wildcard => { + if let Some(list) = current.as_array() { + list.iter() + .enumerate() + .map(|(i, _)| { + let mut new_path = parent.clone(); + new_path.push(PathElement::from(i)); + new_path + }) + .collect() + } else if let Some(obj) = current.as_object() { + obj.keys() + .map(|k| { + let mut new_path = parent.clone(); + new_path.push(PathElement::from(k)); + new_path + }) + .collect() + } else { + vec![] + } + } + Selector::Index(index) => index.query_paths(current, root, parent), + Selector::ArraySlice(slice) => slice.query_paths(current, root, parent), + Selector::Filter(filter) => filter.query_paths(current, root, parent), + } + } } diff --git a/serde_json_path_core/src/spec/selector/name.rs b/serde_json_path_core/src/spec/selector/name.rs index de2c350..0f116b0 100644 --- a/serde_json_path_core/src/spec/selector/name.rs +++ b/serde_json_path_core/src/spec/selector/name.rs @@ -1,7 +1,7 @@ //! Name selector for selecting object keys in JSONPath use serde_json::Value; -use crate::spec::query::Queryable; +use crate::spec::{path::NormalizedPath, query::Queryable}; /// Select a single JSON object key #[derive(Debug, PartialEq, Eq, Clone)] @@ -29,6 +29,20 @@ impl Queryable for Name { vec![] } } + + fn query_paths<'b>( + &self, + current: &'b Value, + _root: &'b Value, + mut parent: NormalizedPath<'b>, + ) -> Vec> { + if let Some((s, _)) = current.as_object().and_then(|o| o.get_key_value(&self.0)) { + parent.push(s.into()); + vec![parent] + } else { + vec![] + } + } } impl From<&str> for Name { diff --git a/serde_json_path_core/src/spec/selector/slice.rs b/serde_json_path_core/src/spec/selector/slice.rs index ffe1140..551f14d 100644 --- a/serde_json_path_core/src/spec/selector/slice.rs +++ b/serde_json_path_core/src/spec/selector/slice.rs @@ -118,6 +118,15 @@ impl Queryable for Slice { vec![] } } + + fn query_paths<'b>( + &self, + current: &'b Value, + _root: &'b Value, + parent: crate::spec::path::NormalizedPath<'b>, + ) -> Vec> { + todo!() + } } fn normalize_slice_index(index: isize, len: isize) -> Option { From cca17b47ab14c34f9f93ecdbbc05bf7f953a0d01 Mon Sep 17 00:00:00 2001 From: Trevor Hilton Date: Sat, 27 Jan 2024 18:00:33 -0500 Subject: [PATCH 02/21] feat: working implementation of normalized paths This provides a complete working implementation of the NormalizedPath type, and provides a method to query for it along with nodes on JsonPath. A fair bit of work is still needed to get the API right, but committing here to have it in a working state. An unused implementation of Queryable on SingularQuery was removed. --- serde_json_path/src/path.rs | 21 +++- serde_json_path_core/src/spec/path.rs | 30 +++--- serde_json_path_core/src/spec/query.rs | 30 ++++-- serde_json_path_core/src/spec/segment.rs | 68 +++++++++++-- .../src/spec/selector/filter.rs | 40 ++++---- .../src/spec/selector/index.rs | 11 ++- serde_json_path_core/src/spec/selector/mod.rs | 12 +-- .../src/spec/selector/name.rs | 8 +- .../src/spec/selector/slice.rs | 98 ++++++++++++++----- 9 files changed, 222 insertions(+), 96 deletions(-) diff --git a/serde_json_path/src/path.rs b/serde_json_path/src/path.rs index c957370..756dd49 100644 --- a/serde_json_path/src/path.rs +++ b/serde_json_path/src/path.rs @@ -4,7 +4,10 @@ use serde::{de::Visitor, Deserialize, Serialize}; use serde_json::Value; use serde_json_path_core::{ node::NodeList, - spec::query::{Query, Queryable}, + spec::{ + path::NormalizedPath, + query::{Query, Queryable}, + }, }; use crate::{parser::parse_query_main, ParseError}; @@ -75,6 +78,10 @@ impl JsonPath { pub fn query<'b>(&self, value: &'b Value) -> NodeList<'b> { self.0.query(value, value).into() } + + pub fn query_paths<'b>(&self, value: &'b Value) -> Vec<(NormalizedPath<'b>, &'b Value)> { + self.0.query_paths_init(value, value) + } } impl FromStr for JsonPath { @@ -153,4 +160,16 @@ mod tests { .expect("round trip"); assert_eq!(p1, p2); } + + #[test] + fn norm_paths() { + let j = json!({"foo": { + "bar": [1, 2, 3] + }}); + let p = JsonPath::parse("$.foo.bar.*").unwrap(); + let r = p.query_paths(&j); + for (np, _) in r { + println!("{pointer}", pointer = np.as_json_pointer()); + } + } } diff --git a/serde_json_path_core/src/spec/path.rs b/serde_json_path_core/src/spec/path.rs index 40792df..6aad4cd 100644 --- a/serde_json_path_core/src/spec/path.rs +++ b/serde_json_path_core/src/spec/path.rs @@ -1,9 +1,17 @@ -use std::ops::{Deref, DerefMut}; - -#[derive(Clone)] +#[derive(Debug, Clone, Default)] pub struct NormalizedPath<'a>(Vec>); impl<'a> NormalizedPath<'a> { + pub(crate) fn push>>(&mut self, elem: T) { + self.0.push(elem.into()) + } + + pub(crate) fn clone_and_push>>(&self, elem: T) -> Self { + let mut new_path = self.clone(); + new_path.push(elem.into()); + new_path + } + pub fn as_json_pointer(&self) -> String { self.0 .iter() @@ -16,21 +24,7 @@ impl<'a> NormalizedPath<'a> { } } -impl<'a> Deref for NormalizedPath<'a> { - type Target = Vec>; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl<'a> DerefMut for NormalizedPath<'a> { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - -#[derive(Clone, Copy)] +#[derive(Debug, Clone, Copy)] pub enum PathElement<'a> { Name(&'a str), Index(usize), diff --git a/serde_json_path_core/src/spec/query.rs b/serde_json_path_core/src/spec/query.rs index c3e73c2..9a71fae 100644 --- a/serde_json_path_core/src/spec/query.rs +++ b/serde_json_path_core/src/spec/query.rs @@ -36,9 +36,16 @@ pub trait Queryable: sealed::Sealed { fn query_paths<'b>( &self, current: &'b Value, - _root: &'b Value, + root: &'b Value, parent: NormalizedPath<'b>, - ) -> Vec>; + ) -> Vec<(NormalizedPath<'b>, &'b Value)>; + fn query_paths_init<'b>( + &self, + current: &'b Value, + root: &'b Value, + ) -> Vec<(NormalizedPath<'b>, &'b Value)> { + self.query_paths(current, root, Default::default()) + } } /// Represents a JSONPath expression @@ -107,9 +114,20 @@ impl Queryable for Query { fn query_paths<'b>( &self, current: &'b Value, - _root: &'b Value, - parent: NormalizedPath<'b>, - ) -> Vec> { - todo!() + root: &'b Value, + _parent: NormalizedPath<'b>, + ) -> Vec<(NormalizedPath<'b>, &'b Value)> { + let mut result: Vec<(NormalizedPath<'b>, &Value)> = match self.kind { + QueryKind::Root => vec![(Default::default(), root)], + QueryKind::Current => vec![(Default::default(), current)], + }; + for s in &self.segments { + let mut r = vec![]; + for (ref np, v) in result { + r.append(&mut s.query_paths(v, root, np.clone())); + } + result = r; + } + result } } diff --git a/serde_json_path_core/src/spec/segment.rs b/serde_json_path_core/src/spec/segment.rs index 5671c7b..f35d81f 100644 --- a/serde_json_path_core/src/spec/segment.rs +++ b/serde_json_path_core/src/spec/segment.rs @@ -1,7 +1,7 @@ //! Types representing segments in JSONPath use serde_json::Value; -use super::{query::Queryable, selector::Selector}; +use super::{path::NormalizedPath, query::Queryable, selector::Selector}; /// A segment of a JSONPath query #[derive(Debug, PartialEq, Eq, Clone)] @@ -59,10 +59,16 @@ impl Queryable for QuerySegment { fn query_paths<'b>( &self, current: &'b Value, - _root: &'b Value, - parent: super::path::NormalizedPath<'b>, - ) -> Vec> { - todo!() + root: &'b Value, + parent: NormalizedPath<'b>, + ) -> Vec<(NormalizedPath<'b>, &'b Value)> { + if matches!(self.kind, QuerySegmentKind::Descendant) { + let mut result = self.segment.query_paths(current, root, parent.clone()); + result.append(&mut descend_paths(self, current, root, parent)); + result + } else { + self.segment.query_paths(current, root, parent) + } } } @@ -81,6 +87,25 @@ fn descend<'b>(segment: &QuerySegment, current: &'b Value, root: &'b Value) -> V query } +fn descend_paths<'b>( + segment: &QuerySegment, + current: &'b Value, + root: &'b Value, + parent: NormalizedPath<'b>, +) -> Vec<(NormalizedPath<'b>, &'b Value)> { + let mut result = Vec::new(); + if let Some(list) = current.as_array() { + for (i, v) in list.iter().enumerate() { + result.append(&mut segment.query_paths(v, root, parent.clone_and_push(i))); + } + } else if let Some(obj) = current.as_object() { + for (k, v) in obj { + result.append(&mut segment.query_paths(v, root, parent.clone_and_push(k))); + } + } + result +} + /// Represents the different forms of JSONPath segment #[derive(Debug, PartialEq, Eq, Clone)] pub enum Segment { @@ -187,9 +212,34 @@ impl Queryable for Segment { fn query_paths<'b>( &self, current: &'b Value, - _root: &'b Value, - parent: super::path::NormalizedPath<'b>, - ) -> Vec> { - todo!() + root: &'b Value, + mut parent: NormalizedPath<'b>, + ) -> Vec<(NormalizedPath<'b>, &'b Value)> { + let mut result = vec![]; + match self { + Segment::LongHand(selectors) => { + for s in selectors { + result.append(&mut s.query_paths(current, root, parent.clone())); + } + } + Segment::DotName(name) => { + if let Some((k, v)) = current.as_object().and_then(|o| o.get_key_value(name)) { + parent.push(k); + result.push((parent, v)); + } + } + Segment::Wildcard => { + if let Some(list) = current.as_array() { + for (i, v) in list.iter().enumerate() { + result.push((parent.clone_and_push(i), v)); + } + } else if let Some(obj) = current.as_object() { + for (k, v) in obj { + result.push((parent.clone_and_push(k), v)); + } + } + } + } + result } } diff --git a/serde_json_path_core/src/spec/selector/filter.rs b/serde_json_path_core/src/spec/selector/filter.rs index bf1394f..cb5e30e 100644 --- a/serde_json_path_core/src/spec/selector/filter.rs +++ b/serde_json_path_core/src/spec/selector/filter.rs @@ -3,6 +3,7 @@ use serde_json::{Number, Value}; use crate::spec::{ functions::{FunctionExpr, JsonPathValue, Validated}, + path::NormalizedPath, query::{Query, QueryKind, Queryable}, segment::{QuerySegment, Segment}, }; @@ -73,10 +74,23 @@ impl Queryable for Filter { fn query_paths<'b>( &self, current: &'b Value, - _root: &'b Value, - parent: crate::spec::path::NormalizedPath<'b>, - ) -> Vec> { - todo!() + root: &'b Value, + parent: NormalizedPath<'b>, + ) -> Vec<(NormalizedPath<'b>, &'b Value)> { + if let Some(list) = current.as_array() { + list.iter() + .enumerate() + .filter(|(_, v)| self.0.test_filter(v, root)) + .map(|(i, v)| (parent.clone_and_push(i), v)) + .collect() + } else if let Some(obj) = current.as_object() { + obj.iter() + .filter(|(_, v)| self.0.test_filter(v, root)) + .map(|(k, v)| (parent.clone_and_push(k), v)) + .collect() + } else { + vec![] + } } } @@ -567,24 +581,6 @@ impl TryFrom for SingularQuery { } } -impl Queryable for SingularQuery { - fn query<'b>(&self, current: &'b Value, root: &'b Value) -> Vec<&'b Value> { - match self.eval_query(current, root) { - Some(v) => vec![v], - None => vec![], - } - } - - fn query_paths<'b>( - &self, - current: &'b Value, - _root: &'b Value, - parent: crate::spec::path::NormalizedPath<'b>, - ) -> Vec> { - todo!() - } -} - impl std::fmt::Display for SingularQuery { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self.kind { diff --git a/serde_json_path_core/src/spec/selector/index.rs b/serde_json_path_core/src/spec/selector/index.rs index 3a2c6a6..55d5a4a 100644 --- a/serde_json_path_core/src/spec/selector/index.rs +++ b/serde_json_path_core/src/spec/selector/index.rs @@ -44,21 +44,22 @@ impl Queryable for Index { current: &'b Value, _root: &'b Value, mut parent: NormalizedPath<'b>, - ) -> Vec> { - if let Some(index) = current.as_array().and_then(|list| { + ) -> Vec<(NormalizedPath<'b>, &'b Value)> { + if let Some((index, value)) = current.as_array().and_then(|list| { if self.0 < 0 { self.0 .checked_abs() .and_then(|i| usize::try_from(i).ok()) .and_then(|i| list.len().checked_sub(i)) + .and_then(|i| list.get(i).map(|v| (i, v))) } else { usize::try_from(self.0) .ok() - .and_then(|i| (list.len() >= i).then_some(i)) + .and_then(|i| list.get(i).map(|v| (i, v))) } }) { - parent.push(index.into()); - vec![parent] + parent.push(index); + vec![(parent, value)] } else { vec![] } diff --git a/serde_json_path_core/src/spec/selector/mod.rs b/serde_json_path_core/src/spec/selector/mod.rs index a88f7b1..1c0dac3 100644 --- a/serde_json_path_core/src/spec/selector/mod.rs +++ b/serde_json_path_core/src/spec/selector/mod.rs @@ -79,25 +79,25 @@ impl Queryable for Selector { current: &'b Value, root: &'b Value, parent: NormalizedPath<'b>, - ) -> Vec> { + ) -> Vec<(NormalizedPath<'b>, &'b Value)> { match self { Selector::Name(name) => name.query_paths(current, root, parent), Selector::Wildcard => { if let Some(list) = current.as_array() { list.iter() .enumerate() - .map(|(i, _)| { + .map(|(i, v)| { let mut new_path = parent.clone(); new_path.push(PathElement::from(i)); - new_path + (new_path, v) }) .collect() } else if let Some(obj) = current.as_object() { - obj.keys() - .map(|k| { + obj.iter() + .map(|(k, v)| { let mut new_path = parent.clone(); new_path.push(PathElement::from(k)); - new_path + (new_path, v) }) .collect() } else { diff --git a/serde_json_path_core/src/spec/selector/name.rs b/serde_json_path_core/src/spec/selector/name.rs index 0f116b0..b47dc3c 100644 --- a/serde_json_path_core/src/spec/selector/name.rs +++ b/serde_json_path_core/src/spec/selector/name.rs @@ -35,10 +35,10 @@ impl Queryable for Name { current: &'b Value, _root: &'b Value, mut parent: NormalizedPath<'b>, - ) -> Vec> { - if let Some((s, _)) = current.as_object().and_then(|o| o.get_key_value(&self.0)) { - parent.push(s.into()); - vec![parent] + ) -> Vec<(NormalizedPath<'b>, &'b Value)> { + if let Some((s, v)) = current.as_object().and_then(|o| o.get_key_value(&self.0)) { + parent.push(s); + vec![(parent, v)] } else { vec![] } diff --git a/serde_json_path_core/src/spec/selector/slice.rs b/serde_json_path_core/src/spec/selector/slice.rs index 551f14d..1e44bfb 100644 --- a/serde_json_path_core/src/spec/selector/slice.rs +++ b/serde_json_path_core/src/spec/selector/slice.rs @@ -1,7 +1,7 @@ //! Slice selectors for selecting array slices in JSONPath use serde_json::Value; -use crate::spec::query::Queryable; +use crate::spec::{path::NormalizedPath, query::Queryable}; /// A slice selector #[derive(Debug, PartialEq, Eq, Default, Clone, Copy)] @@ -59,6 +59,34 @@ impl Slice { self.step = Some(step); self } + + #[inline] + fn bounds_on_forward_slice(&self, len: isize) -> (isize, isize) { + let start_default = self.start.unwrap_or(0); + let end_default = self.end.unwrap_or(len); + let start = normalize_slice_index(start_default, len) + .unwrap_or(0) + .max(0); + let end = normalize_slice_index(end_default, len).unwrap_or(0).max(0); + let lower = start.min(len); + let upper = end.min(len); + (lower, upper) + } + + #[inline] + fn bounds_on_reverse_slice(&self, len: isize) -> Option<(isize, isize)> { + let start_default = self.start.or_else(|| len.checked_sub(1))?; + let end_default = self + .end + .or_else(|| len.checked_mul(-1).and_then(|l| l.checked_sub(1)))?; + let start = normalize_slice_index(start_default, len) + .unwrap_or(0) + .max(-1); + let end = normalize_slice_index(end_default, len).unwrap_or(0).max(-1); + let lower = end.min(len.checked_sub(1).unwrap_or(len)); + let upper = start.min(len.checked_sub(1).unwrap_or(len)); + Some((lower, upper)) + } } impl Queryable for Slice { @@ -74,14 +102,7 @@ impl Queryable for Slice { return vec![]; }; if step > 0 { - let start_default = self.start.unwrap_or(0); - let end_default = self.end.unwrap_or(len); - let start = normalize_slice_index(start_default, len) - .unwrap_or(0) - .max(0); - let end = normalize_slice_index(end_default, len).unwrap_or(0).max(0); - let lower = start.min(len); - let upper = end.min(len); + let (lower, upper) = self.bounds_on_forward_slice(len); let mut i = lower; while i < upper { if let Some(v) = usize::try_from(i).ok().and_then(|i| list.get(i)) { @@ -90,21 +111,9 @@ impl Queryable for Slice { i += step; } } else { - let Some(start_default) = self.start.or_else(|| len.checked_sub(1)) else { + let Some((lower, upper)) = self.bounds_on_reverse_slice(len) else { return vec![]; }; - let Some(end_default) = self - .end - .or_else(|| len.checked_mul(-1).and_then(|l| l.checked_sub(1))) - else { - return vec![]; - }; - let start = normalize_slice_index(start_default, len) - .unwrap_or(0) - .max(-1); - let end = normalize_slice_index(end_default, len).unwrap_or(0).max(-1); - let lower = end.min(len.checked_sub(1).unwrap_or(len)); - let upper = start.min(len.checked_sub(1).unwrap_or(len)); let mut i = upper; while lower < i { if let Some(v) = usize::try_from(i).ok().and_then(|i| list.get(i)) { @@ -123,9 +132,48 @@ impl Queryable for Slice { &self, current: &'b Value, _root: &'b Value, - parent: crate::spec::path::NormalizedPath<'b>, - ) -> Vec> { - todo!() + parent: NormalizedPath<'b>, + ) -> Vec<(NormalizedPath<'b>, &'b Value)> { + if let Some(list) = current.as_array() { + let mut result = Vec::new(); + let step = self.step.unwrap_or(1); + if step == 0 { + return vec![]; + } + let Ok(len) = isize::try_from(list.len()) else { + return vec![]; + }; + if step > 0 { + let (lower, upper) = self.bounds_on_forward_slice(len); + let mut i = lower; + while i < upper { + if let Some((i, v)) = usize::try_from(i) + .ok() + .and_then(|i| list.get(i).map(|v| (i, v))) + { + result.push((parent.clone_and_push(i), v)); + } + i += step; + } + } else { + let Some((lower, upper)) = self.bounds_on_reverse_slice(len) else { + return vec![]; + }; + let mut i = upper; + while lower < i { + if let Some((i, v)) = usize::try_from(i) + .ok() + .and_then(|i| list.get(i).map(|v| (i, v))) + { + result.push((parent.clone_and_push(i), v)); + } + i += step; + } + } + result + } else { + vec![] + } } } From f2c23c82e754417d0654969c1fa17419ce4679c4 Mon Sep 17 00:00:00 2001 From: Trevor Hilton Date: Sun, 28 Jan 2024 09:02:07 -0500 Subject: [PATCH 03/21] feat: add LocatedNodeList type The LocatedNodeList is similar to NodeList but provides the location along with each node. Queryable::query_paths was renamed to query_located to coincide with LocatedNodeList Trait implementations added to NormalizedPath and PathElement types. --- serde_json_path/src/path.rs | 10 ++- serde_json_path_core/src/node.rs | 75 ++++++++++++++++++- serde_json_path_core/src/spec/path.rs | 73 +++++++++++++++++- serde_json_path_core/src/spec/query.rs | 13 +--- serde_json_path_core/src/spec/segment.rs | 14 ++-- .../src/spec/selector/filter.rs | 2 +- .../src/spec/selector/index.rs | 2 +- serde_json_path_core/src/spec/selector/mod.rs | 10 +-- .../src/spec/selector/name.rs | 2 +- .../src/spec/selector/slice.rs | 2 +- 10 files changed, 170 insertions(+), 33 deletions(-) diff --git a/serde_json_path/src/path.rs b/serde_json_path/src/path.rs index 756dd49..bb3832d 100644 --- a/serde_json_path/src/path.rs +++ b/serde_json_path/src/path.rs @@ -3,7 +3,7 @@ use std::str::FromStr; use serde::{de::Visitor, Deserialize, Serialize}; use serde_json::Value; use serde_json_path_core::{ - node::NodeList, + node::{LocatedNodeList, NodeList}, spec::{ path::NormalizedPath, query::{Query, Queryable}, @@ -79,8 +79,10 @@ impl JsonPath { self.0.query(value, value).into() } - pub fn query_paths<'b>(&self, value: &'b Value) -> Vec<(NormalizedPath<'b>, &'b Value)> { - self.0.query_paths_init(value, value) + pub fn query_located<'b>(&self, value: &'b Value) -> LocatedNodeList<'b> { + self.0 + .query_located(value, value, Default::default()) + .into() } } @@ -167,7 +169,7 @@ mod tests { "bar": [1, 2, 3] }}); let p = JsonPath::parse("$.foo.bar.*").unwrap(); - let r = p.query_paths(&j); + let r = p.query_located(&j); for (np, _) in r { println!("{pointer}", pointer = np.as_json_pointer()); } diff --git a/serde_json_path_core/src/node.rs b/serde_json_path_core/src/node.rs index c3534d5..24a3aa2 100644 --- a/serde_json_path_core/src/node.rs +++ b/serde_json_path_core/src/node.rs @@ -1,9 +1,11 @@ //! Types representing nodes within a JSON object -use std::slice::Iter; +use std::{cmp::Ordering, slice::Iter}; use serde::Serialize; use serde_json::Value; +use crate::spec::path::NormalizedPath; + /// A list of nodes resulting from a JSONPath query /// /// Each node within the list is a borrowed reference to the node in the original @@ -261,6 +263,77 @@ impl<'a> IntoIterator for NodeList<'a> { } } +#[derive(Debug, Default, Eq, PartialEq, Serialize, Clone)] +pub struct LocatedNodeList<'a>(Vec<(NormalizedPath<'a>, &'a Value)>); + +impl<'a> LocatedNodeList<'a> { + pub fn at_most_one( + mut self, + ) -> Result, &'a Value)>, AtMostOneError> { + if self.0.is_empty() { + Ok(None) + } else if self.0.len() > 1 { + Err(AtMostOneError(self.0.len())) + } else { + Ok(Some(self.0.pop().unwrap())) + } + } + + pub fn exactly_one(mut self) -> Result<(NormalizedPath<'a>, &'a Value), ExactlyOneError> { + if self.0.is_empty() { + Err(ExactlyOneError::Empty) + } else if self.0.len() > 1 { + Err(ExactlyOneError::MoreThanOne(self.0.len())) + } else { + Ok(self.0.pop().unwrap()) + } + } + + pub fn all(self) -> Vec<(NormalizedPath<'a>, &'a Value)> { + self.0 + } + + pub fn len(&self) -> usize { + self.0.len() + } + + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + pub fn iter(&self) -> Iter<'_, (NormalizedPath<'a>, &'a Value)> { + self.0.iter() + } + + pub fn dedup(mut self) -> Self { + self.0 + // This unwrap is safe, since the paths corresponding to + // a query against a Value will always be ordered. + // + // TODO - the below From impl may in theory allow someone + // to violate this, so may need to remedy that... + .sort_unstable_by(|a, b| a.0.partial_cmp(&b.0).unwrap()); + self.0.dedup(); + self + } +} + +impl<'a> From, &'a Value)>> for LocatedNodeList<'a> { + fn from(v: Vec<(NormalizedPath<'a>, &'a Value)>) -> Self { + Self(v) + } +} + +impl<'a> IntoIterator for LocatedNodeList<'a> { + type Item = (NormalizedPath<'a>, &'a Value); + + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + #[cfg(test)] mod tests { use crate::node::NodeList; diff --git a/serde_json_path_core/src/spec/path.rs b/serde_json_path_core/src/spec/path.rs index 6aad4cd..7ef6f4d 100644 --- a/serde_json_path_core/src/spec/path.rs +++ b/serde_json_path_core/src/spec/path.rs @@ -1,4 +1,8 @@ -#[derive(Debug, Clone, Default)] +use std::{cmp::Ordering, fmt::Display, slice::Iter}; + +use serde::Serialize; + +#[derive(Debug, Default, Eq, PartialEq, Clone, PartialOrd)] pub struct NormalizedPath<'a>(Vec>); impl<'a> NormalizedPath<'a> { @@ -22,9 +26,43 @@ impl<'a> NormalizedPath<'a> { acc }) } + + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + pub fn len(&self) -> usize { + self.0.len() + } + + pub fn iter(&self) -> Iter<'_, PathElement<'a>> { + self.0.iter() + } +} + +impl<'a> Display for NormalizedPath<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "$")?; + for elem in &self.0 { + match elem { + PathElement::Name(name) => write!(f, "['{name}']")?, + PathElement::Index(index) => write!(f, "[{index}]")?, + } + } + Ok(()) + } +} + +impl<'a> Serialize for NormalizedPath<'a> { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(self.to_string().as_str()) + } } -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Eq, PartialEq, Clone)] pub enum PathElement<'a> { Name(&'a str), Index(usize), @@ -39,6 +77,25 @@ impl<'a> PathElement<'a> { } } +impl<'a> PartialOrd for PathElement<'a> { + fn partial_cmp(&self, other: &Self) -> Option { + match (self, other) { + (PathElement::Name(a), PathElement::Name(b)) => a.partial_cmp(b), + (PathElement::Index(a), PathElement::Index(b)) => a.partial_cmp(b), + _ => None, + } + } +} + +impl<'a> Display for PathElement<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + PathElement::Name(n) => write!(f, "{n}"), + PathElement::Index(i) => write!(f, "{i}"), + } + } +} + impl<'a> From<&'a String> for PathElement<'a> { fn from(s: &'a String) -> Self { Self::Name(s.as_str()) @@ -51,6 +108,18 @@ impl<'a> From for PathElement<'a> { } } +impl<'a> Serialize for PathElement<'a> { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + match self { + PathElement::Name(s) => serializer.serialize_str(s), + PathElement::Index(i) => serializer.serialize_u64(*i as u64), + } + } +} + #[cfg(test)] mod tests { use super::{NormalizedPath, PathElement}; diff --git a/serde_json_path_core/src/spec/query.rs b/serde_json_path_core/src/spec/query.rs index 9a71fae..cd4a8e3 100644 --- a/serde_json_path_core/src/spec/query.rs +++ b/serde_json_path_core/src/spec/query.rs @@ -33,19 +33,12 @@ mod sealed { pub trait Queryable: sealed::Sealed { /// Query `self` using a current node, and the root node fn query<'b>(&self, current: &'b Value, root: &'b Value) -> Vec<&'b Value>; - fn query_paths<'b>( + fn query_located<'b>( &self, current: &'b Value, root: &'b Value, parent: NormalizedPath<'b>, ) -> Vec<(NormalizedPath<'b>, &'b Value)>; - fn query_paths_init<'b>( - &self, - current: &'b Value, - root: &'b Value, - ) -> Vec<(NormalizedPath<'b>, &'b Value)> { - self.query_paths(current, root, Default::default()) - } } /// Represents a JSONPath expression @@ -111,7 +104,7 @@ impl Queryable for Query { query } - fn query_paths<'b>( + fn query_located<'b>( &self, current: &'b Value, root: &'b Value, @@ -124,7 +117,7 @@ impl Queryable for Query { for s in &self.segments { let mut r = vec![]; for (ref np, v) in result { - r.append(&mut s.query_paths(v, root, np.clone())); + r.append(&mut s.query_located(v, root, np.clone())); } result = r; } diff --git a/serde_json_path_core/src/spec/segment.rs b/serde_json_path_core/src/spec/segment.rs index f35d81f..8c49a9f 100644 --- a/serde_json_path_core/src/spec/segment.rs +++ b/serde_json_path_core/src/spec/segment.rs @@ -56,18 +56,18 @@ impl Queryable for QuerySegment { query } - fn query_paths<'b>( + fn query_located<'b>( &self, current: &'b Value, root: &'b Value, parent: NormalizedPath<'b>, ) -> Vec<(NormalizedPath<'b>, &'b Value)> { if matches!(self.kind, QuerySegmentKind::Descendant) { - let mut result = self.segment.query_paths(current, root, parent.clone()); + let mut result = self.segment.query_located(current, root, parent.clone()); result.append(&mut descend_paths(self, current, root, parent)); result } else { - self.segment.query_paths(current, root, parent) + self.segment.query_located(current, root, parent) } } } @@ -96,11 +96,11 @@ fn descend_paths<'b>( let mut result = Vec::new(); if let Some(list) = current.as_array() { for (i, v) in list.iter().enumerate() { - result.append(&mut segment.query_paths(v, root, parent.clone_and_push(i))); + result.append(&mut segment.query_located(v, root, parent.clone_and_push(i))); } } else if let Some(obj) = current.as_object() { for (k, v) in obj { - result.append(&mut segment.query_paths(v, root, parent.clone_and_push(k))); + result.append(&mut segment.query_located(v, root, parent.clone_and_push(k))); } } result @@ -209,7 +209,7 @@ impl Queryable for Segment { query } - fn query_paths<'b>( + fn query_located<'b>( &self, current: &'b Value, root: &'b Value, @@ -219,7 +219,7 @@ impl Queryable for Segment { match self { Segment::LongHand(selectors) => { for s in selectors { - result.append(&mut s.query_paths(current, root, parent.clone())); + result.append(&mut s.query_located(current, root, parent.clone())); } } Segment::DotName(name) => { diff --git a/serde_json_path_core/src/spec/selector/filter.rs b/serde_json_path_core/src/spec/selector/filter.rs index cb5e30e..4c8555e 100644 --- a/serde_json_path_core/src/spec/selector/filter.rs +++ b/serde_json_path_core/src/spec/selector/filter.rs @@ -71,7 +71,7 @@ impl Queryable for Filter { } } - fn query_paths<'b>( + fn query_located<'b>( &self, current: &'b Value, root: &'b Value, diff --git a/serde_json_path_core/src/spec/selector/index.rs b/serde_json_path_core/src/spec/selector/index.rs index 55d5a4a..98b0036 100644 --- a/serde_json_path_core/src/spec/selector/index.rs +++ b/serde_json_path_core/src/spec/selector/index.rs @@ -39,7 +39,7 @@ impl Queryable for Index { } } - fn query_paths<'b>( + fn query_located<'b>( &self, current: &'b Value, _root: &'b Value, diff --git a/serde_json_path_core/src/spec/selector/mod.rs b/serde_json_path_core/src/spec/selector/mod.rs index 1c0dac3..596a47b 100644 --- a/serde_json_path_core/src/spec/selector/mod.rs +++ b/serde_json_path_core/src/spec/selector/mod.rs @@ -74,14 +74,14 @@ impl Queryable for Selector { query } - fn query_paths<'b>( + fn query_located<'b>( &self, current: &'b Value, root: &'b Value, parent: NormalizedPath<'b>, ) -> Vec<(NormalizedPath<'b>, &'b Value)> { match self { - Selector::Name(name) => name.query_paths(current, root, parent), + Selector::Name(name) => name.query_located(current, root, parent), Selector::Wildcard => { if let Some(list) = current.as_array() { list.iter() @@ -104,9 +104,9 @@ impl Queryable for Selector { vec![] } } - Selector::Index(index) => index.query_paths(current, root, parent), - Selector::ArraySlice(slice) => slice.query_paths(current, root, parent), - Selector::Filter(filter) => filter.query_paths(current, root, parent), + Selector::Index(index) => index.query_located(current, root, parent), + Selector::ArraySlice(slice) => slice.query_located(current, root, parent), + Selector::Filter(filter) => filter.query_located(current, root, parent), } } } diff --git a/serde_json_path_core/src/spec/selector/name.rs b/serde_json_path_core/src/spec/selector/name.rs index b47dc3c..1837d72 100644 --- a/serde_json_path_core/src/spec/selector/name.rs +++ b/serde_json_path_core/src/spec/selector/name.rs @@ -30,7 +30,7 @@ impl Queryable for Name { } } - fn query_paths<'b>( + fn query_located<'b>( &self, current: &'b Value, _root: &'b Value, diff --git a/serde_json_path_core/src/spec/selector/slice.rs b/serde_json_path_core/src/spec/selector/slice.rs index 1e44bfb..f7c5fc8 100644 --- a/serde_json_path_core/src/spec/selector/slice.rs +++ b/serde_json_path_core/src/spec/selector/slice.rs @@ -128,7 +128,7 @@ impl Queryable for Slice { } } - fn query_paths<'b>( + fn query_located<'b>( &self, current: &'b Value, _root: &'b Value, From 5b58d5d80a2334c34e8cde7c214ed0434817d654 Mon Sep 17 00:00:00 2001 From: Trevor Hilton Date: Sun, 28 Jan 2024 15:48:14 -0500 Subject: [PATCH 04/21] feat: add convenience iterators to LocatedNodeList The Locations and Nodes iterator types were added to support more convenient iteration over a LocatedNodeList query result. An in-place version of LocatedNodeList::dedup was added, to provide an alternative option to dedup's move semantics. Code re-orderd to put error types at the end of node.rs file. --- serde_json_path_core/src/node.rs | 149 ++++++++++++++++++++++--------- 1 file changed, 106 insertions(+), 43 deletions(-) diff --git a/serde_json_path_core/src/node.rs b/serde_json_path_core/src/node.rs index 24a3aa2..5ce4fb3 100644 --- a/serde_json_path_core/src/node.rs +++ b/serde_json_path_core/src/node.rs @@ -1,5 +1,5 @@ //! Types representing nodes within a JSON object -use std::{cmp::Ordering, slice::Iter}; +use std::{iter::FusedIterator, slice::Iter}; use serde::Serialize; use serde_json::Value; @@ -211,42 +211,6 @@ impl<'a> NodeList<'a> { } } -/// Error produced when expecting no more than one node from a query -#[derive(Debug, thiserror::Error)] -#[error("nodelist expected to contain at most one entry, but instead contains {0} entries")] -pub struct AtMostOneError(pub usize); - -/// Error produced when expecting exactly one node from a query -#[derive(Debug, thiserror::Error)] -pub enum ExactlyOneError { - /// The query resulted in an empty [`NodeList`] - #[error("nodelist expected to contain one entry, but is empty")] - Empty, - /// The query resulted in a [`NodeList`] containing more than one node - #[error("nodelist expected to contain one entry, but instead contains {0} entries")] - MoreThanOne(usize), -} - -impl ExactlyOneError { - /// Check that it is the `Empty` variant - pub fn is_empty(&self) -> bool { - matches!(self, Self::Empty) - } - - /// Check that it is the `MoreThanOne` variant - pub fn is_more_than_one(&self) -> bool { - self.as_more_than_one().is_some() - } - - /// Extract the number of nodes, if it was more than one, or `None` otherwise - pub fn as_more_than_one(&self) -> Option { - match self { - ExactlyOneError::Empty => None, - ExactlyOneError::MoreThanOne(u) => Some(*u), - } - } -} - impl<'a> From> for NodeList<'a> { fn from(nodes: Vec<&'a Value>) -> Self { Self(nodes) @@ -305,16 +269,27 @@ impl<'a> LocatedNodeList<'a> { self.0.iter() } + pub fn locations(&self) -> Locations<'_> { + Locations { inner: self.iter() } + } + + pub fn nodes(&self) -> Nodes<'_> { + Nodes { inner: self.iter() } + } + pub fn dedup(mut self) -> Self { + self.dedup_in_place(); + self + } + + pub fn dedup_in_place(&mut self) { + // This unwrap should be safe, since the paths corresponding to + // a query against a Value will always be ordered. + // + // TODO - the below From impl may allow someone to violate this self.0 - // This unwrap is safe, since the paths corresponding to - // a query against a Value will always be ordered. - // - // TODO - the below From impl may in theory allow someone - // to violate this, so may need to remedy that... .sort_unstable_by(|a, b| a.0.partial_cmp(&b.0).unwrap()); self.0.dedup(); - self } } @@ -334,6 +309,94 @@ impl<'a> IntoIterator for LocatedNodeList<'a> { } } +pub struct Locations<'a> { + inner: Iter<'a, (NormalizedPath<'a>, &'a Value)>, +} + +impl<'a> Iterator for Locations<'a> { + type Item = &'a NormalizedPath<'a>; + + fn next(&mut self) -> Option { + self.inner.next().map(|(np, _)| np) + } +} + +impl<'a> DoubleEndedIterator for Locations<'a> { + fn next_back(&mut self) -> Option { + self.inner.next_back().map(|(np, _)| np) + } +} + +impl<'a> ExactSizeIterator for Locations<'a> { + fn len(&self) -> usize { + self.inner.len() + } +} + +impl<'a> FusedIterator for Locations<'a> {} + +pub struct Nodes<'a> { + inner: Iter<'a, (NormalizedPath<'a>, &'a Value)>, +} + +impl<'a> Iterator for Nodes<'a> { + type Item = &'a Value; + + fn next(&mut self) -> Option { + self.inner.next().map(|(_, n)| *n) + } +} + +impl<'a> DoubleEndedIterator for Nodes<'a> { + fn next_back(&mut self) -> Option { + self.inner.next_back().map(|(_, n)| *n) + } +} + +impl<'a> ExactSizeIterator for Nodes<'a> { + fn len(&self) -> usize { + self.inner.len() + } +} + +impl<'a> FusedIterator for Nodes<'a> {} + +/// Error produced when expecting no more than one node from a query +#[derive(Debug, thiserror::Error)] +#[error("nodelist expected to contain at most one entry, but instead contains {0} entries")] +pub struct AtMostOneError(pub usize); + +/// Error produced when expecting exactly one node from a query +#[derive(Debug, thiserror::Error)] +pub enum ExactlyOneError { + /// The query resulted in an empty [`NodeList`] + #[error("nodelist expected to contain one entry, but is empty")] + Empty, + /// The query resulted in a [`NodeList`] containing more than one node + #[error("nodelist expected to contain one entry, but instead contains {0} entries")] + MoreThanOne(usize), +} + +impl ExactlyOneError { + /// Check that it is the `Empty` variant + pub fn is_empty(&self) -> bool { + matches!(self, Self::Empty) + } + + /// Check that it is the `MoreThanOne` variant + pub fn is_more_than_one(&self) -> bool { + self.as_more_than_one().is_some() + } + + /// Extract the number of nodes, if it was more than one, or `None` otherwise + pub fn as_more_than_one(&self) -> Option { + match self { + ExactlyOneError::Empty => None, + ExactlyOneError::MoreThanOne(u) => Some(*u), + } + } +} + #[cfg(test)] mod tests { use crate::node::NodeList; From 53135af185aeb936c1e7193b6bec4da3ebc58c0f Mon Sep 17 00:00:00 2001 From: Trevor Hilton Date: Sun, 28 Jan 2024 22:52:39 -0500 Subject: [PATCH 05/21] feat: add LocatedNode type The LocatedNode type represents a node, i.e., &Value, as well as its location within the original Value that was queried. Defining a dedicated type for this was done to replace the use of the (NormalizedPath, &Value) every- where, and allows for addition of convenience methods. This also ensures controlled construction of LocatedNodeList, which ensures that their deduplication can be done safely, i.e., the unwrap is safe. This commit also started adding in the missing docs for newly created items in this branch. --- serde_json_path/src/lib.rs | 6 +- serde_json_path/src/path.rs | 31 ++++++-- serde_json_path_core/src/lib.rs | 1 + serde_json_path_core/src/node.rs | 78 ++++++++++++++----- serde_json_path_core/src/{spec => }/path.rs | 0 serde_json_path_core/src/spec/mod.rs | 1 - serde_json_path_core/src/spec/query.rs | 26 ++++--- serde_json_path_core/src/spec/segment.rs | 25 ++++-- .../src/spec/selector/filter.rs | 23 ++++-- .../src/spec/selector/index.rs | 8 +- serde_json_path_core/src/spec/selector/mod.rs | 23 +++--- .../src/spec/selector/name.rs | 10 +-- .../src/spec/selector/slice.rs | 18 +++-- 13 files changed, 170 insertions(+), 80 deletions(-) rename serde_json_path_core/src/{spec => }/path.rs (100%) diff --git a/serde_json_path/src/lib.rs b/serde_json_path/src/lib.rs index e44eb40..c820cf2 100644 --- a/serde_json_path/src/lib.rs +++ b/serde_json_path/src/lib.rs @@ -329,7 +329,11 @@ pub use ext::JsonPathExt; #[doc(inline)] pub use path::JsonPath; #[doc(inline)] -pub use serde_json_path_core::node::{AtMostOneError, ExactlyOneError, NodeList}; +pub use serde_json_path_core::node::{ + AtMostOneError, ExactlyOneError, LocatedNode, LocatedNodeList, NodeList, +}; +#[doc(inline)] +pub use serde_json_path_core::path::NormalizedPath; pub use serde_json_path_core::spec::functions; diff --git a/serde_json_path/src/path.rs b/serde_json_path/src/path.rs index bb3832d..041b944 100644 --- a/serde_json_path/src/path.rs +++ b/serde_json_path/src/path.rs @@ -4,10 +4,7 @@ use serde::{de::Visitor, Deserialize, Serialize}; use serde_json::Value; use serde_json_path_core::{ node::{LocatedNodeList, NodeList}, - spec::{ - path::NormalizedPath, - query::{Query, Queryable}, - }, + spec::query::{Query, Queryable}, }; use crate::{parser::parse_query_main, ParseError}; @@ -68,8 +65,8 @@ impl JsonPath { /// # use serde_json::json; /// # use serde_json_path::JsonPath; /// # fn main() -> Result<(), serde_json_path::ParseError> { - /// let path = JsonPath::parse("$.foo[::2]")?; /// let value = json!({"foo": [1, 2, 3, 4]}); + /// let path = JsonPath::parse("$.foo[::2]")?; /// let nodes = path.query(&value); /// assert_eq!(nodes.all(), vec![1, 3]); /// # Ok(()) @@ -79,6 +76,26 @@ impl JsonPath { self.0.query(value, value).into() } + /// Query a [`serde_json::Value`] using this [`JsonPath`] to produce a [`LocatedNodeList`] + /// + /// # Example + /// ```rust + /// # use serde_json::{json, Value}; + /// # use serde_json_path::{JsonPath,NormalizedPath}; + /// # fn main() -> Result<(), serde_json_path::ParseError> { + /// let value = json!({"foo": [1, 2, 3, 4]}); + /// let path = JsonPath::parse("$.foo[2:]")?; + /// let query = path.query_located(&value); + /// let nodes: Vec<&Value> = query.nodes().collect(); + /// assert_eq!(nodes, vec![3, 4]); + /// let locs: Vec = query + /// .locations() + /// .map(|loc| loc.to_string()) + /// .collect(); + /// assert_eq!(locs, ["$['foo'][2]", "$['foo'][3]"]); + /// # Ok(()) + /// # } + /// ``` pub fn query_located<'b>(&self, value: &'b Value) -> LocatedNodeList<'b> { self.0 .query_located(value, value, Default::default()) @@ -170,8 +187,8 @@ mod tests { }}); let p = JsonPath::parse("$.foo.bar.*").unwrap(); let r = p.query_located(&j); - for (np, _) in r { - println!("{pointer}", pointer = np.as_json_pointer()); + for ln in r { + println!("{pointer}", pointer = ln.location().as_json_pointer()); } } } diff --git a/serde_json_path_core/src/lib.rs b/serde_json_path_core/src/lib.rs index 0ecdd65..52cc683 100644 --- a/serde_json_path_core/src/lib.rs +++ b/serde_json_path_core/src/lib.rs @@ -41,4 +41,5 @@ #![forbid(unsafe_code)] pub mod node; +pub mod path; pub mod spec; diff --git a/serde_json_path_core/src/node.rs b/serde_json_path_core/src/node.rs index 5ce4fb3..e5af808 100644 --- a/serde_json_path_core/src/node.rs +++ b/serde_json_path_core/src/node.rs @@ -4,7 +4,7 @@ use std::{iter::FusedIterator, slice::Iter}; use serde::Serialize; use serde_json::Value; -use crate::spec::path::NormalizedPath; +use crate::path::NormalizedPath; /// A list of nodes resulting from a JSONPath query /// @@ -227,13 +227,51 @@ impl<'a> IntoIterator for NodeList<'a> { } } +#[derive(Debug, Eq, PartialEq, Serialize, Clone)] +pub struct LocatedNode<'a> { + pub(crate) loc: NormalizedPath<'a>, + pub(crate) node: &'a Value, +} + +impl<'a> LocatedNode<'a> { + pub fn location(&self) -> &NormalizedPath<'a> { + &self.loc + } + + pub fn node(&self) -> &'a Value { + self.node + } +} + +/// A list of nodes resulting from a JSONPath query, along with their locations +/// +/// As with [`NodeList`], each node is a borrowed reference to the node in the original +/// [`serde_json::Value`] that was queried. Each node in the list is paired with its location +/// represented by a [`NormalizedPath`]. #[derive(Debug, Default, Eq, PartialEq, Serialize, Clone)] -pub struct LocatedNodeList<'a>(Vec<(NormalizedPath<'a>, &'a Value)>); +pub struct LocatedNodeList<'a>(Vec>); impl<'a> LocatedNodeList<'a> { - pub fn at_most_one( - mut self, - ) -> Result, &'a Value)>, AtMostOneError> { + /// Extract _at most_ one entry from a [`LocatedNodeList`] + /// + /// This is intended for queries that are expected to optionally yield a single node. + /// + /// # Usage + /// ```rust + /// # use serde_json::json; + /// # use serde_json_path::JsonPath; + /// # use serde_json_path::AtMostOneError; + /// # fn main() -> Result<(), serde_json_path::ParseError> { + /// let value = json!({"foo": ["bar", "baz"]}); + /// # { + /// let path = JsonPath::parse("$.foo[0]")?; + /// let node = path.query_located(&value).at_most_one().unwrap(); + /// assert_eq!("$['foo'][0]", node.unwrap().location().to_string()); + /// # } + /// # Ok(()) + /// # } + /// ``` + pub fn at_most_one(mut self) -> Result>, AtMostOneError> { if self.0.is_empty() { Ok(None) } else if self.0.len() > 1 { @@ -243,7 +281,7 @@ impl<'a> LocatedNodeList<'a> { } } - pub fn exactly_one(mut self) -> Result<(NormalizedPath<'a>, &'a Value), ExactlyOneError> { + pub fn exactly_one(mut self) -> Result, ExactlyOneError> { if self.0.is_empty() { Err(ExactlyOneError::Empty) } else if self.0.len() > 1 { @@ -253,7 +291,7 @@ impl<'a> LocatedNodeList<'a> { } } - pub fn all(self) -> Vec<(NormalizedPath<'a>, &'a Value)> { + pub fn all(self) -> Vec> { self.0 } @@ -265,7 +303,7 @@ impl<'a> LocatedNodeList<'a> { self.0.is_empty() } - pub fn iter(&self) -> Iter<'_, (NormalizedPath<'a>, &'a Value)> { + pub fn iter(&self) -> Iter<'_, LocatedNode<'a>> { self.0.iter() } @@ -285,22 +323,20 @@ impl<'a> LocatedNodeList<'a> { pub fn dedup_in_place(&mut self) { // This unwrap should be safe, since the paths corresponding to // a query against a Value will always be ordered. - // - // TODO - the below From impl may allow someone to violate this self.0 - .sort_unstable_by(|a, b| a.0.partial_cmp(&b.0).unwrap()); + .sort_unstable_by(|a, b| a.loc.partial_cmp(&b.loc).unwrap()); self.0.dedup(); } } -impl<'a> From, &'a Value)>> for LocatedNodeList<'a> { - fn from(v: Vec<(NormalizedPath<'a>, &'a Value)>) -> Self { +impl<'a> From>> for LocatedNodeList<'a> { + fn from(v: Vec>) -> Self { Self(v) } } impl<'a> IntoIterator for LocatedNodeList<'a> { - type Item = (NormalizedPath<'a>, &'a Value); + type Item = LocatedNode<'a>; type IntoIter = std::vec::IntoIter; @@ -309,21 +345,22 @@ impl<'a> IntoIterator for LocatedNodeList<'a> { } } +#[derive(Debug)] pub struct Locations<'a> { - inner: Iter<'a, (NormalizedPath<'a>, &'a Value)>, + inner: Iter<'a, LocatedNode<'a>>, } impl<'a> Iterator for Locations<'a> { type Item = &'a NormalizedPath<'a>; fn next(&mut self) -> Option { - self.inner.next().map(|(np, _)| np) + self.inner.next().map(|l| l.location()) } } impl<'a> DoubleEndedIterator for Locations<'a> { fn next_back(&mut self) -> Option { - self.inner.next_back().map(|(np, _)| np) + self.inner.next_back().map(|l| l.location()) } } @@ -335,21 +372,22 @@ impl<'a> ExactSizeIterator for Locations<'a> { impl<'a> FusedIterator for Locations<'a> {} +#[derive(Debug)] pub struct Nodes<'a> { - inner: Iter<'a, (NormalizedPath<'a>, &'a Value)>, + inner: Iter<'a, LocatedNode<'a>>, } impl<'a> Iterator for Nodes<'a> { type Item = &'a Value; fn next(&mut self) -> Option { - self.inner.next().map(|(_, n)| *n) + self.inner.next().map(|l| l.node()) } } impl<'a> DoubleEndedIterator for Nodes<'a> { fn next_back(&mut self) -> Option { - self.inner.next_back().map(|(_, n)| *n) + self.inner.next_back().map(|l| l.node()) } } diff --git a/serde_json_path_core/src/spec/path.rs b/serde_json_path_core/src/path.rs similarity index 100% rename from serde_json_path_core/src/spec/path.rs rename to serde_json_path_core/src/path.rs diff --git a/serde_json_path_core/src/spec/mod.rs b/serde_json_path_core/src/spec/mod.rs index 2dc84e1..585baf5 100644 --- a/serde_json_path_core/src/spec/mod.rs +++ b/serde_json_path_core/src/spec/mod.rs @@ -1,6 +1,5 @@ //! Types representing the IETF JSONPath Standard pub mod functions; -pub mod path; pub mod query; pub mod segment; pub mod selector; diff --git a/serde_json_path_core/src/spec/query.rs b/serde_json_path_core/src/spec/query.rs index cd4a8e3..6aa9ff1 100644 --- a/serde_json_path_core/src/spec/query.rs +++ b/serde_json_path_core/src/spec/query.rs @@ -1,7 +1,9 @@ //! Types representing queries in JSONPath use serde_json::Value; -use super::{path::NormalizedPath, segment::QuerySegment}; +use crate::{node::LocatedNode, path::NormalizedPath}; + +use super::segment::QuerySegment; mod sealed { use crate::spec::{ @@ -38,7 +40,7 @@ pub trait Queryable: sealed::Sealed { current: &'b Value, root: &'b Value, parent: NormalizedPath<'b>, - ) -> Vec<(NormalizedPath<'b>, &'b Value)>; + ) -> Vec>; } /// Represents a JSONPath expression @@ -108,16 +110,22 @@ impl Queryable for Query { &self, current: &'b Value, root: &'b Value, - _parent: NormalizedPath<'b>, - ) -> Vec<(NormalizedPath<'b>, &'b Value)> { - let mut result: Vec<(NormalizedPath<'b>, &Value)> = match self.kind { - QueryKind::Root => vec![(Default::default(), root)], - QueryKind::Current => vec![(Default::default(), current)], + parent: NormalizedPath<'b>, + ) -> Vec> { + let mut result: Vec> = match self.kind { + QueryKind::Root => vec![LocatedNode { + loc: Default::default(), + node: root, + }], + QueryKind::Current => vec![LocatedNode { + loc: parent, + node: current, + }], }; for s in &self.segments { let mut r = vec![]; - for (ref np, v) in result { - r.append(&mut s.query_located(v, root, np.clone())); + for LocatedNode { loc, node } in result { + r.append(&mut s.query_located(node, root, loc.clone())); } result = r; } diff --git a/serde_json_path_core/src/spec/segment.rs b/serde_json_path_core/src/spec/segment.rs index 8c49a9f..beb220b 100644 --- a/serde_json_path_core/src/spec/segment.rs +++ b/serde_json_path_core/src/spec/segment.rs @@ -1,7 +1,9 @@ //! Types representing segments in JSONPath use serde_json::Value; -use super::{path::NormalizedPath, query::Queryable, selector::Selector}; +use crate::{node::LocatedNode, path::NormalizedPath}; + +use super::{query::Queryable, selector::Selector}; /// A segment of a JSONPath query #[derive(Debug, PartialEq, Eq, Clone)] @@ -61,7 +63,7 @@ impl Queryable for QuerySegment { current: &'b Value, root: &'b Value, parent: NormalizedPath<'b>, - ) -> Vec<(NormalizedPath<'b>, &'b Value)> { + ) -> Vec> { if matches!(self.kind, QuerySegmentKind::Descendant) { let mut result = self.segment.query_located(current, root, parent.clone()); result.append(&mut descend_paths(self, current, root, parent)); @@ -92,7 +94,7 @@ fn descend_paths<'b>( current: &'b Value, root: &'b Value, parent: NormalizedPath<'b>, -) -> Vec<(NormalizedPath<'b>, &'b Value)> { +) -> Vec> { let mut result = Vec::new(); if let Some(list) = current.as_array() { for (i, v) in list.iter().enumerate() { @@ -214,7 +216,7 @@ impl Queryable for Segment { current: &'b Value, root: &'b Value, mut parent: NormalizedPath<'b>, - ) -> Vec<(NormalizedPath<'b>, &'b Value)> { + ) -> Vec> { let mut result = vec![]; match self { Segment::LongHand(selectors) => { @@ -225,17 +227,26 @@ impl Queryable for Segment { Segment::DotName(name) => { if let Some((k, v)) = current.as_object().and_then(|o| o.get_key_value(name)) { parent.push(k); - result.push((parent, v)); + result.push(LocatedNode { + loc: parent, + node: v, + }); } } Segment::Wildcard => { if let Some(list) = current.as_array() { for (i, v) in list.iter().enumerate() { - result.push((parent.clone_and_push(i), v)); + result.push(LocatedNode { + loc: parent.clone_and_push(i), + node: v, + }); } } else if let Some(obj) = current.as_object() { for (k, v) in obj { - result.push((parent.clone_and_push(k), v)); + result.push(LocatedNode { + loc: parent.clone_and_push(k), + node: v, + }); } } } diff --git a/serde_json_path_core/src/spec/selector/filter.rs b/serde_json_path_core/src/spec/selector/filter.rs index 4c8555e..d63e8a2 100644 --- a/serde_json_path_core/src/spec/selector/filter.rs +++ b/serde_json_path_core/src/spec/selector/filter.rs @@ -1,11 +1,14 @@ //! Types representing filter selectors in JSONPath use serde_json::{Number, Value}; -use crate::spec::{ - functions::{FunctionExpr, JsonPathValue, Validated}, +use crate::{ + node::LocatedNode, path::NormalizedPath, - query::{Query, QueryKind, Queryable}, - segment::{QuerySegment, Segment}, + spec::{ + functions::{FunctionExpr, JsonPathValue, Validated}, + query::{Query, QueryKind, Queryable}, + segment::{QuerySegment, Segment}, + }, }; use super::{index::Index, name::Name, Selector}; @@ -76,17 +79,23 @@ impl Queryable for Filter { current: &'b Value, root: &'b Value, parent: NormalizedPath<'b>, - ) -> Vec<(NormalizedPath<'b>, &'b Value)> { + ) -> Vec> { if let Some(list) = current.as_array() { list.iter() .enumerate() .filter(|(_, v)| self.0.test_filter(v, root)) - .map(|(i, v)| (parent.clone_and_push(i), v)) + .map(|(i, v)| LocatedNode { + loc: parent.clone_and_push(i), + node: v, + }) .collect() } else if let Some(obj) = current.as_object() { obj.iter() .filter(|(_, v)| self.0.test_filter(v, root)) - .map(|(k, v)| (parent.clone_and_push(k), v)) + .map(|(k, v)| LocatedNode { + loc: parent.clone_and_push(k), + node: v, + }) .collect() } else { vec![] diff --git a/serde_json_path_core/src/spec/selector/index.rs b/serde_json_path_core/src/spec/selector/index.rs index 98b0036..66e3fc7 100644 --- a/serde_json_path_core/src/spec/selector/index.rs +++ b/serde_json_path_core/src/spec/selector/index.rs @@ -1,7 +1,7 @@ //! Index selectors in JSONPath use serde_json::Value; -use crate::spec::{path::NormalizedPath, query::Queryable}; +use crate::{node::LocatedNode, path::NormalizedPath, spec::query::Queryable}; /// For selecting array elements by their index /// @@ -44,8 +44,8 @@ impl Queryable for Index { current: &'b Value, _root: &'b Value, mut parent: NormalizedPath<'b>, - ) -> Vec<(NormalizedPath<'b>, &'b Value)> { - if let Some((index, value)) = current.as_array().and_then(|list| { + ) -> Vec> { + if let Some((index, node)) = current.as_array().and_then(|list| { if self.0 < 0 { self.0 .checked_abs() @@ -59,7 +59,7 @@ impl Queryable for Index { } }) { parent.push(index); - vec![(parent, value)] + vec![LocatedNode { loc: parent, node }] } else { vec![] } diff --git a/serde_json_path_core/src/spec/selector/mod.rs b/serde_json_path_core/src/spec/selector/mod.rs index 596a47b..2ec51de 100644 --- a/serde_json_path_core/src/spec/selector/mod.rs +++ b/serde_json_path_core/src/spec/selector/mod.rs @@ -6,12 +6,11 @@ pub mod slice; use serde_json::Value; +use crate::{node::LocatedNode, path::NormalizedPath}; + use self::{filter::Filter, index::Index, name::Name, slice::Slice}; -use super::{ - path::{NormalizedPath, PathElement}, - query::Queryable, -}; +use super::query::Queryable; /// A JSONPath selector #[derive(Debug, PartialEq, Eq, Clone)] @@ -79,25 +78,23 @@ impl Queryable for Selector { current: &'b Value, root: &'b Value, parent: NormalizedPath<'b>, - ) -> Vec<(NormalizedPath<'b>, &'b Value)> { + ) -> Vec> { match self { Selector::Name(name) => name.query_located(current, root, parent), Selector::Wildcard => { if let Some(list) = current.as_array() { list.iter() .enumerate() - .map(|(i, v)| { - let mut new_path = parent.clone(); - new_path.push(PathElement::from(i)); - (new_path, v) + .map(|(i, node)| LocatedNode { + loc: parent.clone_and_push(i), + node, }) .collect() } else if let Some(obj) = current.as_object() { obj.iter() - .map(|(k, v)| { - let mut new_path = parent.clone(); - new_path.push(PathElement::from(k)); - (new_path, v) + .map(|(k, node)| LocatedNode { + loc: parent.clone_and_push(k), + node, }) .collect() } else { diff --git a/serde_json_path_core/src/spec/selector/name.rs b/serde_json_path_core/src/spec/selector/name.rs index 1837d72..758ef66 100644 --- a/serde_json_path_core/src/spec/selector/name.rs +++ b/serde_json_path_core/src/spec/selector/name.rs @@ -1,7 +1,7 @@ //! Name selector for selecting object keys in JSONPath use serde_json::Value; -use crate::spec::{path::NormalizedPath, query::Queryable}; +use crate::{node::LocatedNode, path::NormalizedPath, spec::query::Queryable}; /// Select a single JSON object key #[derive(Debug, PartialEq, Eq, Clone)] @@ -35,10 +35,10 @@ impl Queryable for Name { current: &'b Value, _root: &'b Value, mut parent: NormalizedPath<'b>, - ) -> Vec<(NormalizedPath<'b>, &'b Value)> { - if let Some((s, v)) = current.as_object().and_then(|o| o.get_key_value(&self.0)) { - parent.push(s); - vec![(parent, v)] + ) -> Vec> { + if let Some((name, node)) = current.as_object().and_then(|o| o.get_key_value(&self.0)) { + parent.push(name); + vec![LocatedNode { loc: parent, node }] } else { vec![] } diff --git a/serde_json_path_core/src/spec/selector/slice.rs b/serde_json_path_core/src/spec/selector/slice.rs index f7c5fc8..ddb779f 100644 --- a/serde_json_path_core/src/spec/selector/slice.rs +++ b/serde_json_path_core/src/spec/selector/slice.rs @@ -1,7 +1,7 @@ //! Slice selectors for selecting array slices in JSONPath use serde_json::Value; -use crate::spec::{path::NormalizedPath, query::Queryable}; +use crate::{node::LocatedNode, path::NormalizedPath, spec::query::Queryable}; /// A slice selector #[derive(Debug, PartialEq, Eq, Default, Clone, Copy)] @@ -133,7 +133,7 @@ impl Queryable for Slice { current: &'b Value, _root: &'b Value, parent: NormalizedPath<'b>, - ) -> Vec<(NormalizedPath<'b>, &'b Value)> { + ) -> Vec> { if let Some(list) = current.as_array() { let mut result = Vec::new(); let step = self.step.unwrap_or(1); @@ -147,11 +147,14 @@ impl Queryable for Slice { let (lower, upper) = self.bounds_on_forward_slice(len); let mut i = lower; while i < upper { - if let Some((i, v)) = usize::try_from(i) + if let Some((i, node)) = usize::try_from(i) .ok() .and_then(|i| list.get(i).map(|v| (i, v))) { - result.push((parent.clone_and_push(i), v)); + result.push(LocatedNode { + loc: parent.clone_and_push(i), + node, + }); } i += step; } @@ -161,11 +164,14 @@ impl Queryable for Slice { }; let mut i = upper; while lower < i { - if let Some((i, v)) = usize::try_from(i) + if let Some((i, node)) = usize::try_from(i) .ok() .and_then(|i| list.get(i).map(|v| (i, v))) { - result.push((parent.clone_and_push(i), v)); + result.push(LocatedNode { + loc: parent.clone_and_push(i), + node, + }); } i += step; } From 13a80ece9c68fad7fd86fcf9f456237bce0510b6 Mon Sep 17 00:00:00 2001 From: Trevor Hilton Date: Sun, 28 Jan 2024 23:03:48 -0500 Subject: [PATCH 06/21] docs: update JsonPath::query_located docs --- serde_json_path/src/path.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/serde_json_path/src/path.rs b/serde_json_path/src/path.rs index 041b944..81a6f8a 100644 --- a/serde_json_path/src/path.rs +++ b/serde_json_path/src/path.rs @@ -83,16 +83,16 @@ impl JsonPath { /// # use serde_json::{json, Value}; /// # use serde_json_path::{JsonPath,NormalizedPath}; /// # fn main() -> Result<(), serde_json_path::ParseError> { - /// let value = json!({"foo": [1, 2, 3, 4]}); - /// let path = JsonPath::parse("$.foo[2:]")?; + /// let value = json!({"foo": {"bar": 1, "baz": 2}}); + /// let path = JsonPath::parse("$.foo.*")?; /// let query = path.query_located(&value); /// let nodes: Vec<&Value> = query.nodes().collect(); - /// assert_eq!(nodes, vec![3, 4]); + /// assert_eq!(nodes, vec![1, 2]); /// let locs: Vec = query /// .locations() /// .map(|loc| loc.to_string()) /// .collect(); - /// assert_eq!(locs, ["$['foo'][2]", "$['foo'][3]"]); + /// assert_eq!(locs, ["$['foo']['bar']", "$['foo']['baz']"]); /// # Ok(()) /// # } /// ``` From d6ca84d25c72b5ff3a3b4aee5fb7b67a6fa47ad7 Mon Sep 17 00:00:00 2001 From: Trevor Hilton Date: Mon, 29 Jan 2024 21:51:17 -0500 Subject: [PATCH 07/21] docs: add docs to LocatedNodeList and relatives --- serde_json_path/src/lib.rs | 13 +++- serde_json_path_core/src/node.rs | 130 ++++++++++++++++++++++++++++--- serde_json_path_core/src/path.rs | 3 + 3 files changed, 135 insertions(+), 11 deletions(-) diff --git a/serde_json_path/src/lib.rs b/serde_json_path/src/lib.rs index c820cf2..adc82f3 100644 --- a/serde_json_path/src/lib.rs +++ b/serde_json_path/src/lib.rs @@ -328,9 +328,20 @@ pub use error::ParseError; pub use ext::JsonPathExt; #[doc(inline)] pub use path::JsonPath; +/// A list of nodes resulting from a JSONPath query, along with their locations +/// +/// This is produced by the [`JsonPath::query_located`] method. +/// +/// As with [`NodeList`], each node is a borrowed reference to the node in the original +/// [`serde_json::Value`] that was queried; however, each node in the list is paired with its +/// location represented by a [`NormalizedPath`]. +/// +/// In addition to the locations, [`LocatedNodeList`] provides useful functionality over [`NodeList`] +/// such as de-duplication of query results (see [`dedup`][LocatedNodeList::dedup]). +pub use serde_json_path_core::node::LocatedNodeList; #[doc(inline)] pub use serde_json_path_core::node::{ - AtMostOneError, ExactlyOneError, LocatedNode, LocatedNodeList, NodeList, + AtMostOneError, ExactlyOneError, LocatedNode, Locations, NodeList, Nodes, }; #[doc(inline)] pub use serde_json_path_core::path::NormalizedPath; diff --git a/serde_json_path_core/src/node.rs b/serde_json_path_core/src/node.rs index e5af808..3127772 100644 --- a/serde_json_path_core/src/node.rs +++ b/serde_json_path_core/src/node.rs @@ -82,7 +82,7 @@ impl<'a> NodeList<'a> { } } - /// Extract all nodes yielded by the query. + /// Extract all nodes yielded by the query /// /// This is intended for queries that are expected to yield zero or more nodes. /// @@ -107,7 +107,7 @@ impl<'a> NodeList<'a> { self.0.len() } - /// Check if a [NodeList] is empty + /// Check if a [`NodeList`] is empty pub fn is_empty(&self) -> bool { self.0.is_empty() } @@ -227,6 +227,7 @@ impl<'a> IntoIterator for NodeList<'a> { } } +/// A node within a JSON value, along with its location #[derive(Debug, Eq, PartialEq, Serialize, Clone)] pub struct LocatedNode<'a> { pub(crate) loc: NormalizedPath<'a>, @@ -234,20 +235,19 @@ pub struct LocatedNode<'a> { } impl<'a> LocatedNode<'a> { + /// Get the location of the node as a [`NormalizedPath`] pub fn location(&self) -> &NormalizedPath<'a> { &self.loc } + /// Get the node itself pub fn node(&self) -> &'a Value { self.node } } -/// A list of nodes resulting from a JSONPath query, along with their locations -/// -/// As with [`NodeList`], each node is a borrowed reference to the node in the original -/// [`serde_json::Value`] that was queried. Each node in the list is paired with its location -/// represented by a [`NormalizedPath`]. +// This is documented in the serde_json_path crate, for linking purposes +#[allow(missing_docs)] #[derive(Debug, Default, Eq, PartialEq, Serialize, Clone)] pub struct LocatedNodeList<'a>(Vec>); @@ -261,12 +261,15 @@ impl<'a> LocatedNodeList<'a> { /// # use serde_json::json; /// # use serde_json_path::JsonPath; /// # use serde_json_path::AtMostOneError; - /// # fn main() -> Result<(), serde_json_path::ParseError> { + /// # fn main() -> Result<(), Box> { /// let value = json!({"foo": ["bar", "baz"]}); /// # { /// let path = JsonPath::parse("$.foo[0]")?; - /// let node = path.query_located(&value).at_most_one().unwrap(); - /// assert_eq!("$['foo'][0]", node.unwrap().location().to_string()); + /// let Some(node) = path.query_located(&value).at_most_one()? else { + /// /* ... */ + /// # unreachable!("query should not be empty"); + /// }; + /// assert_eq!("$['foo'][0]", node.location().to_string()); /// # } /// # Ok(()) /// # } @@ -281,6 +284,25 @@ impl<'a> LocatedNodeList<'a> { } } + /// Extract _exactly_ one entry from a [`LocatedNodeList`] + /// + /// This is intended for queries that are expected to yield a single node. + /// + /// # Usage + /// ```rust + /// # use serde_json::json; + /// # use serde_json_path::JsonPath; + /// # use serde_json_path::ExactlyOneError; + /// # fn main() -> Result<(), Box> { + /// let value = json!({"foo": ["bar", "baz"]}); + /// # { + /// let path = JsonPath::parse("$.foo[? @ == 'bar']")?; + /// let node = path.query_located(&value).exactly_one()?; + /// assert_eq!("$['foo'][0]", node.location().to_string()); + /// # } + /// # Ok(()) + /// # } + /// ``` pub fn exactly_one(mut self) -> Result, ExactlyOneError> { if self.0.is_empty() { Err(ExactlyOneError::Empty) @@ -291,35 +313,117 @@ impl<'a> LocatedNodeList<'a> { } } + /// Extract all located nodes yielded by the query + /// + /// This is intended for queries that are expected to yield zero or more nodes. + /// + /// # Usage + /// ```rust + /// # use serde_json::json; + /// # use serde_json_path::JsonPath; + /// # fn main() -> Result<(), serde_json_path::ParseError> { + /// let value = json!({"foo": ["bar", "baz"]}); + /// let path = JsonPath::parse("$.foo.*")?; + /// let nodes = path.query_located(&value).all(); + /// assert_eq!(nodes[0].location().to_string(), "$['foo'][0]"); + /// assert_eq!(nodes[0].node(), "bar"); + /// assert_eq!(nodes[1].location().to_string(), "$['foo'][1]"); + /// assert_eq!(nodes[1].node(), "baz"); + /// # Ok(()) + /// # } + /// ``` pub fn all(self) -> Vec> { self.0 } + /// Get the length of a [`LocatedNodeList`] pub fn len(&self) -> usize { self.0.len() } + /// Check if a [`LocatedNodeList`] is empty pub fn is_empty(&self) -> bool { self.0.is_empty() } + /// Get an iterator over a [`LocatedNodeList`] + /// + /// Note that [`LocatedNodeList`] also implements [`IntoIterator`]. + /// + /// To iterate over just locations, see [`locations`][LocatedNodeList::locations]. To iterate + /// over just nodes, see [`nodes`][LocatedNodeList::nodes]. pub fn iter(&self) -> Iter<'_, LocatedNode<'a>> { self.0.iter() } + /// Get an iterator over the locations of nodes within a [`LocatedNodeList`] + /// + /// # Usage + /// ```rust + /// # use serde_json::json; + /// # use serde_json_path::JsonPath; + /// # fn main() -> Result<(), serde_json_path::ParseError> { + /// let value = json!({"foo": ["bar", "baz"]}); + /// let path = JsonPath::parse("$.foo.*")?; + /// let locations: Vec = path + /// .query_located(&value) + /// .locations() + /// .map(|loc| loc.to_string()) + /// .collect(); + /// assert_eq!(locations, ["$['foo'][0]", "$['foo'][1]"]); + /// # Ok(()) + /// # } + /// ``` pub fn locations(&self) -> Locations<'_> { Locations { inner: self.iter() } } + /// Get an iterator over the nodes within a [`LocatedNodeList`] pub fn nodes(&self) -> Nodes<'_> { Nodes { inner: self.iter() } } + /// Deduplicate a [`LocatedNodeList`] and return the result + /// + /// See also, [`dedup_in_place`][LocatedNodeList::dedup_in_place]. + /// + /// # Usage + /// ```rust + /// # use serde_json::json; + /// # use serde_json_path::JsonPath; + /// # fn main() -> Result<(), serde_json_path::ParseError> { + /// let value = json!({"foo": ["bar", "baz"]}); + /// let path = JsonPath::parse("$.foo[0, 0, 1, 1]")?; + /// let nodes = path.query_located(&value); + /// assert_eq!(4, nodes.len()); + /// let nodes = path.query_located(&value).dedup(); + /// assert_eq!(2, nodes.len()); + /// # Ok(()) + /// # } + /// ``` pub fn dedup(mut self) -> Self { self.dedup_in_place(); self } + /// Deduplicate a [`LocatedNodeList`] _in-place_ + /// + /// See also, [`dedup`][LocatedNodeList::dedup]. + /// + /// # Usage + /// ```rust + /// # use serde_json::json; + /// # use serde_json_path::JsonPath; + /// # fn main() -> Result<(), serde_json_path::ParseError> { + /// let value = json!({"foo": ["bar", "baz"]}); + /// let path = JsonPath::parse("$.foo[0, 0, 1, 1]")?; + /// let mut nodes = path.query_located(&value); + /// assert_eq!(4, nodes.len()); + /// nodes.dedup_in_place(); + /// assert_eq!(2, nodes.len()); + /// # Ok(()) + /// # } + /// ``` pub fn dedup_in_place(&mut self) { // This unwrap should be safe, since the paths corresponding to // a query against a Value will always be ordered. @@ -345,6 +449,9 @@ impl<'a> IntoIterator for LocatedNodeList<'a> { } } +/// An iterator over the locations in a [`LocatedNodeList`] +/// +/// Produced by the [`LocatedNodeList::locations`] method. #[derive(Debug)] pub struct Locations<'a> { inner: Iter<'a, LocatedNode<'a>>, @@ -372,6 +479,9 @@ impl<'a> ExactSizeIterator for Locations<'a> { impl<'a> FusedIterator for Locations<'a> {} +/// An iterator over the nodes in a [`LocatedNodeList`] +/// +/// Produced by the [`LocatedNodeList::nodes`] method. #[derive(Debug)] pub struct Nodes<'a> { inner: Iter<'a, LocatedNode<'a>>, diff --git a/serde_json_path_core/src/path.rs b/serde_json_path_core/src/path.rs index 7ef6f4d..c129892 100644 --- a/serde_json_path_core/src/path.rs +++ b/serde_json_path_core/src/path.rs @@ -1,3 +1,6 @@ +//! Types for representing [Normalized Paths][norm-paths] from the JSONPath specification +//! +//! [norm-paths]: https://www.ietf.org/archive/id/draft-ietf-jsonpath-base-21.html#name-normalized-paths use std::{cmp::Ordering, fmt::Display, slice::Iter}; use serde::Serialize; From 8388907ffa63810960d269f471461beb99d4181f Mon Sep 17 00:00:00 2001 From: Trevor Hilton Date: Tue, 30 Jan 2024 10:23:10 -0500 Subject: [PATCH 08/21] docs: add remaining missing docs --- serde_json_path/src/lib.rs | 11 +++++-- serde_json_path_core/src/node.rs | 12 ++++--- serde_json_path_core/src/path.rs | 43 ++++++++++++++++++++++++-- serde_json_path_core/src/spec/query.rs | 2 ++ 4 files changed, 59 insertions(+), 9 deletions(-) diff --git a/serde_json_path/src/lib.rs b/serde_json_path/src/lib.rs index adc82f3..3a1d8e8 100644 --- a/serde_json_path/src/lib.rs +++ b/serde_json_path/src/lib.rs @@ -334,7 +334,7 @@ pub use path::JsonPath; /// /// As with [`NodeList`], each node is a borrowed reference to the node in the original /// [`serde_json::Value`] that was queried; however, each node in the list is paired with its -/// location represented by a [`NormalizedPath`]. +/// location, which is represented by a [`NormalizedPath`]. /// /// In addition to the locations, [`LocatedNodeList`] provides useful functionality over [`NodeList`] /// such as de-duplication of query results (see [`dedup`][LocatedNodeList::dedup]). @@ -343,8 +343,15 @@ pub use serde_json_path_core::node::LocatedNodeList; pub use serde_json_path_core::node::{ AtMostOneError, ExactlyOneError, LocatedNode, Locations, NodeList, Nodes, }; -#[doc(inline)] +/// Represents a [Normalized Path][norm-path] from the JSONPath specification +/// +/// A [`NormalizedPath`] is used to represent the location of a node within a query result +/// produced by the [`JsonPath::query_located`] method. +/// +/// [norm-path]: https://www.ietf.org/archive/id/draft-ietf-jsonpath-base-21.html#name-normalized-paths pub use serde_json_path_core::path::NormalizedPath; +#[doc(inline)] +pub use serde_json_path_core::path::PathElement; pub use serde_json_path_core::spec::functions; diff --git a/serde_json_path_core/src/node.rs b/serde_json_path_core/src/node.rs index 3127772..e5888b0 100644 --- a/serde_json_path_core/src/node.rs +++ b/serde_json_path_core/src/node.rs @@ -246,8 +246,14 @@ impl<'a> LocatedNode<'a> { } } -// This is documented in the serde_json_path crate, for linking purposes -#[allow(missing_docs)] +/// A list of nodes resulting from a JSONPath query, along with their locations +/// +/// As with [`NodeList`], each node is a borrowed reference to the node in the original +/// [`serde_json::Value`] that was queried; however, each node in the list is paired with its +/// location, which is represented by a [`NormalizedPath`]. +/// +/// In addition to the locations, [`LocatedNodeList`] provides useful functionality over [`NodeList`] +/// such as de-duplication of query results (see [`dedup`][LocatedNodeList::dedup]). #[derive(Debug, Default, Eq, PartialEq, Serialize, Clone)] pub struct LocatedNodeList<'a>(Vec>); @@ -260,7 +266,6 @@ impl<'a> LocatedNodeList<'a> { /// ```rust /// # use serde_json::json; /// # use serde_json_path::JsonPath; - /// # use serde_json_path::AtMostOneError; /// # fn main() -> Result<(), Box> { /// let value = json!({"foo": ["bar", "baz"]}); /// # { @@ -292,7 +297,6 @@ impl<'a> LocatedNodeList<'a> { /// ```rust /// # use serde_json::json; /// # use serde_json_path::JsonPath; - /// # use serde_json_path::ExactlyOneError; /// # fn main() -> Result<(), Box> { /// let value = json!({"foo": ["bar", "baz"]}); /// # { diff --git a/serde_json_path_core/src/path.rs b/serde_json_path_core/src/path.rs index c129892..2781536 100644 --- a/serde_json_path_core/src/path.rs +++ b/serde_json_path_core/src/path.rs @@ -5,6 +5,8 @@ use std::{cmp::Ordering, fmt::Display, slice::Iter}; use serde::Serialize; +// Documented in the serde_json_path crate, for linking purposes +#[allow(missing_docs)] #[derive(Debug, Default, Eq, PartialEq, Clone, PartialOrd)] pub struct NormalizedPath<'a>(Vec>); @@ -19,25 +21,57 @@ impl<'a> NormalizedPath<'a> { new_path } + /// Get the [`NormalizedPath`] as a [JSON Pointer][json-pointer] string + /// + /// This can be used with the [`serde_json::Value::pointer`] or + /// [`serde_json::Value::pointer_mut`] methods. + /// + /// # Example + /// ```rust + /// # use serde_json::json; + /// # use serde_json_path::JsonPath; + /// # fn main() -> Result<(), Box> { + /// let mut value = json!({"foo": ["bar", "baz"]}); + /// # { + /// let path = JsonPath::parse("$.foo[? @ == 'bar']")?; + /// let pointer= path + /// .query_located(&value) + /// .exactly_one()? + /// .location() + /// .as_json_pointer(); + /// *value.pointer_mut(&pointer).unwrap() = "bop".into(); + /// assert_eq!(value, json!({"foo": ["bop", "baz"]})); + /// # } + /// # Ok(()) + /// # } + /// ``` + /// + /// [json-pointer]: https://datatracker.ietf.org/doc/html/rfc6901 pub fn as_json_pointer(&self) -> String { self.0 .iter() .map(PathElement::as_json_pointer) .fold(String::from(""), |mut acc, s| { acc.push('/'); - acc.push_str(&s.replace('~', "~0").replace('/', "~1")); + acc.push_str(&s); acc }) } + /// Check if the [`NormalizedPath`] is empty + /// + /// This would also represent the normalized path of the root node in a JSON object, i.e., + /// `$`. pub fn is_empty(&self) -> bool { self.0.is_empty() } + /// Get the length of the [`NormalizedPath`] pub fn len(&self) -> usize { self.0.len() } + /// Get an iterator over the [`PathElement`]s of the [`NormalizedPath`] pub fn iter(&self) -> Iter<'_, PathElement<'a>> { self.0.iter() } @@ -65,17 +99,20 @@ impl<'a> Serialize for NormalizedPath<'a> { } } +/// An element within a [`NormalizedPath`] #[derive(Debug, Eq, PartialEq, Clone)] pub enum PathElement<'a> { + /// A key within a JSON object Name(&'a str), + /// An index of a JSON Array Index(usize), } impl<'a> PathElement<'a> { fn as_json_pointer(&self) -> String { match self { - PathElement::Name(ref s) => format!("{s}"), - PathElement::Index(i) => format!("{i}"), + PathElement::Name(s) => s.replace('~', "~0").replace('/', "~1"), + PathElement::Index(i) => i.to_string(), } } } diff --git a/serde_json_path_core/src/spec/query.rs b/serde_json_path_core/src/spec/query.rs index 6aa9ff1..2394a1a 100644 --- a/serde_json_path_core/src/spec/query.rs +++ b/serde_json_path_core/src/spec/query.rs @@ -35,6 +35,8 @@ mod sealed { pub trait Queryable: sealed::Sealed { /// Query `self` using a current node, and the root node fn query<'b>(&self, current: &'b Value, root: &'b Value) -> Vec<&'b Value>; + /// Query `self` using a current node, the root node, and the normalized path of the current + /// node's parent fn query_located<'b>( &self, current: &'b Value, From 6a2eb079a7de8ca9b0f7acbd84d6a5bdf3be8c51 Mon Sep 17 00:00:00 2001 From: Trevor Hilton Date: Tue, 30 Jan 2024 11:08:44 -0500 Subject: [PATCH 09/21] docs: start on top-level docs and add docs to Display for NormalizedPath --- serde_json_path/src/lib.rs | 10 +++++++--- serde_json_path_core/src/node.rs | 11 +++++++++++ serde_json_path_core/src/path.rs | 19 +++++++++++++++++-- 3 files changed, 35 insertions(+), 5 deletions(-) diff --git a/serde_json_path/src/lib.rs b/serde_json_path/src/lib.rs index 3a1d8e8..43eebb6 100644 --- a/serde_json_path/src/lib.rs +++ b/serde_json_path/src/lib.rs @@ -14,12 +14,16 @@ //! //! * The [`JsonPath`] struct, which represents a parsed JSONPath query. //! * The [`NodeList`] struct, which represents the result of a JSONPath query performed on a -//! [`serde_json::Value`]. +//! [`serde_json::Value`] using the [`JsonPath::query`] method. //! -//! In addition, the [`JsonPathExt`] trait is provided, which extends the [`serde_json::Value`] +//! In addition to these, there is also the [`LocatedNodeList`], produced by the [`JsonPath::query_located`] +//! method, which, in addition to the nodes themselves, provides the location of each node in a query result +//! as a [`NormalizedPath`]. +//! +//! The [`JsonPathExt`] trait is also provided, which extends the [`serde_json::Value`] //! type with the [`json_path`][JsonPathExt::json_path] method for performing JSONPath queries. //! -//! Finally, the [`#[function]`][function] attribute macro allows you to extend your JSONPath +//! Finally, the [`#[function]`][function] attribute macro can be used to extend JSONPath //! queries to use custom functions. //! //! # Usage diff --git a/serde_json_path_core/src/node.rs b/serde_json_path_core/src/node.rs index e5888b0..6e1795a 100644 --- a/serde_json_path_core/src/node.rs +++ b/serde_json_path_core/src/node.rs @@ -240,12 +240,23 @@ impl<'a> LocatedNode<'a> { &self.loc } + /// Take the location of the node as a [`NormalizedPath`] + pub fn to_location(self) -> NormalizedPath<'a> { + self.loc + } + /// Get the node itself pub fn node(&self) -> &'a Value { self.node } } +impl<'a> From> for NormalizedPath<'a> { + fn from(node: LocatedNode<'a>) -> Self { + node.to_location() + } +} + /// A list of nodes resulting from a JSONPath query, along with their locations /// /// As with [`NodeList`], each node is a borrowed reference to the node in the original diff --git a/serde_json_path_core/src/path.rs b/serde_json_path_core/src/path.rs index 2781536..7e1a4a0 100644 --- a/serde_json_path_core/src/path.rs +++ b/serde_json_path_core/src/path.rs @@ -32,7 +32,6 @@ impl<'a> NormalizedPath<'a> { /// # use serde_json_path::JsonPath; /// # fn main() -> Result<(), Box> { /// let mut value = json!({"foo": ["bar", "baz"]}); - /// # { /// let path = JsonPath::parse("$.foo[? @ == 'bar']")?; /// let pointer= path /// .query_located(&value) @@ -41,7 +40,6 @@ impl<'a> NormalizedPath<'a> { /// .as_json_pointer(); /// *value.pointer_mut(&pointer).unwrap() = "bop".into(); /// assert_eq!(value, json!({"foo": ["bop", "baz"]})); - /// # } /// # Ok(()) /// # } /// ``` @@ -78,6 +76,23 @@ impl<'a> NormalizedPath<'a> { } impl<'a> Display for NormalizedPath<'a> { + /// Format the [`NormalizedPath`] as a JSONPath string using the canonical bracket notation + /// as per the [JSONPath Specification][norm-paths] + /// + /// # Example + /// ```rust + /// # use serde_json::json; + /// # use serde_json_path::JsonPath; + /// # fn main() -> Result<(), Box> { + /// let value = json!({"foo": ["bar", "baz"]}); + /// let path = JsonPath::parse("$.foo[0]")?; + /// let location = path.query_located(&value).exactly_one()?.to_location(); + /// assert_eq!(location.to_string(), "$['foo'][0]"); + /// # Ok(()) + /// # } + /// ``` + /// + /// [norm-paths]: https://www.ietf.org/archive/id/draft-ietf-jsonpath-base-21.html#name-normalized-paths fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "$")?; for elem in &self.0 { From 39b1a6a7d7b8a4eb44a96a816265d56a699abedf Mon Sep 17 00:00:00 2001 From: Trevor Hilton Date: Tue, 30 Jan 2024 21:35:13 -0500 Subject: [PATCH 10/21] docs: finish top-level docs --- serde_json_path/src/lib.rs | 50 +++++++++++++++++++---- serde_json_path_core/src/node.rs | 70 ++++++++++++++++++++++++++++---- 2 files changed, 103 insertions(+), 17 deletions(-) diff --git a/serde_json_path/src/lib.rs b/serde_json_path/src/lib.rs index 43eebb6..9170c3c 100644 --- a/serde_json_path/src/lib.rs +++ b/serde_json_path/src/lib.rs @@ -10,17 +10,16 @@ //! //! # Features //! -//! This crate provides two key abstractions: +//! This crate provides three key abstractions: //! //! * The [`JsonPath`] struct, which represents a parsed JSONPath query. //! * The [`NodeList`] struct, which represents the result of a JSONPath query performed on a //! [`serde_json::Value`] using the [`JsonPath::query`] method. +//! * The [`LocatedNodeList`] struct, which is similar to [`NodeList`], but includes the location +//! of each node in the query string as a [`NormalizedPath`], and is produced by the +//! [`JsonPath::query_located`] method. //! -//! In addition to these, there is also the [`LocatedNodeList`], produced by the [`JsonPath::query_located`] -//! method, which, in addition to the nodes themselves, provides the location of each node in a query result -//! as a [`NormalizedPath`]. -//! -//! The [`JsonPathExt`] trait is also provided, which extends the [`serde_json::Value`] +//! In addition, the [`JsonPathExt`] trait is provided, which extends the [`serde_json::Value`] //! type with the [`json_path`][JsonPathExt::json_path] method for performing JSONPath queries. //! //! Finally, the [`#[function]`][function] attribute macro can be used to extend JSONPath @@ -41,9 +40,11 @@ //! # } //! ``` //! -//! You can then use the parsed JSONPath to query a [`serde_json::Value`]. Every JSONPath query -//! produces a [`NodeList`], which provides several accessor methods that you can use depending on -//! the nature of your query and its expected output. +//! You then have two options to query a [`serde_json::Value`] using the parsed JSONPath: +//! [`JsonPath::query`] or [`JsonPath::query_located`]. The former will produce a [`NodeList`], +//! while the latter will produce a [`LocatedNodeList`]. The two options provide similar +//! functionality, but it is recommended to use the former unless you have need of node locations +//! in the query results. //! //! ## Querying for single nodes //! @@ -281,6 +282,37 @@ //! # Ok(()) //! # } //! ``` +//! +//! ## `NormalizedPath` support +//! +//! Should you need to know the locations of the nodes produced by your queries, you can make use +//! of the [`JsonPath::query_located`] method to perform the query. The resulting +//! [`LocatedNodeList`] contains both the nodes produced by the query, as well as their locations +//! represented by their [`NormalizedPath`]. +//! +//! ```rust +//! # use serde_json::json; +//! # use serde_json_path::JsonPath; +//! # fn main() -> Result<(), Box> { +//! let value = json!({ +//! "foo": { +//! "bar": { +//! "baz": 1 +//! }, +//! "baz": 2 +//! }, +//! "baz": 3, +//! }); +//! let path = JsonPath::parse("$..[? @.baz == 1]")?; +//! let location = path +//! .query_located(&value) +//! .exactly_one()? +//! .location() +//! .to_string(); +//! assert_eq!(location, "$['foo']['bar']"); +//! # Ok(()) +//! # } +//! ``` #![warn( clippy::all, diff --git a/serde_json_path_core/src/node.rs b/serde_json_path_core/src/node.rs index 6e1795a..25805e8 100644 --- a/serde_json_path_core/src/node.rs +++ b/serde_json_path_core/src/node.rs @@ -257,14 +257,7 @@ impl<'a> From> for NormalizedPath<'a> { } } -/// A list of nodes resulting from a JSONPath query, along with their locations -/// -/// As with [`NodeList`], each node is a borrowed reference to the node in the original -/// [`serde_json::Value`] that was queried; however, each node in the list is paired with its -/// location, which is represented by a [`NormalizedPath`]. -/// -/// In addition to the locations, [`LocatedNodeList`] provides useful functionality over [`NodeList`] -/// such as de-duplication of query results (see [`dedup`][LocatedNodeList::dedup]). +#[allow(missing_docs)] #[derive(Debug, Default, Eq, PartialEq, Serialize, Clone)] pub struct LocatedNodeList<'a>(Vec>); @@ -446,6 +439,67 @@ impl<'a> LocatedNodeList<'a> { .sort_unstable_by(|a, b| a.loc.partial_cmp(&b.loc).unwrap()); self.0.dedup(); } + + /// Return the first entry in the [`LocatedNodeList`], or `None` if it is empty + /// + /// # Usage + /// ```rust + /// # use serde_json::json; + /// # use serde_json_path::JsonPath; + /// # use serde_json_path::LocatedNode; + /// # fn main() -> Result<(), serde_json_path::ParseError> { + /// let value = json!({"foo": ["bar", "baz"]}); + /// let path = JsonPath::parse("$.foo.*")?; + /// let nodes = path.query_located(&value); + /// let first = nodes.first().map(LocatedNode::node); + /// assert_eq!(first, Some(&json!("bar"))); + /// # Ok(()) + /// # } + /// ``` + pub fn first(&self) -> Option<&LocatedNode<'a>> { + self.0.first() + } + + /// Return the last entry in the [`LocatedNodeList`], or `None` if it is empty + /// + /// # Usage + /// ```rust + /// # use serde_json::json; + /// # use serde_json_path::JsonPath; + /// # use serde_json_path::LocatedNode; + /// # fn main() -> Result<(), serde_json_path::ParseError> { + /// let value = json!({"foo": ["bar", "baz"]}); + /// let path = JsonPath::parse("$.foo.*")?; + /// let nodes = path.query_located(&value); + /// let last = nodes.last().map(LocatedNode::node); + /// assert_eq!(last, Some(&json!("baz"))); + /// # Ok(()) + /// # } + /// ``` + pub fn last(&self) -> Option<&LocatedNode<'a>> { + self.0.last() + } + + /// Returns the node at the given index in the [`LocatedNodeList`], or `None` if the + /// given index is out of bounds. + /// + /// # Usage + /// ```rust + /// # use serde_json::json; + /// # use serde_json_path::JsonPath; + /// # use serde_json_path::LocatedNode; + /// # fn main() -> Result<(), serde_json_path::ParseError> { + /// let value = json!({"foo": ["bar", "biz", "bop"]}); + /// let path = JsonPath::parse("$.foo.*")?; + /// let nodes = path.query_located(&value); + /// assert_eq!(nodes.get(1).map(LocatedNode::node), Some(&json!("biz"))); + /// assert!(nodes.get(4).is_none()); + /// # Ok(()) + /// # } + /// ``` + pub fn get(&self, index: usize) -> Option<&LocatedNode<'a>> { + self.0.get(index) + } } impl<'a> From>> for LocatedNodeList<'a> { From 0fe9baa17e2e4ffdf9cb5530961ada0c2b31acb5 Mon Sep 17 00:00:00 2001 From: Trevor Hilton Date: Tue, 30 Jan 2024 21:38:37 -0500 Subject: [PATCH 11/21] fix: remove useless test --- serde_json_path/src/path.rs | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/serde_json_path/src/path.rs b/serde_json_path/src/path.rs index 81a6f8a..10a3a66 100644 --- a/serde_json_path/src/path.rs +++ b/serde_json_path/src/path.rs @@ -179,16 +179,4 @@ mod tests { .expect("round trip"); assert_eq!(p1, p2); } - - #[test] - fn norm_paths() { - let j = json!({"foo": { - "bar": [1, 2, 3] - }}); - let p = JsonPath::parse("$.foo.bar.*").unwrap(); - let r = p.query_located(&j); - for ln in r { - println!("{pointer}", pointer = ln.location().as_json_pointer()); - } - } } From 22f6c4a7b89c74a4ac07296ecfa49c861a133c22 Mon Sep 17 00:00:00 2001 From: Trevor Hilton Date: Tue, 30 Jan 2024 22:07:36 -0500 Subject: [PATCH 12/21] docs: minor adjustments to path docs --- serde_json_path_core/src/path.rs | 33 ++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/serde_json_path_core/src/path.rs b/serde_json_path_core/src/path.rs index 7e1a4a0..8b28920 100644 --- a/serde_json_path_core/src/path.rs +++ b/serde_json_path_core/src/path.rs @@ -58,8 +58,8 @@ impl<'a> NormalizedPath<'a> { /// Check if the [`NormalizedPath`] is empty /// - /// This would also represent the normalized path of the root node in a JSON object, i.e., - /// `$`. + /// An empty normalized path represents the location of the root node of the JSON object, + /// i.e., `$`. pub fn is_empty(&self) -> bool { self.0.is_empty() } @@ -70,11 +70,40 @@ impl<'a> NormalizedPath<'a> { } /// Get an iterator over the [`PathElement`]s of the [`NormalizedPath`] + /// + /// Node that [`NormalizedPath`] also implements [`IntoIterator`] + /// + /// # Example + /// ```rust + /// # use serde_json::json; + /// # use serde_json_path::JsonPath; + /// # fn main() -> Result<(), Box> { + /// let mut value = json!({"foo": {"bar": 1, "baz": 2, "bop": 3}}); + /// let path = JsonPath::parse("$.foo[? @ == 2]")?; + /// let location = path.query_located(&value).exactly_one()?.to_location(); + /// let elements: Vec = location + /// .iter() + /// .map(|ele| ele.to_string()) + /// .collect(); + /// assert_eq!(elements, ["foo", "baz"]); + /// # Ok(()) + /// # } + /// ``` pub fn iter(&self) -> Iter<'_, PathElement<'a>> { self.0.iter() } } +impl<'a> IntoIterator for NormalizedPath<'a> { + type Item = PathElement<'a>; + + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + impl<'a> Display for NormalizedPath<'a> { /// Format the [`NormalizedPath`] as a JSONPath string using the canonical bracket notation /// as per the [JSONPath Specification][norm-paths] From 5ef1c068b325e534f34c16ebbb0a136d40fac488 Mon Sep 17 00:00:00 2001 From: Trevor Hilton Date: Tue, 30 Jan 2024 22:08:22 -0500 Subject: [PATCH 13/21] fix: typo --- serde_json_path_core/src/path.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/serde_json_path_core/src/path.rs b/serde_json_path_core/src/path.rs index 8b28920..de14826 100644 --- a/serde_json_path_core/src/path.rs +++ b/serde_json_path_core/src/path.rs @@ -71,7 +71,7 @@ impl<'a> NormalizedPath<'a> { /// Get an iterator over the [`PathElement`]s of the [`NormalizedPath`] /// - /// Node that [`NormalizedPath`] also implements [`IntoIterator`] + /// Note that [`NormalizedPath`] also implements [`IntoIterator`] /// /// # Example /// ```rust From 4f639ece6027b3d5f571f8ccadec50e53d30d15d Mon Sep 17 00:00:00 2001 From: Trevor Hilton Date: Wed, 31 Jan 2024 20:41:44 -0500 Subject: [PATCH 14/21] feat: get, first, and last to NormalizedPath feat: PartialEq implementations for PathElement --- serde_json_path/src/lib.rs | 2 +- serde_json_path_core/src/path.rs | 95 +++++++++++++++++++++++++++++++- 2 files changed, 95 insertions(+), 2 deletions(-) diff --git a/serde_json_path/src/lib.rs b/serde_json_path/src/lib.rs index 9170c3c..d3ec791 100644 --- a/serde_json_path/src/lib.rs +++ b/serde_json_path/src/lib.rs @@ -283,7 +283,7 @@ //! # } //! ``` //! -//! ## `NormalizedPath` support +//! ## Node locations and `NormalizedPath` //! //! Should you need to know the locations of the nodes produced by your queries, you can make use //! of the [`JsonPath::query_located`] method to perform the query. The resulting diff --git a/serde_json_path_core/src/path.rs b/serde_json_path_core/src/path.rs index de14826..e36fc3d 100644 --- a/serde_json_path_core/src/path.rs +++ b/serde_json_path_core/src/path.rs @@ -1,7 +1,11 @@ //! Types for representing [Normalized Paths][norm-paths] from the JSONPath specification //! //! [norm-paths]: https://www.ietf.org/archive/id/draft-ietf-jsonpath-base-21.html#name-normalized-paths -use std::{cmp::Ordering, fmt::Display, slice::Iter}; +use std::{ + cmp::Ordering, + fmt::Display, + slice::{Iter, SliceIndex}, +}; use serde::Serialize; @@ -92,6 +96,68 @@ impl<'a> NormalizedPath<'a> { pub fn iter(&self) -> Iter<'_, PathElement<'a>> { self.0.iter() } + + /// Get the [`PathElement`] at `index`, or `None` if the index is out of bounds + /// + /// # Example + /// ```rust + /// # use serde_json::json; + /// # use serde_json_path::JsonPath; + /// # fn main() -> Result<(), Box> { + /// let value = json!({"foo": {"bar": {"baz": "bop"}}}); + /// let path = JsonPath::parse("$..baz")?; + /// let location = path.query_located(&value).exactly_one()?.to_location(); + /// assert_eq!(location.to_string(), "$['foo']['bar']['baz']"); + /// assert!(location.get(0).is_some_and(|p| p == "foo")); + /// assert!(location.get(1..).is_some_and(|p| p == ["bar", "baz"])); + /// assert!(location.get(3).is_none()); + /// # Ok(()) + /// # } + /// ``` + pub fn get(&self, index: I) -> Option<&I::Output> + where + I: SliceIndex<[PathElement<'a>]>, + { + self.0.get(index) + } + + /// Get the first [`PathElement`], or `None` if the path is empty + /// + /// # Example + /// ```rust + /// # use serde_json::json; + /// # use serde_json_path::JsonPath; + /// # fn main() -> Result<(), Box> { + /// let value = json!(["foo", true, {"bar": false}, {"bar": true}]); + /// let path = JsonPath::parse("$..[? @ == false]")?; + /// let location = path.query_located(&value).exactly_one()?.to_location(); + /// assert_eq!(location.to_string(), "$[2]['bar']"); + /// assert!(location.first().is_some_and(|p| *p == 2)); + /// # Ok(()) + /// # } + /// ``` + pub fn first(&self) -> Option<&PathElement<'a>> { + self.0.first() + } + + /// Get the last [`PathElement`], or `None` if the path is empty + /// + /// # Example + /// ```rust + /// # use serde_json::json; + /// # use serde_json_path::JsonPath; + /// # fn main() -> Result<(), Box> { + /// let value = json!({"foo": {"bar": [1, 2, 3]}}); + /// let path = JsonPath::parse("$..[? @ == 2]")?; + /// let location = path.query_located(&value).exactly_one()?.to_location(); + /// assert_eq!(location.to_string(), "$['foo']['bar'][1]"); + /// assert!(location.last().is_some_and(|p| *p == 1)); + /// # Ok(()) + /// # } + /// ``` + pub fn last(&self) -> Option<&PathElement<'a>> { + self.0.last() + } } impl<'a> IntoIterator for NormalizedPath<'a> { @@ -171,6 +237,33 @@ impl<'a> PartialOrd for PathElement<'a> { } } +impl<'a> PartialEq for PathElement<'a> { + fn eq(&self, other: &str) -> bool { + match self { + PathElement::Name(s) => s.eq(&other), + PathElement::Index(_) => false, + } + } +} + +impl<'a, 'b> PartialEq<&str> for PathElement<'a> { + fn eq(&self, other: &&str) -> bool { + match self { + PathElement::Name(s) => s.eq(other), + PathElement::Index(_) => false, + } + } +} + +impl<'a> PartialEq for PathElement<'a> { + fn eq(&self, other: &usize) -> bool { + match self { + PathElement::Name(_) => false, + PathElement::Index(i) => i.eq(other), + } + } +} + impl<'a> Display for PathElement<'a> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { From b9246381551e4f77638498834e8221e9799bf6b2 Mon Sep 17 00:00:00 2001 From: Trevor Hilton Date: Wed, 31 Jan 2024 20:52:20 -0500 Subject: [PATCH 15/21] feat: add convenience methods to PathElement --- serde_json_path_core/src/path.rs | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/serde_json_path_core/src/path.rs b/serde_json_path_core/src/path.rs index e36fc3d..553e4b8 100644 --- a/serde_json_path_core/src/path.rs +++ b/serde_json_path_core/src/path.rs @@ -225,6 +225,32 @@ impl<'a> PathElement<'a> { PathElement::Index(i) => i.to_string(), } } + + /// Get the underlying name if the [`PathElement`] is `Name`, or `None` otherwise + pub fn as_name(&self) -> Option<&str> { + match self { + PathElement::Name(n) => Some(n), + PathElement::Index(_) => None, + } + } + + /// Get the underlying index if the [`PathElement`] is `Index`, or `None` otherwise + pub fn as_index(&self) -> Option { + match self { + PathElement::Name(_) => None, + PathElement::Index(i) => Some(*i), + } + } + + /// Test if the [`PathElement`] is `Name` + pub fn is_name(&self) -> bool { + self.as_name().is_some() + } + + /// Test if the [`PathElement`] is `Index` + pub fn is_index(&self) -> bool { + self.as_index().is_some() + } } impl<'a> PartialOrd for PathElement<'a> { From afd0649ac00aea87b44525c3721ff964fdffaa39 Mon Sep 17 00:00:00 2001 From: Trevor Hilton Date: Wed, 31 Jan 2024 21:37:04 -0500 Subject: [PATCH 16/21] docs: improve descendant example --- serde_json_path/src/lib.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/serde_json_path/src/lib.rs b/serde_json_path/src/lib.rs index d3ec791..a3b90ab 100644 --- a/serde_json_path/src/lib.rs +++ b/serde_json_path/src/lib.rs @@ -274,7 +274,8 @@ //! "baz": 1 //! }, //! "baz": 2 -//! } +//! }, +//! "baz": 3, //! }); //! let path = JsonPath::parse("$.foo..baz")?; //! let nodes = path.query(&value).all(); From ee1c60570c54295e0dc278b6446279a7ef38dae3 Mon Sep 17 00:00:00 2001 From: Trevor Hilton Date: Wed, 31 Jan 2024 21:38:23 -0500 Subject: [PATCH 17/21] fix: clippy --- serde_json_path_core/src/path.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/serde_json_path_core/src/path.rs b/serde_json_path_core/src/path.rs index 553e4b8..35444d0 100644 --- a/serde_json_path_core/src/path.rs +++ b/serde_json_path_core/src/path.rs @@ -272,7 +272,7 @@ impl<'a> PartialEq for PathElement<'a> { } } -impl<'a, 'b> PartialEq<&str> for PathElement<'a> { +impl<'a> PartialEq<&str> for PathElement<'a> { fn eq(&self, other: &&str) -> bool { match self { PathElement::Name(s) => s.eq(other), From 53ce3909257b110e48efd0ed6e49a5da9ab6aef7 Mon Sep 17 00:00:00 2001 From: Trevor Hilton Date: Thu, 1 Feb 2024 15:17:31 -0500 Subject: [PATCH 18/21] fix: change as_ to to_json_pointer to follow API guidelines --- serde_json_path_core/src/node.rs | 18 ++++++++++++++++++ serde_json_path_core/src/path.rs | 12 ++++++------ 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/serde_json_path_core/src/node.rs b/serde_json_path_core/src/node.rs index 25805e8..1577672 100644 --- a/serde_json_path_core/src/node.rs +++ b/serde_json_path_core/src/node.rs @@ -360,6 +360,24 @@ impl<'a> LocatedNodeList<'a> { /// /// To iterate over just locations, see [`locations`][LocatedNodeList::locations]. To iterate /// over just nodes, see [`nodes`][LocatedNodeList::nodes]. + /// + /// # Example + /// ```rust + /// # use serde_json::{json, Value}; + /// # use serde_json_path::JsonPath; + /// # fn main() -> Result<(), serde_json_path::ParseError> { + /// let value = json!({"foo": ["bar", "baz"]}); + /// let path = JsonPath::parse("$.foo.*")?; + /// let pairs: Vec<(String, &Value)> = path + /// .query_located(&value) + /// .iter() + /// .map(|q| (q.location().to_string(), q.node())) + /// .collect(); + /// assert_eq!(pairs[0], ("$['foo'][0]".to_owned(), &json!("bar"))); + /// assert_eq!(pairs[1], ("$['foo'][1]".to_owned(), &json!("baz"))); + /// # Ok(()) + /// # } + /// ``` pub fn iter(&self) -> Iter<'_, LocatedNode<'a>> { self.0.iter() } diff --git a/serde_json_path_core/src/path.rs b/serde_json_path_core/src/path.rs index 35444d0..5f78cc9 100644 --- a/serde_json_path_core/src/path.rs +++ b/serde_json_path_core/src/path.rs @@ -41,7 +41,7 @@ impl<'a> NormalizedPath<'a> { /// .query_located(&value) /// .exactly_one()? /// .location() - /// .as_json_pointer(); + /// .to_json_pointer(); /// *value.pointer_mut(&pointer).unwrap() = "bop".into(); /// assert_eq!(value, json!({"foo": ["bop", "baz"]})); /// # Ok(()) @@ -49,10 +49,10 @@ impl<'a> NormalizedPath<'a> { /// ``` /// /// [json-pointer]: https://datatracker.ietf.org/doc/html/rfc6901 - pub fn as_json_pointer(&self) -> String { + pub fn to_json_pointer(&self) -> String { self.0 .iter() - .map(PathElement::as_json_pointer) + .map(PathElement::to_json_pointer) .fold(String::from(""), |mut acc, s| { acc.push('/'); acc.push_str(&s); @@ -219,7 +219,7 @@ pub enum PathElement<'a> { } impl<'a> PathElement<'a> { - fn as_json_pointer(&self) -> String { + fn to_json_pointer(&self) -> String { match self { PathElement::Name(s) => s.replace('~', "~0").replace('/', "~1"), PathElement::Index(i) => i.to_string(), @@ -334,7 +334,7 @@ mod tests { PathElement::Index(42), PathElement::Name("bar"), ]); - assert_eq!(np.as_json_pointer(), "/foo/42/bar",); + assert_eq!(np.to_json_pointer(), "/foo/42/bar"); } #[test] @@ -344,6 +344,6 @@ mod tests { PathElement::Index(42), PathElement::Name("baz/bop"), ]); - assert_eq!(np.as_json_pointer(), "/foo~0bar/42/baz~1bop",); + assert_eq!(np.to_json_pointer(), "/foo~0bar/42/baz~1bop"); } } From 2a22ac2cc09b70bde3c6e68e9c55e03bdc377281 Mon Sep 17 00:00:00 2001 From: Trevor Hilton Date: Thu, 1 Feb 2024 15:23:42 -0500 Subject: [PATCH 19/21] test: add send and sync tests for LocatedNodeList --- serde_json_path_core/src/node.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/serde_json_path_core/src/node.rs b/serde_json_path_core/src/node.rs index 1577672..113eb3e 100644 --- a/serde_json_path_core/src/node.rs +++ b/serde_json_path_core/src/node.rs @@ -634,7 +634,7 @@ impl ExactlyOneError { #[cfg(test)] mod tests { - use crate::node::NodeList; + use crate::node::{LocatedNodeList, NodeList}; use serde_json::{json, to_value}; use serde_json_path::JsonPath; @@ -642,12 +642,14 @@ mod tests { fn test_send() { fn assert_send() {} assert_send::(); + assert_send::(); } #[test] fn test_sync() { fn assert_sync() {} assert_sync::(); + assert_sync::(); } #[test] From eeaa619c2d646d1a8349865ff32680790167372b Mon Sep 17 00:00:00 2001 From: Trevor Hilton Date: Thu, 1 Feb 2024 15:35:41 -0500 Subject: [PATCH 20/21] docs: changelog --- serde_json_path/CHANGELOG.md | 47 +++++++++++++++++++++++++++++++ serde_json_path_core/CHANGELOG.md | 47 +++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+) diff --git a/serde_json_path/CHANGELOG.md b/serde_json_path/CHANGELOG.md index 813e8a4..a51c586 100644 --- a/serde_json_path/CHANGELOG.md +++ b/serde_json_path/CHANGELOG.md @@ -7,6 +7,53 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 # Unreleased +## Added: `NormalizedPath` and `PathElement` types ([#78]) + +The `NormalizedPath` struct represents the location of a node within a JSON object. Its representation is like so: + +```rust +pub struct NormalizedPath<'a>(Vec); + +pub enum PathElement<'a> { + Name(&'a str), + Index(usize), +} +``` + +Several methods were included to interact with a `NormalizedPath`, e.g., `first`, `last`, `get`, `iter`, etc., but notably there is a `to_json_pointer` method, which allows direct conversion to a JSON Pointer to be used with the [`serde_json::Value::pointer`][pointer] or [`serde_json::Value::pointer_mut`][pointer-mut] methods. + +[pointer]: https://docs.rs/serde_json/latest/serde_json/enum.Value.html#method.pointer +[pointer-mut]: https://docs.rs/serde_json/latest/serde_json/enum.Value.html#method.pointer_mut + +The new `PathElement` type also comes equipped with several methods, and both it and `NormalizedPath` have eagerly implemented traits from the standard library / `serde` to help improve interoperability. + +## Added: `LocatedNodeList` and `LocatedNode` types ([#78]) + +The `LocatedNodeList` struct was built to have a similar API surface to the `NodeList` struct, but includes additional methods that give access to the location of each node produced by the original query. For example, it has the `locations` and `nodes` methods to provide dedicated iterators over locations or nodes, respectively, but also provides the `iter` method to iterate over the location/node pairs. Here is an example: + +```rust +use serde_json::{json, Value}; +use serde_json_path::JsonPath; +let value = json!({"foo": {"bar": 1, "baz": 2}}); +let path = JsonPath::parse("$.foo.*")?; +let query = path.query_located(&value); +let nodes: Vec<&Value> = query.nodes().collect(); +assert_eq!(nodes, vec![1, 2]); +let locs: Vec = query + .locations() + .map(|loc| loc.to_string()) + .collect(); +assert_eq!(locs, ["$['foo']['bar']", "$['foo']['baz']"]); +``` + +The location/node pairs are represented by the `LocatedNode` type. + +The `LocatedNodeList` provides one unique bit of functionality over `NodeList`: deduplication of the query results, via the `LocatedNodeList::dedup` and `LocatedNodeList::dedup_in_place` methods. + +[#78]: https://github.com/hiltontj/serde_json_path/pull/78 + +## Other Changes + - **internal**: address new clippy lints in Rust 1.75 ([#75]) - **internal**: address new clippy lints in Rust 1.74 ([#70]) - **internal**: code clean-up ([#72]) diff --git a/serde_json_path_core/CHANGELOG.md b/serde_json_path_core/CHANGELOG.md index f2cb0fe..0be1e6b 100644 --- a/serde_json_path_core/CHANGELOG.md +++ b/serde_json_path_core/CHANGELOG.md @@ -7,6 +7,53 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 # Unreleased +## Added: `NormalizedPath` and `PathElement` types ([#78]) + +The `NormalizedPath` struct represents the location of a node within a JSON object. Its representation is like so: + +```rust +pub struct NormalizedPath<'a>(Vec); + +pub enum PathElement<'a> { + Name(&'a str), + Index(usize), +} +``` + +Several methods were included to interact with a `NormalizedPath`, e.g., `first`, `last`, `get`, `iter`, etc., but notably there is a `to_json_pointer` method, which allows direct conversion to a JSON Pointer to be used with the [`serde_json::Value::pointer`][pointer] or [`serde_json::Value::pointer_mut`][pointer-mut] methods. + +[pointer]: https://docs.rs/serde_json/latest/serde_json/enum.Value.html#method.pointer +[pointer-mut]: https://docs.rs/serde_json/latest/serde_json/enum.Value.html#method.pointer_mut + +The new `PathElement` type also comes equipped with several methods, and both it and `NormalizedPath` have eagerly implemented traits from the standard library / `serde` to help improve interoperability. + +## Added: `LocatedNodeList` and `LocatedNode` types ([#78]) + +The `LocatedNodeList` struct was built to have a similar API surface to the `NodeList` struct, but includes additional methods that give access to the location of each node produced by the original query. For example, it has the `locations` and `nodes` methods to provide dedicated iterators over locations or nodes, respectively, but also provides the `iter` method to iterate over the location/node pairs. Here is an example: + +```rust +use serde_json::{json, Value}; +use serde_json_path::JsonPath; +let value = json!({"foo": {"bar": 1, "baz": 2}}); +let path = JsonPath::parse("$.foo.*")?; +let query = path.query_located(&value); +let nodes: Vec<&Value> = query.nodes().collect(); +assert_eq!(nodes, vec![1, 2]); +let locs: Vec = query + .locations() + .map(|loc| loc.to_string()) + .collect(); +assert_eq!(locs, ["$['foo']['bar']", "$['foo']['baz']"]); +``` + +The location/node pairs are represented by the `LocatedNode` type. + +The `LocatedNodeList` provides one unique bit of functionality over `NodeList`: deduplication of the query results, via the `LocatedNodeList::dedup` and `LocatedNodeList::dedup_in_place` methods. + +[#78]: https://github.com/hiltontj/serde_json_path/pull/78 + +## Other Changes + - **internal**: address new clippy lints in Rust 1.75 ([#75]) - **internal**: address new clippy lints in Rust 1.74 and update some tracing instrumentation ([#70]) - **internal**: code clean-up ([#72]) From caddcab336763858beca1072923ef93ed2be7c31 Mon Sep 17 00:00:00 2001 From: Trevor Hilton Date: Fri, 2 Feb 2024 15:20:35 -0500 Subject: [PATCH 21/21] feat: include query_located in CTS test --- serde_json_path/tests/compliance.rs | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/serde_json_path/tests/compliance.rs b/serde_json_path/tests/compliance.rs index 13850b3..531562f 100644 --- a/serde_json_path/tests/compliance.rs +++ b/serde_json_path/tests/compliance.rs @@ -24,7 +24,7 @@ struct TestCase { } #[test] -fn compliace_test_suite() { +fn compliance_test_suite() { let cts_json_str = fs::read_to_string("../jsonpath-compliance-test-suite/cts.json") .expect("read cts.json file"); @@ -50,12 +50,25 @@ fn compliace_test_suite() { "{name}: parsing {selector:?} should have failed", ); } else { - let actual = path.expect("valid JSON Path string").query(document).all(); + let path = path.expect("valid JSON Path string"); let expected = result.iter().collect::>(); - assert_eq!( - expected, actual, - "{name}: incorrect result, expected {expected:?}, got {actual:?}" - ); + { + // Query using JsonPath::query + let actual = path.query(document).all(); + assert_eq!( + expected, actual, + "{name}: incorrect result, expected {expected:?}, got {actual:?}" + ); + } + { + // Query using JsonPath::query_located + let q = path.query_located(document); + let actual = q.nodes().collect::>(); + assert_eq!( + expected, actual, + "(located) {name}: incorrect result, expected {expected:?}, got {actual:?}" + ); + } } } }