Skip to content

Commit

Permalink
Merge pull request #78 from hiltontj/76-norm-paths
Browse files Browse the repository at this point in the history
[#76] Support Normalized Paths
  • Loading branch information
hiltontj authored Feb 2, 2024
2 parents 7f45ef0 + caddcab commit 3b231b3
Show file tree
Hide file tree
Showing 15 changed files with 1,265 additions and 73 deletions.
47 changes: 47 additions & 0 deletions serde_json_path/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<PathElement<'a>);

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<String> = 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])
Expand Down
75 changes: 67 additions & 8 deletions serde_json_path/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
//!
Expand Down Expand Up @@ -269,14 +274,46 @@
//! "baz": 1
//! },
//! "baz": 2
//! }
//! },
//! "baz": 3,
//! });
//! let path = JsonPath::parse("$.foo..baz")?;
//! let nodes = path.query(&value).all();
//! assert_eq!(nodes, vec![2, 1]);
//! # 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<dyn std::error::Error>> {
//! 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,
Expand Down Expand Up @@ -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;

Expand Down
30 changes: 28 additions & 2 deletions serde_json_path/src/path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
};

Expand Down Expand Up @@ -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(())
Expand All @@ -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<String> = 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 {
Expand Down
25 changes: 19 additions & 6 deletions serde_json_path/tests/compliance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");

Expand All @@ -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::<Vec<&Value>>();
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::<Vec<&Value>>();
assert_eq!(
expected, actual,
"(located) {name}: incorrect result, expected {expected:?}, got {actual:?}"
);
}
}
}
}
Expand Down
47 changes: 47 additions & 0 deletions serde_json_path_core/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<PathElement<'a>);

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<String> = 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])
Expand Down
1 change: 1 addition & 0 deletions serde_json_path_core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,5 @@
#![forbid(unsafe_code)]

pub mod node;
pub mod path;
pub mod spec;
Loading

0 comments on commit 3b231b3

Please sign in to comment.