Skip to content

Commit

Permalink
feat: parsing for the slice selector
Browse files Browse the repository at this point in the history
  • Loading branch information
V0ldek committed Jan 15, 2024
1 parent 91dd197 commit cc66ea0
Show file tree
Hide file tree
Showing 5 changed files with 154 additions and 268 deletions.
2 changes: 1 addition & 1 deletion crates/rsonpath-lib/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ impl UnsupportedFeatureError {
}

/// Slice Selector – supporting slice selectors.
/// https://github.com/V0ldek/rsonpath/issues/152
/// <https://github.com/V0ldek/rsonpath/issues/152>
#[must_use]
#[inline(always)]
pub fn slice_selector() -> Self {
Expand Down
70 changes: 67 additions & 3 deletions crates/rsonpath-syntax/src/builder.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! Utility for building a [`JsonPathQuery`](`crate::JsonPathQuery`)
//! programmatically.
use crate::{num::JsonInt, str::JsonString, Index, JsonPathQuery, Segment, Selector, Selectors};
use crate::{num::JsonInt, str::JsonString, Index, JsonPathQuery, Segment, Selector, Selectors, SliceBuilder};

/// Builder for [`JsonPathQuery`] instances.
///
Expand All @@ -13,12 +13,13 @@ use crate::{num::JsonInt, str::JsonString, Index, JsonPathQuery, Segment, Select
/// .descendant_name("b")
/// .child_wildcard()
/// .child_name("c")
/// .descendant_wildcard();
/// .descendant_wildcard()
/// .child_slice(|x| x.with_start(3).with_end(-7).with_step(2));
///
/// // Can also use `builder.build()` as a non-consuming version.
/// let query: JsonPathQuery = builder.into();
///
/// assert_eq!(query.to_string(), "$['a']..['b'][*]['c']..[*]");
/// assert_eq!(query.to_string(), "$['a']..['b'][*]['c']..[*][3:-7:2]");
/// ```
pub struct JsonPathQueryBuilder {
segments: Vec<Segment>,
Expand Down Expand Up @@ -133,6 +134,17 @@ impl JsonPathQueryBuilder {
self.child(|x| x.index(idx))
}

/// Add a child segment with a single slice selector.
///
/// This is a shorthand for `.child(|x| x.slice(slice_builder))`.
#[inline(always)]
pub fn child_slice<F>(&mut self, slice_builder: F) -> &mut Self
where
F: FnOnce(&mut SliceBuilder) -> &mut SliceBuilder,
{
self.child(|x| x.slice(slice_builder))
}

/// Add a descendant segment with a single name selector.
///
/// This is a shorthand for `.descendant(|x| x.name(name))`.
Expand All @@ -157,6 +169,17 @@ impl JsonPathQueryBuilder {
self.descendant(|x| x.index(idx))
}

/// Add a descendant segment with a single slice selector.
///
/// This is a shorthand for `.descendant(|x| x.slice(slice_builder))`.
#[inline(always)]
pub fn descendant_slice<F>(&mut self, slice_builder: F) -> &mut Self
where
F: FnOnce(&mut SliceBuilder) -> &mut SliceBuilder,
{
self.descendant(|x| x.slice(slice_builder))
}

/// Produce a [`JsonPathQuery`] from the builder.
///
/// This clones all data in the builder to create the query.
Expand Down Expand Up @@ -225,6 +248,47 @@ impl JsonPathSelectorsBuilder {
self
}

/// Add a slice selector based on a given start, end, and step integers.
///
/// The result is a [`Selector::Slice`] with given `start`, `end`, and `step`.
///
/// ## Examples
///
/// ```rust
/// # use rsonpath_syntax::{Selector, SliceBuilder, Index, Step, num::{JsonNonZeroUInt, JsonUInt}, builder::JsonPathQueryBuilder};
/// let mut builder = JsonPathQueryBuilder::new();
/// builder.child(|x| x
/// .slice(|s| s.with_start(10).with_end(-20).with_step(5))
/// .slice(|s| s.with_start(-20).with_step(-30)));
/// let result = builder.into_query();
///
/// assert_eq!(result.segments().len(), 1);
/// let segment = &result.segments()[0];
/// let selectors = segment.selectors().as_slice();
/// match (&selectors[0], &selectors[1]) {
/// (Selector::Slice(s1), Selector::Slice(s2)) => {
/// assert_eq!(s1.start(), Index::FromStart(10.into()));
/// assert_eq!(s1.end(), Some(Index::FromEnd(JsonNonZeroUInt::try_from(20).unwrap())));
/// assert_eq!(s1.step(), Step::Forward(5.into()));
/// assert_eq!(s2.start(), Index::FromEnd(JsonNonZeroUInt::try_from(20).unwrap()));
/// assert_eq!(s2.end(), None);
/// assert_eq!(s2.step(), Step::Backward(JsonNonZeroUInt::try_from(30).unwrap()));
/// }
/// _ => unreachable!()
/// }
/// ```
#[inline(always)]
pub fn slice<F>(&mut self, slice_builder: F) -> &mut Self
where
F: FnOnce(&mut SliceBuilder) -> &mut SliceBuilder,
{
let mut slice = SliceBuilder::new();
slice_builder(&mut slice);
let slice = slice.into();
self.selectors.push(Selector::Slice(slice));
self
}

/// Add a wildcard selector.
#[inline(always)]
pub fn wildcard(&mut self) -> &mut Self {
Expand Down
23 changes: 10 additions & 13 deletions crates/rsonpath-syntax/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -351,8 +351,8 @@ pub enum Selector {
/// An index selector matches at most one array element value,
/// depending on the selector's [`Index`].
Index(Index),
// A slice selector matches elements from arrays starting at a given index,
// ending at a given index, and incrementing with a specified step.
/// A slice selector matches elements from arrays starting at a given index,
/// ending at a given index, and incrementing with a specified step.
Slice(Slice),
}

Expand Down Expand Up @@ -392,7 +392,7 @@ impl From<num::JsonInt> for Index {
/// Directional step offset within a JSON array.
#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
pub enum Step {
// Step forward by a given offset amount.
/// Step forward by a given offset amount.
Forward(num::JsonUInt),
/// Step backward by a given offset amount.
Backward(num::JsonNonZeroUInt),
Expand Down Expand Up @@ -455,10 +455,7 @@ pub struct Slice {
/// # use rsonpath_syntax::{Slice, SliceBuilder, Index, Step, num::JsonUInt};
/// let mut builder = SliceBuilder::new();
///
/// builder
/// .with_start(Index::FromEnd(3.try_into().unwrap()))
/// .with_end(Index::FromStart(1.into()))
/// .with_step(Step::Backward(7.try_into().unwrap()));
/// builder.with_start(-3).with_end(1).with_step(-7);
///
/// let slice: Slice = builder.into();
/// assert_eq!(slice.to_string(), "-3:1:-7");
Expand Down Expand Up @@ -523,22 +520,22 @@ impl SliceBuilder {

/// Set the start of the [`Slice`].
#[inline]
pub fn with_start(&mut self, start: Index) -> &mut Self {
self.inner.start = start;
pub fn with_start<N: Into<num::JsonInt>>(&mut self, start: N) -> &mut Self {
self.inner.start = start.into().into();
self
}

/// Set the end of the [`Slice`].
#[inline]
pub fn with_end(&mut self, end: Index) -> &mut Self {
self.inner.end = Some(end);
pub fn with_end<N: Into<num::JsonInt>>(&mut self, end: N) -> &mut Self {
self.inner.end = Some(end.into().into());
self
}

/// Set the step of the [`Slice`].
#[inline]
pub fn with_step(&mut self, step: Step) -> &mut Self {
self.inner.step = step;
pub fn with_step<N: Into<num::JsonInt>>(&mut self, step: N) -> &mut Self {
self.inner.step = step.into().into();
self
}

Expand Down
98 changes: 75 additions & 23 deletions crates/rsonpath-syntax/src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use crate::{
error::{InternalParseError, ParseErrorBuilder, SyntaxError, SyntaxErrorKind},
num::{error::JsonIntParseError, JsonInt, JsonNonZeroUInt, JsonUInt},
str::{JsonString, JsonStringBuilder},
Index, JsonPathQuery, ParserOptions, Result, Segment, Selector, Selectors, SliceBuilder, Step,
Index, JsonPathQuery, ParserOptions, Result, Segment, Selector, Selectors, Step,
};
use nom::{branch::*, bytes::complete::*, character::complete::*, combinator::*, multi::*, sequence::*, *};
use std::{iter::Peekable, str::FromStr};
Expand Down Expand Up @@ -254,12 +254,12 @@ fn wildcard_selector(q: &str) -> IResult<&str, Selector, InternalParseError> {
fn slice_selector(q: &str) -> IResult<&str, Selector, InternalParseError> {
let (rest, opt_start) = terminated(opt(int), ignore_whitespace(char(':')))(q)?;
// We have parsed a ':', so this *must* be a slice selector. Any errors after here are fatal.
let mut slice = SliceBuilder::new();
let mut slice = crate::Slice::default();

if let Some(start_str) = opt_start {
match parse_directional_int(start_str) {
DirectionalInt::Plus(int) => slice.with_start(Index::FromStart(int)),
DirectionalInt::Minus(int) => slice.with_start(Index::FromEnd(int)),
DirectionalInt::Plus(int) => slice.start = Index::FromStart(int),
DirectionalInt::Minus(int) => slice.start = Index::FromEnd(int),
DirectionalInt::Error(err) => {
return fail(
SyntaxErrorKind::SliceStartParseError(err),
Expand All @@ -275,8 +275,8 @@ fn slice_selector(q: &str) -> IResult<&str, Selector, InternalParseError> {

if let Some(end_str) = opt_end {
match parse_directional_int(end_str) {
DirectionalInt::Plus(int) => slice.with_end(Index::FromStart(int)),
DirectionalInt::Minus(int) => slice.with_end(Index::FromEnd(int)),
DirectionalInt::Plus(int) => slice.end = Some(Index::FromStart(int)),
DirectionalInt::Minus(int) => slice.end = Some(Index::FromEnd(int)),
DirectionalInt::Error(err) => {
return fail(SyntaxErrorKind::SliceEndParseError(err), q.len(), end_str.len(), rest);
}
Expand All @@ -288,15 +288,15 @@ fn slice_selector(q: &str) -> IResult<&str, Selector, InternalParseError> {

if let Some(Some(step_str)) = opt_step {
match parse_directional_int(step_str) {
DirectionalInt::Plus(int) => slice.with_step(Step::Forward(int)),
DirectionalInt::Minus(int) => slice.with_step(Step::Backward(int)),
DirectionalInt::Plus(int) => slice.step = Step::Forward(int),
DirectionalInt::Minus(int) => slice.step = Step::Backward(int),
DirectionalInt::Error(err) => {
return fail(SyntaxErrorKind::SliceStepParseError(err), q.len(), step_str.len(), rest);
}
};
}

Ok((rest, Selector::Slice(slice.into())))
Ok((rest, Selector::Slice(slice)))
}

fn index_selector(q: &str) -> IResult<&str, Selector, InternalParseError> {
Expand Down Expand Up @@ -655,11 +655,13 @@ mod tests {
#[derive(Debug, Clone)]
enum SelectorTag {
WildcardChild,
Child(String),
WildcardDescendant,
Descendant(String),
NameChild(String),
ArrayIndexChild(JsonUInt),
ArraySliceChild(JsonUInt, Option<JsonUInt>, JsonUInt),
WildcardDescendant,
NameDescendant(String),
ArrayIndexDescendant(JsonUInt),
ArraySliceDescendant(JsonUInt, Option<JsonUInt>, JsonUInt),
}

#[derive(Debug, Clone)]
Expand Down Expand Up @@ -724,11 +726,13 @@ mod tests {
fn any_selector() -> impl Strategy<Value = Selector> {
prop_oneof![
any_wildcard_child(),
child_any(),
any_wildcard_descendant(),
descendant_any(),
any_child_name(),
any_array_index_child(),
any_array_slice_child(),
any_wildcard_descendant(),
any_descendant_name(),
any_array_index_descendant(),
any_array_slice_descendant(),
]
}

Expand All @@ -749,18 +753,18 @@ mod tests {
}

// .label or ['label']
fn child_any() -> impl Strategy<Value = Selector> {
fn any_child_name() -> impl Strategy<Value = Selector> {
prop_oneof![any_short_name().prop_map(|x| (format!(".{x}"), x)), any_name(),].prop_map(|(s, l)| Selector {
string: s,
tag: SelectorTag::Child(l),
tag: SelectorTag::NameChild(l),
})
}

// ..label or ..['label']
fn descendant_any() -> impl Strategy<Value = Selector> {
fn any_descendant_name() -> impl Strategy<Value = Selector> {
prop_oneof![any_short_name().prop_map(|x| (x.clone(), x)), any_name(),].prop_map(|(x, l)| Selector {
string: format!("..{x}"),
tag: SelectorTag::Descendant(l),
tag: SelectorTag::NameDescendant(l),
})
}

Expand All @@ -771,13 +775,45 @@ mod tests {
})
}

fn any_array_slice_child() -> impl Strategy<Value = Selector> {
(
any_non_negative_array_index(),
proptest::option::of(any_non_negative_array_index()),
any_non_negative_array_index(),
)
.prop_map(|(start, end, step)| Selector {
string: if let Some(end) = end {
format!("[{}:{}:{}]", start.as_u64(), end.as_u64(), step.as_u64())
} else {
format!("[{}::{}]", start.as_u64(), step.as_u64())
},
tag: SelectorTag::ArraySliceChild(start, end, step),
})
}

fn any_array_index_descendant() -> impl Strategy<Value = Selector> {
any_non_negative_array_index().prop_map(|i| Selector {
string: format!("..[{}]", i.as_u64()),
tag: SelectorTag::ArrayIndexDescendant(i),
})
}

fn any_array_slice_descendant() -> impl Strategy<Value = Selector> {
(
any_non_negative_array_index(),
proptest::option::of(any_non_negative_array_index()),
any_non_negative_array_index(),
)
.prop_map(|(start, end, step)| Selector {
string: if let Some(end) = end {
format!("..[{}:{}:{}]", start.as_u64(), end.as_u64(), step.as_u64())
} else {
format!("..[{}::{}]", start.as_u64(), step.as_u64())
},
tag: SelectorTag::ArraySliceDescendant(start, end, step),
})
}

fn any_short_name() -> impl Strategy<Value = String> {
r"([A-Za-z]|_|[^\u0000-\u007F])([A-Za-z0-9]|_|[^\u0000-\u007F])*"
}
Expand Down Expand Up @@ -831,12 +867,20 @@ mod tests {
result += &selector.string;

match selector.tag {
SelectorTag::NameChild(name) => query.child_name(JsonString::new(&name)),
SelectorTag::ArrayIndexChild(idx) => query.child_index(idx),
SelectorTag::ArraySliceChild(start, None, step) =>
query.child_slice(|x| x.with_start(start).with_step(step)),
SelectorTag::ArraySliceChild(start, Some(end), step) =>
query.child_slice(|x| x.with_start(start).with_end(end).with_step(step)),
SelectorTag::WildcardChild => query.child_wildcard(),
SelectorTag::Child(name) => query.child_name(JsonString::new(&name)),
SelectorTag::NameDescendant(name) => query.descendant_name(JsonString::new(&name)),
SelectorTag::ArrayIndexDescendant(idx) => query.descendant_index(idx),
SelectorTag::ArraySliceDescendant(start, None, step) =>
query.descendant_slice(|x| x.with_start(start).with_step(step)),
SelectorTag::ArraySliceDescendant(start, Some(end), step) =>
query.descendant_slice(|x| x.with_start(start).with_end(end).with_step(step)),
SelectorTag::WildcardDescendant => query.descendant_wildcard(),
SelectorTag::Descendant(name) => query.descendant_name(JsonString::new(&name)),
SelectorTag::ArrayIndexChild(idx) => query.child_index(idx),
SelectorTag::ArrayIndexDescendant(idx) => query.descendant_index(idx)
};
}

Expand All @@ -855,6 +899,14 @@ mod tests {

assert_eq!(expected, result);
}

#[test]
fn round_trip((_, query) in any_valid_query()) {
let input = query.to_string();
let result = crate::parse(&input).expect("expected Ok");

assert_eq!(query, result);
}
}
}
}
Expand Down
Loading

0 comments on commit cc66ea0

Please sign in to comment.