Skip to content

Commit

Permalink
feat: add LocatedNode type
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
hiltontj committed Jan 29, 2024
1 parent 5b58d5d commit 53135af
Show file tree
Hide file tree
Showing 13 changed files with 170 additions and 80 deletions.
6 changes: 5 additions & 1 deletion serde_json_path/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
31 changes: 24 additions & 7 deletions serde_json_path/src/path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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(())
Expand All @@ -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<String> = 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())
Expand Down Expand Up @@ -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());
}
}
}
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;

Check failure on line 44 in serde_json_path_core/src/lib.rs

View workflow job for this annotation

GitHub Actions / Test

missing documentation for a module

Check warning on line 44 in serde_json_path_core/src/lib.rs

View workflow job for this annotation

GitHub Actions / Docs

missing documentation for a module

Check failure on line 44 in serde_json_path_core/src/lib.rs

View workflow job for this annotation

GitHub Actions / Docs

missing documentation for a module

Check failure on line 44 in serde_json_path_core/src/lib.rs

View workflow job for this annotation

GitHub Actions / Clippy

missing documentation for a module
pub mod spec;
78 changes: 58 additions & 20 deletions serde_json_path_core/src/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
///
Expand Down Expand Up @@ -227,13 +227,51 @@ impl<'a> IntoIterator for NodeList<'a> {
}
}

#[derive(Debug, Eq, PartialEq, Serialize, Clone)]
pub struct LocatedNode<'a> {

Check failure on line 231 in serde_json_path_core/src/node.rs

View workflow job for this annotation

GitHub Actions / Test

missing documentation for a struct

Check warning on line 231 in serde_json_path_core/src/node.rs

View workflow job for this annotation

GitHub Actions / Docs

missing documentation for a struct

Check failure on line 231 in serde_json_path_core/src/node.rs

View workflow job for this annotation

GitHub Actions / Docs

missing documentation for a struct

Check failure on line 231 in serde_json_path_core/src/node.rs

View workflow job for this annotation

GitHub Actions / Clippy

missing documentation for a struct
pub(crate) loc: NormalizedPath<'a>,
pub(crate) node: &'a Value,
}

impl<'a> LocatedNode<'a> {
pub fn location(&self) -> &NormalizedPath<'a> {

Check failure on line 237 in serde_json_path_core/src/node.rs

View workflow job for this annotation

GitHub Actions / Test

missing documentation for a method

Check warning on line 237 in serde_json_path_core/src/node.rs

View workflow job for this annotation

GitHub Actions / Docs

missing documentation for a method

Check failure on line 237 in serde_json_path_core/src/node.rs

View workflow job for this annotation

GitHub Actions / Docs

missing documentation for a method

Check failure on line 237 in serde_json_path_core/src/node.rs

View workflow job for this annotation

GitHub Actions / Clippy

missing documentation for a method
&self.loc
}

pub fn node(&self) -> &'a Value {

Check failure on line 241 in serde_json_path_core/src/node.rs

View workflow job for this annotation

GitHub Actions / Test

missing documentation for a method

Check warning on line 241 in serde_json_path_core/src/node.rs

View workflow job for this annotation

GitHub Actions / Docs

missing documentation for a method

Check failure on line 241 in serde_json_path_core/src/node.rs

View workflow job for this annotation

GitHub Actions / Docs

missing documentation for a method

Check failure on line 241 in serde_json_path_core/src/node.rs

View workflow job for this annotation

GitHub Actions / Clippy

missing documentation for a method
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<LocatedNode<'a>>);

