Add ObjectPath query language
Some checks failed
CI / Typos (push) Successful in 19s
CI / Build and test (push) Failing after 40s
CI / Clippy (push) Failing after 53s

This commit is contained in:
2026-03-05 21:35:07 -08:00
parent 0053ed3a69
commit a9e402bc83
11 changed files with 657 additions and 48 deletions

View File

@@ -0,0 +1,248 @@
use std::str::FromStr;
use crate::{
Label,
objectpath::{PathParseError, PathSegment, tokenizer::Token},
};
enum State {
Start,
/// We are holding a pointer to an object
Selected,
/// We are waiting for an identifier
Dot,
/// We are indexing an array, waiting for a number
Index,
/// We are indexing an array, waiting for a close-bracket
IndexClose,
}
pub struct Parser {
state: State,
segments: Vec<PathSegment>,
}
impl Parser {
pub fn new() -> Self {
Parser {
state: State::Start,
segments: Vec::new(),
}
}
pub fn parse(
mut self,
source: &str,
tokens: &[(usize, Token<'_>)],
) -> Result<Vec<PathSegment>, PathParseError> {
for t in tokens {
match (self.state, t) {
(State::Start, (_, Token::Root)) => {
self.segments.push(PathSegment::Root);
self.state = State::Selected
}
(State::Start, (p, Token::Ident(_))) => {
return Err(PathParseError::MustStartWithRoot { position: *p });
}
(State::Start, (p, Token::Dot))
| (State::Start, (p, Token::SqbOpen))
| (State::Start, (p, Token::SqbClose)) => {
return Err(PathParseError::Syntax { position: *p });
}
//
// MARK: selected
//
(State::Selected, (_, Token::Dot)) => self.state = State::Dot,
(State::Selected, (_, Token::SqbOpen)) => self.state = State::Index,
(State::Selected, (p, Token::Root))
| (State::Selected, (p, Token::Ident(_)))
| (State::Selected, (p, Token::SqbClose)) => {
return Err(PathParseError::Syntax { position: *p });
}
//
// MARK: dot
//
(State::Dot, (p, Token::Ident(ident))) => {
self.segments
.push(PathSegment::Field(Label::new(*ident).ok_or_else(|| {
PathParseError::InvalidField {
position: *p,
str: (*ident).into(),
}
})?));
self.state = State::Selected;
}
(State::Dot, (p, Token::Root))
| (State::Dot, (p, Token::Dot))
| (State::Dot, (p, Token::SqbOpen))
| (State::Dot, (p, Token::SqbClose)) => {
return Err(PathParseError::Syntax { position: *p });
}
//
// MARK: index
//
(State::Index, (p, Token::Ident(ident))) => {
let idx: i64 = i64::from_str(ident).map_err(|_err| {
PathParseError::InvalidIndexString {
position: *p,
str: (*ident).into(),
}
})?;
self.segments.push(PathSegment::Index(idx));
self.state = State::IndexClose;
}
(State::Index, (p, Token::Root))
| (State::Index, (p, Token::Dot))
| (State::Index, (p, Token::SqbOpen))
| (State::Index, (p, Token::SqbClose)) => {
return Err(PathParseError::Syntax { position: *p });
}
(State::IndexClose, (_, Token::SqbClose)) => self.state = State::Selected,
(State::IndexClose, (p, _)) => {
return Err(PathParseError::Syntax { position: *p });
}
}
}
let position = source.len();
match self.state {
State::Start => Err(PathParseError::Syntax { position: 0 }),
State::Dot => Err(PathParseError::Syntax { position }),
State::Index => Err(PathParseError::Syntax { position }),
State::IndexClose => Err(PathParseError::Syntax { position }),
State::Selected => Ok(()),
}?;
return Ok(self.segments);
}
}
//
// MARK: tests
//
#[expect(clippy::unwrap_used)]
#[cfg(test)]
mod tests {
use crate::objectpath::tokenizer::Tokenizer;
use super::*;
fn parse_test(source: &str, expected: Result<&[PathSegment], PathParseError>) {
let parsed = Tokenizer::new()
.tokenize(source)
.and_then(|tokens| Parser::new().parse(source, &tokens[..]));
match (parsed, expected) {
(Ok(segments), Ok(segs)) => assert_eq!(segments, segs),
(Err(e), Err(expected_err)) => assert_eq!(e, expected_err),
(Ok(segments), Err(e)) => panic!("expected error {e}, got {:?}", segments),
(Err(e), Ok(segs)) => panic!("expected {:?}, got error {e}", segs),
}
}
#[test]
fn root_only() {
parse_test("$", Ok(&[PathSegment::Root]));
}
#[test]
fn single_field() {
parse_test(
"$.foo",
Ok(&[
PathSegment::Root,
PathSegment::Field(Label::new("foo").unwrap()),
]),
);
}
#[test]
fn nested_fields() {
parse_test(
"$.foo.bar.baz",
Ok(&[
PathSegment::Root,
PathSegment::Field(Label::new("foo").unwrap()),
PathSegment::Field(Label::new("bar").unwrap()),
PathSegment::Field(Label::new("baz").unwrap()),
]),
);
}
#[test]
fn array_index() {
parse_test(
"$.items[0]",
Ok(&[
PathSegment::Root,
PathSegment::Field(Label::new("items").unwrap()),
PathSegment::Index(0),
]),
);
}
#[test]
fn chained_indices() {
parse_test(
"$.a[1][2]",
Ok(&[
PathSegment::Root,
PathSegment::Field(Label::new("a").unwrap()),
PathSegment::Index(1),
PathSegment::Index(2),
]),
);
}
#[test]
fn field_after_index() {
parse_test(
"$.a[0].b",
Ok(&[
PathSegment::Root,
PathSegment::Field(Label::new("a").unwrap()),
PathSegment::Index(0),
PathSegment::Field(Label::new("b").unwrap()),
]),
);
}
#[test]
fn negative_index() {
parse_test(
"$.a[-1]",
Ok(&[
PathSegment::Root,
PathSegment::Field(Label::new("a").unwrap()),
PathSegment::Index(-1),
]),
);
}
#[test]
fn non_ascii_error() {
parse_test(
"$.fé",
Err(PathParseError::NonAsciiChar {
position: 3,
char: 'é',
}),
);
}
}