hiltontj / serde_json_path

Query serde_json Values with JSONPath
https://serdejsonpath.live/
MIT License
50 stars 3 forks source link

[#76] Support Normalized Paths #78

Closed hiltontj closed 7 months ago

hiltontj commented 8 months ago

Closes #76.

This PR introduces support for Normalized Paths.

These can be accessed by a new query method JsonPath::query_located, which produces a LocatedNodeList. The LocatedNodeList is similar to NodeList, but contains the location of each node produced by the query, represented using the NormalizedPath type.

All new code has been documented with examples in this PR.

Added: NormalizedPath and PathElement types

The NormalizedPath struct represents the location of a node within a JSON object. Its representation is like so:

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 as_json_pointer method, which allows direct conversion to a JSON Pointer to be used with the serde_json::Value::pointer or serde_json::Value::pointer_mut methods.

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

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:

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.

Implementation Notes

The JsonPath::query_located method adds a completely separate function to perform queries in addition to the original JsonPath::query method. Their implementation of query execution is also completely separate, via the Queryable trait's query (existing) and query_located (new) methods, respectively. This was done, as opposed to, e.g., modifying the original query function to include locations, because I did not want to incur the overhead of producing the locations by default.

TODOs