Skip to content

Commit

Permalink
feat: a start on normalized path support
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
hiltontj committed Jan 26, 2024
1 parent 7f45ef0 commit c01a942
Show file tree
Hide file tree
Showing 9 changed files with 226 additions and 4 deletions.
1 change: 1 addition & 0 deletions serde_json_path_core/src/spec/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
//! Types representing the IETF JSONPath Standard
pub mod functions;
pub mod path;

Check warning on line 3 in serde_json_path_core/src/spec/mod.rs

View workflow job for this annotation

GitHub Actions / Docs

missing documentation for a module
pub mod query;
pub mod segment;
pub mod selector;
83 changes: 83 additions & 0 deletions serde_json_path_core/src/spec/path.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
use std::ops::{Deref, DerefMut};

#[derive(Clone)]
pub struct NormalizedPath<'a>(Vec<PathElement<'a>>);

Check warning on line 4 in serde_json_path_core/src/spec/path.rs

View workflow job for this annotation

GitHub Actions / Docs

missing documentation for a struct

impl<'a> NormalizedPath<'a> {
pub fn as_json_pointer(&self) -> String {

Check warning on line 7 in serde_json_path_core/src/spec/path.rs

View workflow job for this annotation

GitHub Actions / Docs

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

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> {

Check warning on line 34 in serde_json_path_core/src/spec/path.rs

View workflow job for this annotation

GitHub Actions / Docs

missing documentation for an enum
Name(&'a str),

Check warning on line 35 in serde_json_path_core/src/spec/path.rs

View workflow job for this annotation

GitHub Actions / Docs

missing documentation for a variant
Index(usize),

Check warning on line 36 in serde_json_path_core/src/spec/path.rs

View workflow job for this annotation

GitHub Actions / Docs

missing documentation for a variant
}

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<usize> 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",);
}
}
17 changes: 16 additions & 1 deletion serde_json_path_core/src/spec/query.rs
Original file line number Diff line number Diff line change
@@ -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::{
Expand Down Expand Up @@ -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>(

Check warning on line 36 in serde_json_path_core/src/spec/query.rs

View workflow job for this annotation

GitHub Actions / Docs

missing documentation for a method
&self,
current: &'b Value,
_root: &'b Value,
parent: NormalizedPath<'b>,
) -> Vec<NormalizedPath<'b>>;
}

/// Represents a JSONPath expression
Expand Down Expand Up @@ -97,4 +103,13 @@ impl Queryable for Query {
}
query
}

fn query_paths<'b>(
&self,
current: &'b Value,

Check failure on line 109 in serde_json_path_core/src/spec/query.rs

View workflow job for this annotation

GitHub Actions / Clippy

unused variable: `current`

Check failure on line 109 in serde_json_path_core/src/spec/query.rs

View workflow job for this annotation

GitHub Actions / Docs

unused variable: `current`

Check failure on line 109 in serde_json_path_core/src/spec/query.rs

View workflow job for this annotation

GitHub Actions / Test

unused variable: `current`
_root: &'b Value,
parent: NormalizedPath<'b>,

Check failure on line 111 in serde_json_path_core/src/spec/query.rs

View workflow job for this annotation

GitHub Actions / Clippy

unused variable: `parent`

Check failure on line 111 in serde_json_path_core/src/spec/query.rs

View workflow job for this annotation

GitHub Actions / Docs

unused variable: `parent`

Check failure on line 111 in serde_json_path_core/src/spec/query.rs

View workflow job for this annotation

GitHub Actions / Test

unused variable: `parent`
) -> Vec<NormalizedPath<'b>> {
todo!()
}
}
18 changes: 18 additions & 0 deletions serde_json_path_core/src/spec/segment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,15 @@ impl Queryable for QuerySegment {
}
query
}

fn query_paths<'b>(
&self,
current: &'b Value,

Check failure on line 61 in serde_json_path_core/src/spec/segment.rs

View workflow job for this annotation

GitHub Actions / Clippy

unused variable: `current`

Check failure on line 61 in serde_json_path_core/src/spec/segment.rs

View workflow job for this annotation

GitHub Actions / Docs

unused variable: `current`

Check failure on line 61 in serde_json_path_core/src/spec/segment.rs

View workflow job for this annotation

GitHub Actions / Test

unused variable: `current`
_root: &'b Value,
parent: super::path::NormalizedPath<'b>,

Check failure on line 63 in serde_json_path_core/src/spec/segment.rs

View workflow job for this annotation

GitHub Actions / Clippy

unused variable: `parent`

Check failure on line 63 in serde_json_path_core/src/spec/segment.rs

View workflow job for this annotation

GitHub Actions / Docs

unused variable: `parent`

Check failure on line 63 in serde_json_path_core/src/spec/segment.rs

View workflow job for this annotation

GitHub Actions / Test

unused variable: `parent`
) -> Vec<super::path::NormalizedPath<'b>> {
todo!()
}
}

#[cfg_attr(feature = "trace", tracing::instrument(name = "Descend", level = "trace", parent = None, ret))]
Expand Down Expand Up @@ -174,4 +183,13 @@ impl Queryable for Segment {
}
query
}

fn query_paths<'b>(
&self,
current: &'b Value,

