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 std::{fmt, str::FromStr};
use serde::{ use serde::{
Deserialize, Deserializer, Deserialize, Deserializer, Serialize, Serializer,
de::{self, Visitor}, de::{self, Visitor},
}; };
use smartstring::{LazyCompact, SmartString}; use smartstring::{LazyCompact, SmartString};
@@ -49,6 +49,13 @@ pub enum PathSegment {
/// Go to an element of the current list /// Go to an element of the current list
Index(i64), 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, /// A path to aPathSegment::Field inside a nested object,
@@ -63,6 +70,39 @@ pub struct ObjectPath {
pub segments: Vec<PathSegment>, 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 { impl<'de> Deserialize<'de> for ObjectPath {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> { fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
struct PathVisitor; struct PathVisitor;

View File

@@ -87,6 +87,15 @@ enum State {
/// We are indexing an array, waiting for a number /// We are indexing an array, waiting for a number
Index, 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 /// We are indexing an array, waiting for a close-bracket
IndexClose, IndexClose,
} }
@@ -164,8 +173,7 @@ impl Parser {
} }
})?; })?;
self.segments.push(PathSegment::Index(idx)); self.state = State::IndexAfterStart(idx);
self.state = State::IndexClose;
} }
(State::Index, (p, Token::Root)) (State::Index, (p, Token::Root))
@@ -175,6 +183,49 @@ impl Parser {
return Err(PathParseError::Syntax { position: *p }); 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, (_, Token::SqbClose)) => self.state = State::Selected,
(State::IndexClose, (p, _)) => { (State::IndexClose, (p, _)) => {
return Err(PathParseError::Syntax { position: *p }); return Err(PathParseError::Syntax { position: *p });
@@ -187,6 +238,9 @@ impl Parser {
State::Start => Err(PathParseError::Syntax { position: 0 }), State::Start => Err(PathParseError::Syntax { position: 0 }),
State::Dot => Err(PathParseError::Syntax { position }), State::Dot => Err(PathParseError::Syntax { position }),
State::Index => 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::IndexClose => Err(PathParseError::Syntax { position }),
State::Selected => Ok(()), 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)]),
);
}
} }

View File

@@ -140,6 +140,40 @@ impl PileValue {
out = e.get(state, idx).await?; out = e.get(state, idx).await?;
} }
PathSegment::Range {
start,
end,
inclusive,
} => {
let e = match out.map(|x| x.list_extractor()) {
Some(e) => e,
None => {
out = None;
continue;
}
};
let len = e.len(state).await? as i64;
let start_idx = if *start >= 0 { *start } else { len + start };
let end_idx = if *end >= 0 { *end } else { len + end };
let end_idx = if *inclusive { end_idx + 1 } else { end_idx };
let start_idx = start_idx.max(0) as usize;
let end_idx = (end_idx.max(0) as usize).min(len as usize);
let mut items = Vec::new();
for i in start_idx..end_idx {
match e.get(state, i).await? {
Some(v) => items.push(v),
None => break,
}
}
// TODO: lazy view?
out = Some(PileValue::Array(Arc::new(items)));
}
} }
} }