Slice arrays

This commit is contained in:
2026-03-21 10:20:25 -07:00
parent 7caf2553bc
commit 302d2acef3
3 changed files with 173 additions and 3 deletions

View File

@@ -1,7 +1,7 @@
use std::{fmt, str::FromStr};
use serde::{
Deserialize, Deserializer,
Deserialize, Deserializer, Serialize, Serializer,
de::{self, Visitor},
};
use smartstring::{LazyCompact, SmartString};
@@ -49,6 +49,13 @@ pub enum PathSegment {
/// Go to an element of the current list
Index(i64),
/// Go to a slice of the current list
Range {
start: i64,
end: i64,
inclusive: bool,
},
}
/// A path to aPathSegment::Field inside a nested object,
@@ -63,6 +70,39 @@ pub struct ObjectPath {
pub segments: Vec<PathSegment>,
}
impl fmt::Display for ObjectPath {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for seg in &self.segments {
match seg {
PathSegment::Root => write!(f, "$")?,
PathSegment::Field { name, args: None } => write!(f, ".{name}")?,
PathSegment::Field {
name,
args: Some(a),
} => write!(f, ".{name}({a})")?,
PathSegment::Index(i) => write!(f, "[{i}]")?,
PathSegment::Range {
start,
end,
inclusive: false,
} => write!(f, "[{start}..{end}]")?,
PathSegment::Range {
start,
end,
inclusive: true,
} => write!(f, "[{start}..={end}]")?,
}
}
Ok(())
}
}
impl Serialize for ObjectPath {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&self.to_string())
}
}
impl<'de> Deserialize<'de> for ObjectPath {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
struct PathVisitor;

View File

@@ -87,6 +87,15 @@ enum State {
/// We are indexing an array, waiting for a number
Index,
/// We parsed the start index, waiting for `]` or the first `.` of `..`
IndexAfterStart(i64),
/// We saw one `.` after the start index, waiting for the second `.`
IndexRangeDot1(i64),
/// We saw `..`, waiting for the end index (optionally prefixed with `=`)
IndexRangeDot2(i64),
/// We are indexing an array, waiting for a close-bracket
IndexClose,
}
@@ -164,8 +173,7 @@ impl Parser {
}
})?;
self.segments.push(PathSegment::Index(idx));
self.state = State::IndexClose;
self.state = State::IndexAfterStart(idx);
}
(State::Index, (p, Token::Root))
@@ -175,6 +183,49 @@ impl Parser {
return Err(PathParseError::Syntax { position: *p });
}
(State::IndexAfterStart(idx), (_, Token::SqbClose)) => {
self.segments.push(PathSegment::Index(idx));
self.state = State::Selected;
}
(State::IndexAfterStart(idx), (_, Token::Dot)) => {
self.state = State::IndexRangeDot1(idx);
}
(State::IndexAfterStart(_), (p, _)) => {
return Err(PathParseError::Syntax { position: *p });
}
(State::IndexRangeDot1(idx), (_, Token::Dot)) => {
self.state = State::IndexRangeDot2(idx);
}
(State::IndexRangeDot1(_), (p, _)) => {
return Err(PathParseError::Syntax { position: *p });
}
(State::IndexRangeDot2(start), (p, Token::Ident(ident))) => {
let (end_str, inclusive) = if let Some(stripped) = ident.strip_prefix('=') {
(stripped, true)
} else {
(*ident, false)
};
let end: i64 = i64::from_str(end_str).map_err(|_err| {
PathParseError::InvalidIndexString {
position: *p,
str: (*ident).into(),
}
})?;
self.segments.push(PathSegment::Range {
start,
end,
inclusive,
});
self.state = State::IndexClose;
}
(State::IndexRangeDot2(_), (p, _)) => {
return Err(PathParseError::Syntax { position: *p });
}
(State::IndexClose, (_, Token::SqbClose)) => self.state = State::Selected,
(State::IndexClose, (p, _)) => {
return Err(PathParseError::Syntax { position: *p });
@@ -187,6 +238,9 @@ impl Parser {
State::Start => Err(PathParseError::Syntax { position: 0 }),
State::Dot => Err(PathParseError::Syntax { position }),
State::Index => Err(PathParseError::Syntax { position }),
State::IndexAfterStart(_) => Err(PathParseError::Syntax { position }),
State::IndexRangeDot1(_) => Err(PathParseError::Syntax { position }),
State::IndexRangeDot2(_) => Err(PathParseError::Syntax { position }),
State::IndexClose => Err(PathParseError::Syntax { position }),
State::Selected => Ok(()),
}?;
@@ -387,4 +441,46 @@ mod tests {
}),
);
}
// MARK: range
fn range(start: i64, end: i64, inclusive: bool) -> PathSegment {
PathSegment::Range {
start,
end,
inclusive,
}
}
#[test]
fn exclusive_range() {
parse_test(
"$.a[0..5]",
Ok(&[PathSegment::Root, field("a"), range(0, 5, false)]),
);
}
#[test]
fn inclusive_range() {
parse_test(
"$.a[1..=2]",
Ok(&[PathSegment::Root, field("a"), range(1, 2, true)]),
);
}
#[test]
fn range_with_negative_end() {
parse_test(
"$.a[0..-1]",
Ok(&[PathSegment::Root, field("a"), range(0, -1, false)]),
);
}
#[test]
fn range_with_negative_start() {
parse_test(
"$.a[-3..-1]",
Ok(&[PathSegment::Root, field("a"), range(-3, -1, false)]),
);
}
}