Check failure on line 189 in serde_json_path_core/src/spec/segment.rs

View workflow job for this annotation

GitHub Actions / Clippy

unused variable: `current`

Check failure on line 189 in serde_json_path_core/src/spec/segment.rs

View workflow job for this annotation

GitHub Actions / Docs

unused variable: `current`

Check failure on line 189 in serde_json_path_core/src/spec/segment.rs

View workflow job for this annotation

GitHub Actions / Test

unused variable: `current`
_root: &'b Value,
parent: super::path::NormalizedPath<'b>,

Check failure on line 191 in serde_json_path_core/src/spec/segment.rs

View workflow job for this annotation

GitHub Actions / Clippy

unused variable: `parent`

Check failure on line 191 in serde_json_path_core/src/spec/segment.rs

View workflow job for this annotation

GitHub Actions / Docs

unused variable: `parent`

Check failure on line 191 in serde_json_path_core/src/spec/segment.rs

View workflow job for this annotation

GitHub Actions / Test

unused variable: `parent`
) -> Vec<super::path::NormalizedPath<'b>> {
todo!()
}
}
18 changes: 18 additions & 0 deletions serde_json_path_core/src/spec/selector/filter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,15 @@ impl Queryable for Filter {
vec![]
}
}

fn query_paths<'b>(
&self,
current: &'b Value,

Check failure on line 75 in serde_json_path_core/src/spec/selector/filter.rs

View workflow job for this annotation

GitHub Actions / Clippy

unused variable: `current`

Check failure on line 75 in serde_json_path_core/src/spec/selector/filter.rs

View workflow job for this annotation

GitHub Actions / Docs

unused variable: `current`

Check failure on line 75 in serde_json_path_core/src/spec/selector/filter.rs

View workflow job for this annotation

GitHub Actions / Test

unused variable: `current`
_root: &'b Value,
parent: crate::spec::path::NormalizedPath<'b>,

Check failure on line 77 in serde_json_path_core/src/spec/selector/filter.rs

View workflow job for this annotation

GitHub Actions / Clippy

unused variable: `parent`

Check failure on line 77 in serde_json_path_core/src/spec/selector/filter.rs

View workflow job for this annotation

GitHub Actions / Docs

unused variable: `parent`

Check failure on line 77 in serde_json_path_core/src/spec/selector/filter.rs

View workflow job for this annotation

GitHub Actions / Test

unused variable: `parent`
) -> Vec<crate::spec::path::NormalizedPath<'b>> {
todo!()
}
}

/// The top level boolean expression type
Expand Down Expand Up @@ -565,6 +574,15 @@ impl Queryable for SingularQuery {
None => vec![],
}
}

fn query_paths<'b>(
&self,
current: &'b Value,

Check failure on line 580 in serde_json_path_core/src/spec/selector/filter.rs

View workflow job for this annotation

GitHub Actions / Clippy

unused variable: `current`

Check failure on line 580 in serde_json_path_core/src/spec/selector/filter.rs

View workflow job for this annotation

GitHub Actions / Docs

unused variable: `current`

Check failure on line 580 in serde_json_path_core/src/spec/selector/filter.rs

View workflow job for this annotation

GitHub Actions / Test

unused variable: `current`
_root: &'b Value,
parent: crate::spec::path::NormalizedPath<'b>,

Check failure on line 582 in serde_json_path_core/src/spec/selector/filter.rs

View workflow job for this annotation

GitHub Actions / Clippy

unused variable: `parent`

Check failure on line 582 in serde_json_path_core/src/spec/selector/filter.rs

View workflow job for this annotation

GitHub Actions / Docs

unused variable: `parent`

Check failure on line 582 in serde_json_path_core/src/spec/selector/filter.rs

View workflow job for this annotation

GitHub Actions / Test

unused variable: `parent`
) -> Vec<crate::spec::path::NormalizedPath<'b>> {
todo!()
}
}

impl std::fmt::Display for SingularQuery {
Expand Down
27 changes: 26 additions & 1 deletion serde_json_path_core/src/spec/selector/index.rs
Original file line number Diff line number Diff line change
@@ -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
///
Expand Down Expand Up @@ -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<NormalizedPath<'b>> {
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<isize> for Index {
Expand Down
41 changes: 40 additions & 1 deletion serde_json_path_core/src/spec/selector/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -70,4 +73,40 @@ impl Queryable for Selector {
}
query
}

fn query_paths<'b>(
&self,
current: &'b Value,
root: &'b Value,
parent: NormalizedPath<'b>,
) -> Vec<NormalizedPath<'b>> {
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),
}
}
}
16 changes: 15 additions & 1 deletion serde_json_path_core/src/spec/selector/name.rs
Original file line number Diff line number Diff line change
@@ -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)]
Expand Down Expand Up @@ -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<NormalizedPath<'b>> {
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 {
Expand Down
9 changes: 9 additions & 0 deletions serde_json_path_core/src/spec/selector/slice.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<crate::spec::path::NormalizedPath<'b>> {
todo!()
}
}

fn normalize_slice_index(index: isize, len: isize) -> Option<isize> {
Expand Down

0 comments on commit c01a942

Please sign in to comment.