Add ObjectPath query language
This commit is contained in:
248
crates/pile-config/src/objectpath/parser.rs
Normal file
248
crates/pile-config/src/objectpath/parser.rs
Normal 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: 'é',
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user