From 302d2acef334a1ad0ed1b179cc0377b2ef113816 Mon Sep 17 00:00:00 2001 From: rm-dr <96270320+rm-dr@users.noreply.github.com> Date: Sat, 21 Mar 2026 10:20:25 -0700 Subject: [PATCH] Slice arrays --- crates/pile-config/src/objectpath/mod.rs | 42 +++++++- crates/pile-config/src/objectpath/parser.rs | 100 +++++++++++++++++++- crates/pile-value/src/value/value.rs | 34 +++++++ 3 files changed, 173 insertions(+), 3 deletions(-) diff --git a/crates/pile-config/src/objectpath/mod.rs b/crates/pile-config/src/objectpath/mod.rs index f812896..998afb6 100644 --- a/crates/pile-config/src/objectpath/mod.rs +++ b/crates/pile-config/src/objectpath/mod.rs @@ -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, } +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(&self, serializer: S) -> Result { + serializer.serialize_str(&self.to_string()) + } +} + impl<'de> Deserialize<'de> for ObjectPath { fn deserialize>(deserializer: D) -> Result { struct PathVisitor; diff --git a/crates/pile-config/src/objectpath/parser.rs b/crates/pile-config/src/objectpath/parser.rs index 7b42fa2..7172a90 100644 --- a/crates/pile-config/src/objectpath/parser.rs +++ b/crates/pile-config/src/objectpath/parser.rs @@ -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)]), + ); + } } diff --git a/crates/pile-value/src/value/value.rs b/crates/pile-value/src/value/value.rs index a0d5efa..1e30160 100644 --- a/crates/pile-value/src/value/value.rs +++ b/crates/pile-value/src/value/value.rs @@ -140,6 +140,40 @@ impl PileValue { 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))); + } } }