impl<'a> LocatedNodeList<'a> {
pub fn at_most_one(
mut self,
) -> Result<Option<(NormalizedPath<'a>, &'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<Option<LocatedNode<'a>>, AtMostOneError> {
if self.0.is_empty() {
Ok(None)
} else if self.0.len() > 1 {
Expand All @@ -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<LocatedNode<'a>, ExactlyOneError> {

Check failure on line 284 in serde_json_path_core/src/node.rs

View workflow job for this annotation

GitHub Actions / Test

missing documentation for a method

Check warning on line 284 in serde_json_path_core/src/node.rs

View workflow job for this annotation

GitHub Actions / Docs

missing documentation for a method

Check failure on line 284 in serde_json_path_core/src/node.rs

View workflow job for this annotation

GitHub Actions / Docs

missing documentation for a method

Check failure on line 284 in serde_json_path_core/src/node.rs

View workflow job for this annotation

GitHub Actions / Clippy

missing documentation for a method
if self.0.is_empty() {
Err(ExactlyOneError::Empty)
} else if self.0.len() > 1 {
Expand All @@ -253,7 +291,7 @@ impl<'a> LocatedNodeList<'a> {
}
}

pub fn all(self) -> Vec<(NormalizedPath<'a>, &'a Value)> {
pub fn all(self) -> Vec<LocatedNode<'a>> {

Check failure on line 294 in serde_json_path_core/src/node.rs

View workflow job for this annotation

GitHub Actions / Test

missing documentation for a method

Check warning on line 294 in serde_json_path_core/src/node.rs

View workflow job for this annotation

GitHub Actions / Docs

missing documentation for a method

Check failure on line 294 in serde_json_path_core/src/node.rs

View workflow job for this annotation

GitHub Actions / Docs

missing documentation for a method

Check failure on line 294 in serde_json_path_core/src/node.rs

View workflow job for this annotation

GitHub Actions / Clippy

missing documentation for a method
self.0
}

Expand All @@ -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>> {

Check failure on line 306 in serde_json_path_core/src/node.rs

View workflow job for this annotation

GitHub Actions / Test

missing documentation for a method

Check warning on line 306 in serde_json_path_core/src/node.rs

View workflow job for this annotation

GitHub Actions / Docs

missing documentation for a method

Check failure on line 306 in serde_json_path_core/src/node.rs

View workflow job for this annotation

GitHub Actions / Docs

missing documentation for a method

Check failure on line 306 in serde_json_path_core/src/node.rs

View workflow job for this annotation

GitHub Actions / Clippy

missing documentation for a method
self.0.iter()
}

Expand All @@ -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<Vec<(NormalizedPath<'a>, &'a Value)>> for LocatedNodeList<'a> {
fn from(v: Vec<(NormalizedPath<'a>, &'a Value)>) -> Self {
impl<'a> From<Vec<LocatedNode<'a>>> for LocatedNodeList<'a> {
fn from(v: Vec<LocatedNode<'a>>) -> Self {
Self(v)
}
}

impl<'a> IntoIterator for LocatedNodeList<'a> {
type Item = (NormalizedPath<'a>, &'a Value);
type Item = LocatedNode<'a>;

type IntoIter = std::vec::IntoIter<Self::Item>;

Expand All @@ -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::Item> {
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::Item> {
self.inner.next_back().map(|(np, _)| np)
self.inner.next_back().map(|l| l.location())
}
}

Expand All @@ -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::Item> {
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::Item> {
self.inner.next_back().map(|(_, n)| *n)
self.inner.next_back().map(|l| l.node())
}
}

Expand Down
File renamed without changes.
1 change: 0 additions & 1 deletion serde_json_path_core/src/spec/mod.rs
Original file line number Diff line number Diff line change
@@ -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;
26 changes: 17 additions & 9 deletions serde_json_path_core/src/spec/query.rs
Original file line number Diff line number Diff line change
@@ -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::{
Expand Down Expand Up @@ -38,7 +40,7 @@ pub trait Queryable: sealed::Sealed {
current: &'b Value,
root: &'b Value,
parent: NormalizedPath<'b>,
) -> Vec<(NormalizedPath<'b>, &'b Value)>;
) -> Vec<LocatedNode<'b>>;
}

/// Represents a JSONPath expression
Expand Down Expand Up @@ -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<LocatedNode<'b>> {
let mut result: Vec<LocatedNode<'b>> = 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;
}
Expand Down
25 changes: 18 additions & 7 deletions serde_json_path_core/src/spec/segment.rs
Original file line number Diff line number Diff line change
@@ -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)]
Expand Down Expand Up @@ -61,7 +63,7 @@ impl Queryable for QuerySegment {
current: &'b Value,
root: &'b Value,
parent: NormalizedPath<'b>,
) -> Vec<(NormalizedPath<'b>, &'b Value)> {
) -> Vec<LocatedNode<'b>> {
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));
Expand Down Expand Up @@ -92,7 +94,7 @@ fn descend_paths<'b>(
current: &'b Value,
root: &'b Value,
parent: NormalizedPath<'b>,
) -> Vec<(NormalizedPath<'b>, &'b Value)> {
) -> Vec<LocatedNode<'b>> {
let mut result = Vec::new();
if let Some(list) = current.as_array() {
for (i, v) in list.iter().enumerate() {
Expand Down Expand Up @@ -214,7 +216,7 @@ impl Queryable for Segment {
current: &'b Value,
root: &'b Value,
mut parent: NormalizedPath<'b>,
) -> Vec<(NormalizedPath<'b>, &'b Value)> {
) -> Vec<LocatedNode<'b>> {
let mut result = vec![];
match self {
Segment::LongHand(selectors) => {
Expand All @@ -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,
});
}
}
}
Expand Down
Loading

0 comments on commit 53135af

Please sign in to comment.