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/src/lib.rs b/serde_json_path/src/lib.rs index e44eb40..a3b90ab 100644 --- a/serde_json_path/src/lib.rs +++ b/serde_json_path/src/lib.rs @@ -10,16 +10,19 @@ //! //! # 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`]. +//! [`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, 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 allows you to extend your JSONPath +//! Finally, the [`#[function]`][function] attribute macro can be used to extend JSONPath //! queries to use custom functions. //! //! # Usage @@ -37,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 //! @@ -269,7 +274,8 @@ //! "baz": 1 //! }, //! "baz": 2 -//! } +//! }, +//! "baz": 3, //! }); //! let path = JsonPath::parse("$.foo..baz")?; //! let nodes = path.query(&value).all(); @@ -277,6 +283,37 @@ //! # Ok(()) //! # } //! ``` +//! +//! ## 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 +//! [`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, @@ -328,8 +365,30 @@ 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, 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]). +pub use serde_json_path_core::node::LocatedNodeList; +#[doc(inline)] +pub use serde_json_path_core::node::{ + AtMostOneError, ExactlyOneError, LocatedNode, Locations, NodeList, Nodes, +}; +/// 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::node::{AtMostOneError, ExactlyOneError, NodeList}; +pub use serde_json_path_core::path::PathElement; 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 c957370..10a3a66 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::query::{Query, Queryable}, }; @@ -65,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(()) @@ -75,6 +75,32 @@ impl JsonPath { pub fn query<'b>(&self, value: &'b Value) -> NodeList<'b> { 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": {"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']"]); + /// # Ok(()) + /// # } + /// ``` + pub fn query_located<'b>(&self, value: &'b Value) -> LocatedNodeList<'b> { + self.0 + .query_located(value, value, Default::default()) + .into() + } } impl FromStr for JsonPath { 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:?}" + ); + } } } } 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]) 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 c3534d5..113eb3e 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::{iter::FusedIterator, slice::Iter}; use serde::Serialize; use serde_json::Value; +use crate::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 @@ -80,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. /// @@ -105,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() } @@ -209,6 +211,391 @@ impl<'a> NodeList<'a> { } } +impl<'a> From> for NodeList<'a> { + fn from(nodes: Vec<&'a Value>) -> Self { + Self(nodes) + } +} + +impl<'a> IntoIterator for NodeList<'a> { + type Item = &'a Value; + + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + +/// 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>, + pub(crate) node: &'a Value, +} + +impl<'a> LocatedNode<'a> { + /// Get the location of the node as a [`NormalizedPath`] + pub fn location(&self) -> &NormalizedPath<'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() + } +} + +#[allow(missing_docs)] +#[derive(Debug, Default, Eq, PartialEq, Serialize, Clone)] +pub struct LocatedNodeList<'a>(Vec>); + +impl<'a> LocatedNodeList<'a> { + /// 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; + /// # fn main() -> Result<(), Box> { + /// let value = json!({"foo": ["bar", "baz"]}); + /// # { + /// let path = JsonPath::parse("$.foo[0]")?; + /// 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(()) + /// # } + /// ``` + pub fn at_most_one(mut self) -> Result>, 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())) + } + } + + /// 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; + /// # 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) + } else if self.0.len() > 1 { + Err(ExactlyOneError::MoreThanOne(self.0.len())) + } else { + Ok(self.0.pop().unwrap()) + } + } + + /// 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]. + /// + /// # 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() + } + + /// 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. + self.0 + .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> { + fn from(v: Vec>) -> Self { + Self(v) + } +} + +impl<'a> IntoIterator for LocatedNodeList<'a> { + type Item = LocatedNode<'a>; + + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + +/// 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>>, +} + +impl<'a> Iterator for Locations<'a> { + type Item = &'a NormalizedPath<'a>; + + fn next(&mut self) -> Option { + self.inner.next().map(|l| l.location()) + } +} + +impl<'a> DoubleEndedIterator for Locations<'a> { + fn next_back(&mut self) -> Option { + self.inner.next_back().map(|l| l.location()) + } +} + +impl<'a> ExactSizeIterator for Locations<'a> { + fn len(&self) -> usize { + self.inner.len() + } +} + +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>>, +} + +impl<'a> Iterator for Nodes<'a> { + type Item = &'a Value; + + fn next(&mut self) -> Option { + self.inner.next().map(|l| l.node()) + } +} + +impl<'a> DoubleEndedIterator for Nodes<'a> { + fn next_back(&mut self) -> Option { + self.inner.next_back().map(|l| l.node()) + } +} + +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")] @@ -245,25 +632,9 @@ impl ExactlyOneError { } } -impl<'a> From> for NodeList<'a> { - fn from(nodes: Vec<&'a Value>) -> Self { - Self(nodes) - } -} - -impl<'a> IntoIterator for NodeList<'a> { - type Item = &'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; + use crate::node::{LocatedNodeList, NodeList}; use serde_json::{json, to_value}; use serde_json_path::JsonPath; @@ -271,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] diff --git a/serde_json_path_core/src/path.rs b/serde_json_path_core/src/path.rs new file mode 100644 index 0000000..5f78cc9 --- /dev/null +++ b/serde_json_path_core/src/path.rs @@ -0,0 +1,349 @@ +//! 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, SliceIndex}, +}; + +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>); + +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 + } + + /// 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() + /// .to_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 to_json_pointer(&self) -> String { + self.0 + .iter() + .map(PathElement::to_json_pointer) + .fold(String::from(""), |mut acc, s| { + acc.push('/'); + acc.push_str(&s); + acc + }) + } + + /// Check if the [`NormalizedPath`] is empty + /// + /// 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() + } + + /// Get the length of the [`NormalizedPath`] + pub fn len(&self) -> usize { + self.0.len() + } + + /// Get an iterator over the [`PathElement`]s of the [`NormalizedPath`] + /// + /// Note 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() + } + + /// 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> { + 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] + /// + /// # 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 { + 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()) + } +} + +/// 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 to_json_pointer(&self) -> String { + match self { + PathElement::Name(s) => s.replace('~', "~0").replace('/', "~1"), + 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> { + 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> PartialEq for PathElement<'a> { + fn eq(&self, other: &str) -> bool { + match self { + PathElement::Name(s) => s.eq(&other), + PathElement::Index(_) => false, + } + } +} + +impl<'a> 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 { + 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()) + } +} + +impl<'a> From for PathElement<'a> { + fn from(index: usize) -> Self { + Self::Index(index) + } +} + +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}; + + #[test] + fn normalized_path_to_json_pointer() { + let np = NormalizedPath(vec![ + PathElement::Name("foo"), + PathElement::Index(42), + PathElement::Name("bar"), + ]); + assert_eq!(np.to_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.to_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..2394a1a 100644 --- a/serde_json_path_core/src/spec/query.rs +++ b/serde_json_path_core/src/spec/query.rs @@ -1,6 +1,8 @@ //! Types representing queries in JSONPath use serde_json::Value; +use crate::{node::LocatedNode, path::NormalizedPath}; + use super::segment::QuerySegment; mod sealed { @@ -33,6 +35,14 @@ 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, + root: &'b Value, + parent: NormalizedPath<'b>, + ) -> Vec>; } /// Represents a JSONPath expression @@ -97,4 +107,30 @@ impl Queryable for Query { } query } + + fn query_located<'b>( + &self, + current: &'b Value, + root: &'b Value, + 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 LocatedNode { loc, node } in result { + r.append(&mut s.query_located(node, root, loc.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 5ce1b0b..beb220b 100644 --- a/serde_json_path_core/src/spec/segment.rs +++ b/serde_json_path_core/src/spec/segment.rs @@ -1,6 +1,8 @@ //! Types representing segments in JSONPath use serde_json::Value; +use crate::{node::LocatedNode, path::NormalizedPath}; + use super::{query::Queryable, selector::Selector}; /// A segment of a JSONPath query @@ -55,6 +57,21 @@ impl Queryable for QuerySegment { } query } + + fn query_located<'b>( + &self, + current: &'b Value, + root: &'b Value, + parent: NormalizedPath<'b>, + ) -> 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)); + result + } else { + self.segment.query_located(current, root, parent) + } + } } #[cfg_attr(feature = "trace", tracing::instrument(name = "Descend", level = "trace", parent = None, ret))] @@ -72,6 +89,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> { + let mut result = Vec::new(); + if let Some(list) = current.as_array() { + for (i, v) in list.iter().enumerate() { + 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_located(v, root, parent.clone_and_push(k))); + } + } + result +} + /// Represents the different forms of JSONPath segment #[derive(Debug, PartialEq, Eq, Clone)] pub enum Segment { @@ -174,4 +210,47 @@ impl Queryable for Segment { } query } + + fn query_located<'b>( + &self, + current: &'b Value, + root: &'b Value, + mut parent: NormalizedPath<'b>, + ) -> Vec> { + let mut result = vec![]; + match self { + Segment::LongHand(selectors) => { + for s in selectors { + result.append(&mut s.query_located(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(LocatedNode { + loc: parent, + node: v, + }); + } + } + Segment::Wildcard => { + if let Some(list) = current.as_array() { + for (i, v) in list.iter().enumerate() { + 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(LocatedNode { + loc: parent.clone_and_push(k), + node: 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 563a535..d63e8a2 100644 --- a/serde_json_path_core/src/spec/selector/filter.rs +++ b/serde_json_path_core/src/spec/selector/filter.rs @@ -1,10 +1,14 @@ //! Types representing filter selectors in JSONPath use serde_json::{Number, Value}; -use crate::spec::{ - functions::{FunctionExpr, JsonPathValue, Validated}, - query::{Query, QueryKind, Queryable}, - segment::{QuerySegment, Segment}, +use crate::{ + node::LocatedNode, + path::NormalizedPath, + spec::{ + functions::{FunctionExpr, JsonPathValue, Validated}, + query::{Query, QueryKind, Queryable}, + segment::{QuerySegment, Segment}, + }, }; use super::{index::Index, name::Name, Selector}; @@ -69,6 +73,34 @@ impl Queryable for Filter { vec![] } } + + fn query_located<'b>( + &self, + current: &'b Value, + root: &'b Value, + parent: NormalizedPath<'b>, + ) -> Vec> { + if let Some(list) = current.as_array() { + list.iter() + .enumerate() + .filter(|(_, v)| self.0.test_filter(v, root)) + .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)| LocatedNode { + loc: parent.clone_and_push(k), + node: v, + }) + .collect() + } else { + vec![] + } + } } /// The top level boolean expression type @@ -558,15 +590,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![], - } - } -} - 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 ab369df..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::query::Queryable; +use crate::{node::LocatedNode, path::NormalizedPath, spec::query::Queryable}; /// For selecting array elements by their index /// @@ -38,6 +38,32 @@ impl Queryable for Index { vec![] } } + + fn query_located<'b>( + &self, + current: &'b Value, + _root: &'b Value, + mut parent: NormalizedPath<'b>, + ) -> Vec> { + if let Some((index, node)) = 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.get(i).map(|v| (i, v))) + } + }) { + parent.push(index); + vec![LocatedNode { loc: parent, node }] + } 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..2ec51de 100644 --- a/serde_json_path_core/src/spec/selector/mod.rs +++ b/serde_json_path_core/src/spec/selector/mod.rs @@ -6,6 +6,8 @@ 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::query::Queryable; @@ -70,4 +72,38 @@ impl Queryable for Selector { } query } + + fn query_located<'b>( + &self, + current: &'b Value, + root: &'b Value, + parent: NormalizedPath<'b>, + ) -> 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, node)| LocatedNode { + loc: parent.clone_and_push(i), + node, + }) + .collect() + } else if let Some(obj) = current.as_object() { + obj.iter() + .map(|(k, node)| LocatedNode { + loc: parent.clone_and_push(k), + node, + }) + .collect() + } else { + vec![] + } + } + 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 de2c350..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::query::Queryable; +use crate::{node::LocatedNode, path::NormalizedPath, spec::query::Queryable}; /// Select a single JSON object key #[derive(Debug, PartialEq, Eq, Clone)] @@ -29,6 +29,20 @@ impl Queryable for Name { vec![] } } + + fn query_located<'b>( + &self, + current: &'b Value, + _root: &'b Value, + mut parent: NormalizedPath<'b>, + ) -> 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![] + } + } } 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..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::query::Queryable; +use crate::{node::LocatedNode, path::NormalizedPath, spec::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)) { @@ -118,6 +127,60 @@ impl Queryable for Slice { vec![] } } + + fn query_located<'b>( + &self, + current: &'b Value, + _root: &'b Value, + parent: NormalizedPath<'b>, + ) -> Vec> { + 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, node)) = usize::try_from(i) + .ok() + .and_then(|i| list.get(i).map(|v| (i, v))) + { + result.push(LocatedNode { + loc: parent.clone_and_push(i), + node, + }); + } + 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, node)) = usize::try_from(i) + .ok() + .and_then(|i| list.get(i).map(|v| (i, v))) + { + result.push(LocatedNode { + loc: parent.clone_and_push(i), + node, + }); + } + i += step; + } + } + result + } else { + vec![] + } + } } fn normalize_slice_index(index: isize, len: isize) -> Option {