use super::{DotArg, IndexArg, Selector};
use winnow::ascii::dec_uint;
use winnow::combinator::{alt, delimited, eof, peek, repeat, seq};
use winnow::error::ParserError;
use winnow::prelude::*;
use winnow::token::{one_of, take_while};
pub(crate) fn parse_selector(mut input: &str) -> Result<Selector, String> {
let input: &mut &str = &mut input;
match root.parse_next(input) {
Ok(selector) => {
match repeat(0.., delimited(ws, alt((dot, index)), alt((ws, eof))))
.fold(
|| selector.clone(),
|acc, selector_part: SelectorPart| match selector_part {
SelectorPart::DotArg(dot_arg) => Selector::Dot(Box::new(acc), dot_arg),
SelectorPart::IndexArg(index_arg) => {
Selector::Index(Box::new(acc), index_arg)
}
},
)
.parse(input)
{
Ok(selector) => Ok(selector),
Err(err) => Err(format!("{err}")),
}
}
Err(_) => Err("expected root selector '$'".to_string()),
}
}
fn root(input: &mut &str) -> ModalResult<Selector> {
delimited(ws, "$", ws)
.parse_next(input)
.map(|_| Selector::Root)
}
enum SelectorPart {
DotArg(DotArg),
IndexArg(IndexArg),
}
fn dot(input: &mut &str) -> ModalResult<SelectorPart> {
struct Matcher {
dot_arg: DotArg,
}
delimited(
ws,
seq! {
Matcher {
_: ".",
_: ws,
dot_arg: dot_arg,
}
},
ws,
)
.parse_next(input)
.map(|matcher| SelectorPart::DotArg(matcher.dot_arg.clone()))
}
fn index(input: &mut &str) -> ModalResult<SelectorPart> {
struct Matcher {
index_arg: IndexArg,
}
delimited(
ws,
seq! {
Matcher {
_: "[",
_: ws,
index_arg: index_arg,
_: ws,
_: "]"
}
},
ws,
)
.parse_next(input)
.map(|matcher| SelectorPart::IndexArg(matcher.index_arg))
}
fn dot_arg(input: &mut &str) -> ModalResult<DotArg> {
unquoted_field.parse_next(input).map(DotArg::Field)
}
fn index_arg(input: &mut &str) -> ModalResult<IndexArg> {
alt((
dec_uint.map(|n: usize| IndexArg::Number(n)),
alt((unquoted_field, quoted_field)).map(IndexArg::Field),
wildcard.map(|_| IndexArg::Wildcard),
item.map(|_| IndexArg::Item),
))
.parse_next(input)
}
fn wildcard(input: &mut &str) -> ModalResult<()> {
"*".parse_next(input).map(|_| ())
}
fn item(input: &mut &str) -> ModalResult<()> {
"@".parse_next(input).map(|_| ())
}
fn unquoted_field(input: &mut &str) -> ModalResult<String> {
let first = one_of(|ch: char| ch == '_' || ch.is_alphabetic()).parse_next(input)?;
let rest = take_while(0.., |c: char| c.is_alphanumeric() || c == '_').parse_next(input)?;
Ok(format!("{first}{rest}"))
}
fn quoted_field(input: &mut &str) -> ModalResult<String> {
let mut quote_char = peek(one_of(|ch| ch == '"' || ch == '\''));
let (_, quote_char) = quote_char.parse_peek(*input)?;
delimited(
quote_char,
take_while(1.., move |c| c != quote_char),
quote_char,
)
.parse_next(input)
.map(String::from)
}
fn ws<'i, E: ParserError<&'i str>>(input: &mut &'i str) -> ModalResult<&'i str, E> {
take_while(0.., WS).parse_next(input)
}
const WS: &[char] = &[' ', '\t', '\r', '\n'];
#[cfg(test)]
mod test {
use super::parse_selector;
use super::{DotArg, IndexArg, Selector};
#[test]
fn basic() {
assert_eq!(parse_selector("$"), Ok(Selector::Root));
assert_eq!(
parse_selector("$.name"),
Ok(Selector::Dot(
Box::new(Selector::Root),
DotArg::Field("name".into())
))
);
assert_eq!(
parse_selector("$[*]"),
Ok(Selector::Index(
Box::new(Selector::Root),
IndexArg::Wildcard,
))
);
assert_eq!(
parse_selector("$[@]"),
Ok(Selector::Index(Box::new(Selector::Root), IndexArg::Item,))
);
assert_eq!(
parse_selector("$ [ * ] . age "),
Ok(Selector::Dot(
Box::new(Selector::Index(
Box::new(Selector::Root),
IndexArg::Wildcard
)),
DotArg::Field("age".into())
))
);
}
#[test]
fn unquoted_field_names_can_contain_underscores() {
assert_eq!(
parse_selector("$.name_of_dog"),
Ok(Selector::Dot(
Box::new(Selector::Root),
DotArg::Field("name_of_dog".into())
))
);
assert_eq!(
parse_selector("$._name_of_dog"),
Ok(Selector::Dot(
Box::new(Selector::Root),
DotArg::Field("_name_of_dog".into())
))
);
}
#[test]
fn field_names_can_be_single_quoted_inside_array_notation() {
assert_eq!(
parse_selector("$['name']"),
Ok(Selector::Index(
Box::new(Selector::Root),
IndexArg::Field("name".into())
))
);
}
#[test]
fn field_names_can_be_double_quoted_inside_array_notation() {
assert_eq!(
parse_selector("$[\"name\"]"),
Ok(Selector::Index(
Box::new(Selector::Root),
IndexArg::Field("name".into())
))
);
}
#[test]
fn quoted_field_names_can_contain_whitespace() {
assert_eq!(
parse_selector("$[\" n a m e \"]"),
Ok(Selector::Index(
Box::new(Selector::Root),
IndexArg::Field(" n a m e ".into())
))
);
}
#[test]
fn is_whitespace_tolerant() {
assert_eq!(
parse_selector(" $ [ * ] . age "),
Ok(Selector::Dot(
Box::new(Selector::Index(
Box::new(Selector::Root),
IndexArg::Wildcard
)),
DotArg::Field("age".into())
))
);
}
#[test]
fn index_arg_field_allows_periods() {
assert_eq!(
parse_selector("$['foo.bar']"),
Ok(Selector::Index(
Box::new(Selector::Root),
IndexArg::Field("foo.bar".into())
))
);
}
#[test]
fn fields_that_look_like_selectors() {
assert_eq!(
parse_selector("$['foo.bar.*']"),
Ok(Selector::Index(
Box::new(Selector::Root),
IndexArg::Field("foo.bar.*".into())
))
);
}
}