mod accessor;
mod construction;
mod primitives;
use nom::{
Parser,
branch::alt,
character::complete::{char, multispace0},
combinator::all_consuming,
multi::many0,
sequence::{delimited, preceded},
};
use simd_json::OwnedValue as Value;
use simd_json::StaticNode;
use super::ast::{Filter, FunctionCall};
use super::builtins::Builtin;
use crate::error::ParseError;
pub fn parse(input: &str) -> Result<Filter, ParseError> {
match all_consuming(parse_filter).parse(input) {
Ok((_, filter)) => Ok(filter),
Err(e) => {
let (position, message) = match &e {
nom::Err::Error(e) | nom::Err::Failure(e) => {
let pos = input.len() - e.input.len();
let msg = if e.input.is_empty() {
"unexpected end of input".to_string()
} else {
format!("unexpected '{}'", e.input.chars().next().unwrap_or('?'))
};
(pos, msg)
}
nom::Err::Incomplete(_) => (input.len(), "incomplete input".to_string()),
};
Err(ParseError { message, position })
}
}
}
fn parse_filter(input: &str) -> nom::IResult<&str, Filter> {
delimited(multispace0, parse_pipe_chain, multispace0).parse(input)
}
pub(crate) fn parse_pipe_chain(input: &str) -> nom::IResult<&str, Filter> {
let (input, first) = parse_alternative_expr(input)?;
let (input, rest) = many0(preceded(
delimited(multispace0, char('|'), multispace0),
parse_alternative_expr,
))
.parse(input)?;
let filter = rest
.into_iter()
.fold(first, |acc, f| Filter::Pipe(Box::new(acc), Box::new(f)));
Ok((input, filter))
}
fn parse_alternative_expr(input: &str) -> nom::IResult<&str, Filter> {
use nom::bytes::complete::tag;
let (input, first) = parse_or_expr(input)?;
let (input, rest) = many0(preceded(
delimited(multispace0, tag("//"), multispace0),
parse_or_expr,
))
.parse(input)?;
let filter = rest.into_iter().fold(first, |acc, f| {
Filter::Alternative(Box::new(acc), Box::new(f))
});
Ok((input, filter))
}
fn parse_or_expr(input: &str) -> nom::IResult<&str, Filter> {
use super::ast::BoolOp;
use nom::bytes::complete::tag;
let (input, first) = parse_and_expr(input)?;
let (input, rest) = many0(preceded(
delimited(multispace0, tag("or"), multispace0),
parse_and_expr,
))
.parse(input)?;
let filter = rest.into_iter().fold(first, |acc, f| {
Filter::BoolOp(Box::new(acc), BoolOp::Or, Box::new(f))
});
Ok((input, filter))
}
fn parse_and_expr(input: &str) -> nom::IResult<&str, Filter> {
use super::ast::BoolOp;
use nom::bytes::complete::tag;
let (input, first) = parse_comparison_expr(input)?;
let (input, rest) = many0(preceded(
delimited(multispace0, tag("and"), multispace0),
parse_comparison_expr,
))
.parse(input)?;
let filter = rest.into_iter().fold(first, |acc, f| {
Filter::BoolOp(Box::new(acc), BoolOp::And, Box::new(f))
});
Ok((input, filter))
}
fn parse_comparison_expr(input: &str) -> nom::IResult<&str, Filter> {
let (input, left) = parse_filter_term(input)?;
let (input, _) = multispace0(input)?;
if let Ok((input, op)) = primitives::parse_compare_op(input) {
let (input, _) = multispace0(input)?;
let (input, right) = parse_filter_term(input)?;
Ok((input, Filter::Compare(Box::new(left), op, Box::new(right))))
} else {
Ok((input, left))
}
}
fn parse_filter_term(input: &str) -> nom::IResult<&str, Filter> {
alt((
construction::parse_array_construction,
construction::parse_object_construction,
parse_if_then_else,
parse_function_call,
parse_literal,
accessor::parse_accessor_chain,
parse_builtin,
))
.parse(input)
}
fn parse_if_then_else(input: &str) -> nom::IResult<&str, Filter> {
use nom::bytes::complete::tag;
let (rest, _) = tag("if").parse(input)?;
if rest
.chars()
.next()
.is_some_and(|c| c.is_alphanumeric() || c == '_')
{
return Err(nom::Err::Error(nom::error::Error::new(
input,
nom::error::ErrorKind::Tag,
)));
}
let (rest, _) = multispace0(rest)?;
let (rest, condition) = parse_pipe_chain(rest)?;
let (rest, _) = multispace0(rest)?;
let (rest, _) = tag("then").parse(rest)?;
let (rest, _) = multispace0(rest)?;
let (rest, then_branch) = parse_pipe_chain(rest)?;
let (rest, _) = multispace0(rest)?;
let (rest, _) = tag("else").parse(rest)?;
let (rest, _) = multispace0(rest)?;
let (rest, else_branch) = parse_pipe_chain(rest)?;
let (rest, _) = multispace0(rest)?;
let (rest, _) = tag("end").parse(rest)?;
Ok((
rest,
Filter::IfThenElse {
condition: Box::new(condition),
then_branch: Box::new(then_branch),
else_branch: Box::new(else_branch),
},
))
}
fn parse_literal(input: &str) -> nom::IResult<&str, Filter> {
alt((
parse_null_literal,
parse_bool_literal,
parse_string_literal,
parse_number_literal,
))
.parse(input)
}
fn parse_null_literal(input: &str) -> nom::IResult<&str, Filter> {
use nom::bytes::complete::tag;
let (rest, _) = tag("null").parse(input)?;
if rest
.chars()
.next()
.is_some_and(|c| c.is_alphanumeric() || c == '_')
{
return Err(nom::Err::Error(nom::error::Error::new(
input,
nom::error::ErrorKind::Tag,
)));
}
Ok((rest, Filter::Literal(Value::Static(StaticNode::Null))))
}
fn parse_bool_literal(input: &str) -> nom::IResult<&str, Filter> {
use nom::bytes::complete::tag;
let true_parser = tag("true").map(|_| Filter::Literal(Value::Static(StaticNode::Bool(true))));
let false_parser =
tag("false").map(|_| Filter::Literal(Value::Static(StaticNode::Bool(false))));
let (rest, filter) = alt((true_parser, false_parser)).parse(input)?;
if rest
.chars()
.next()
.is_some_and(|c| c.is_alphanumeric() || c == '_')
{
return Err(nom::Err::Error(nom::error::Error::new(
input,
nom::error::ErrorKind::Tag,
)));
}
Ok((rest, filter))
}
fn parse_string_literal(input: &str) -> nom::IResult<&str, Filter> {
let (rest, s) = primitives::parse_string_literal(input)?;
Ok((rest, Filter::Literal(Value::String(s))))
}
fn parse_number_literal(input: &str) -> nom::IResult<&str, Filter> {
let (rest, n) = primitives::parse_number(input)?;
Ok((rest, Filter::Literal(Value::Static(n))))
}
fn parse_function_call(input: &str) -> nom::IResult<&str, Filter> {
let (rest, name) = primitives::identifier(input)?;
if !matches!(
name.as_str(),
"map"
| "select"
| "sort_by"
| "group_by"
| "unique_by"
| "min_by"
| "max_by"
| "has"
| "split"
| "join"
| "startswith"
| "endswith"
| "contains"
| "with_entries"
) {
return Err(nom::Err::Error(nom::error::Error::new(
input,
nom::error::ErrorKind::Tag,
)));
}
let (rest, _) = multispace0(rest)?;
let (rest, _) = char('(').parse(rest)?;
let (rest, _) = multispace0(rest)?;
if matches!(
name.as_str(),
"has" | "split" | "join" | "startswith" | "endswith"
) {
let (rest, arg) = primitives::parse_string_literal(rest)?;
let (rest, _) = multispace0(rest)?;
let (rest, _) = char(')').parse(rest)?;
let func = match name.as_str() {
"has" => FunctionCall::Has(arg),
"split" => FunctionCall::Split(arg),
"join" => FunctionCall::Join(arg),
"startswith" => FunctionCall::StartsWith(arg),
"endswith" => FunctionCall::EndsWith(arg),
_ => unreachable!(),
};
return Ok((rest, Filter::Function(func)));
}
let (rest, inner) = parse_pipe_chain(rest)?;
let (rest, _) = multispace0(rest)?;
let (rest, _) = char(')').parse(rest)?;
let func = match name.as_str() {
"map" => FunctionCall::Map(Box::new(inner)),
"select" => FunctionCall::Select(Box::new(inner)),
"sort_by" => FunctionCall::SortBy(Box::new(inner)),
"group_by" => FunctionCall::GroupBy(Box::new(inner)),
"unique_by" => FunctionCall::UniqueBy(Box::new(inner)),
"min_by" => FunctionCall::MinBy(Box::new(inner)),
"max_by" => FunctionCall::MaxBy(Box::new(inner)),
"contains" => FunctionCall::Contains(Box::new(inner)),
"with_entries" => FunctionCall::WithEntries(Box::new(inner)),
_ => unreachable!(),
};
Ok((rest, Filter::Function(func)))
}
fn parse_builtin(input: &str) -> nom::IResult<&str, Filter> {
let (rest, name) = primitives::identifier(input)?;
match Builtin::from_name(&name) {
Some(b) => Ok((rest, Filter::Builtin(b))),
None => Err(nom::Err::Error(nom::error::Error::new(
input,
nom::error::ErrorKind::Tag,
))),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::filter::ast::ObjectKey;
#[test]
fn test_identity() {
assert_eq!(parse("."), Ok(Filter::Identity));
}
#[test]
fn test_identity_with_whitespace() {
assert_eq!(parse(" . "), Ok(Filter::Identity));
}
#[test]
fn test_field() {
assert_eq!(parse(".foo"), Ok(Filter::Field("foo".to_string())));
assert_eq!(parse(".name"), Ok(Filter::Field("name".to_string())));
}
#[test]
fn test_field_with_whitespace() {
assert_eq!(parse(" .foo "), Ok(Filter::Field("foo".to_string())));
}
#[test]
fn test_field_chain() {
assert_eq!(
parse(".foo.bar"),
Ok(Filter::Pipe(
Box::new(Filter::Field("foo".to_string())),
Box::new(Filter::Field("bar".to_string()))
))
);
}
#[test]
fn test_field_chain_triple() {
assert_eq!(
parse(".foo.bar.baz"),
Ok(Filter::Pipe(
Box::new(Filter::Pipe(
Box::new(Filter::Field("foo".to_string())),
Box::new(Filter::Field("bar".to_string()))
)),
Box::new(Filter::Field("baz".to_string()))
))
);
}
#[test]
fn test_underscore_field() {
assert_eq!(
parse("._private"),
Ok(Filter::Field("_private".to_string()))
);
}
#[test]
fn test_field_with_numbers() {
assert_eq!(
parse(".field123"),
Ok(Filter::Field("field123".to_string()))
);
}
#[test]
fn test_unicode_field() {
assert_eq!(parse(".名前"), Ok(Filter::Field("名前".to_string())));
}
#[test]
fn test_keyword_as_field() {
assert_eq!(parse(".null"), Ok(Filter::Field("null".to_string())));
assert_eq!(parse(".true"), Ok(Filter::Field("true".to_string())));
assert_eq!(parse(".false"), Ok(Filter::Field("false".to_string())));
}
#[test]
fn test_index() {
assert_eq!(parse(".[0]"), Ok(Filter::Index(0)));
assert_eq!(parse(".[1]"), Ok(Filter::Index(1)));
assert_eq!(parse(".[42]"), Ok(Filter::Index(42)));
}
#[test]
fn test_negative_index() {
assert_eq!(parse(".[-1]"), Ok(Filter::Index(-1)));
assert_eq!(parse(".[-10]"), Ok(Filter::Index(-10)));
}
#[test]
fn test_index_with_whitespace() {
assert_eq!(parse(".[ 0 ]"), Ok(Filter::Index(0)));
assert_eq!(parse(".[ -1 ]"), Ok(Filter::Index(-1)));
}
#[test]
fn test_iterate() {
assert_eq!(parse(".[]"), Ok(Filter::Iterate));
}
#[test]
fn test_iterate_with_whitespace() {
assert_eq!(parse(".[ ]"), Ok(Filter::Iterate));
}
#[test]
fn test_quoted_field() {
assert_eq!(parse(".[\"foo\"]"), Ok(Filter::Field("foo".to_string())));
}
#[test]
fn test_quoted_field_special_chars() {
assert_eq!(
parse(".[\"foo-bar\"]"),
Ok(Filter::Field("foo-bar".to_string()))
);
assert_eq!(
parse(".[\"with space\"]"),
Ok(Filter::Field("with space".to_string()))
);
assert_eq!(parse(".[\"a.b\"]"), Ok(Filter::Field("a.b".to_string())));
}
#[test]
fn test_quoted_field_empty() {
assert_eq!(parse(".[\"\"]"), Ok(Filter::Field("".to_string())));
}
#[test]
fn test_quoted_field_unicode() {
assert_eq!(
parse(".[\"日本語\"]"),
Ok(Filter::Field("日本語".to_string()))
);
}
#[test]
fn test_quoted_field_with_whitespace() {
assert_eq!(parse(".[ \"foo\" ]"), Ok(Filter::Field("foo".to_string())));
}
#[test]
fn test_quoted_field_escapes() {
assert_eq!(parse(".[\"a\\nb\"]"), Ok(Filter::Field("a\nb".to_string())));
assert_eq!(parse(".[\"a\\tb\"]"), Ok(Filter::Field("a\tb".to_string())));
assert_eq!(
parse(".[\"a\\\"b\"]"),
Ok(Filter::Field("a\"b".to_string()))
);
assert_eq!(
parse(".[\"a\\\\b\"]"),
Ok(Filter::Field("a\\b".to_string()))
);
}
#[test]
fn test_quoted_field_unicode_escape() {
assert_eq!(parse(".[\"\\u0041\"]"), Ok(Filter::Field("A".to_string())));
}
#[test]
fn test_chain_field_index() {
assert_eq!(
parse(".foo[0]"),
Ok(Filter::Pipe(
Box::new(Filter::Field("foo".to_string())),
Box::new(Filter::Index(0))
))
);
}
#[test]
fn test_chain_index_field() {
assert_eq!(
parse(".[0].foo"),
Ok(Filter::Pipe(
Box::new(Filter::Index(0)),
Box::new(Filter::Field("foo".to_string()))
))
);
}
#[test]
fn test_chain_index_index() {
assert_eq!(
parse(".[0][1]"),
Ok(Filter::Pipe(
Box::new(Filter::Index(0)),
Box::new(Filter::Index(1))
))
);
}
#[test]
fn test_chain_iterate_index() {
assert_eq!(
parse(".[][0]"),
Ok(Filter::Pipe(
Box::new(Filter::Iterate),
Box::new(Filter::Index(0))
))
);
}
#[test]
fn test_chain_quoted_quoted() {
assert_eq!(
parse(".[\"a\"][\"b\"]"),
Ok(Filter::Pipe(
Box::new(Filter::Field("a".to_string())),
Box::new(Filter::Field("b".to_string()))
))
);
}
#[test]
fn test_chain_mixed() {
assert_eq!(
parse(".items[0].id"),
Ok(Filter::Pipe(
Box::new(Filter::Pipe(
Box::new(Filter::Field("items".to_string())),
Box::new(Filter::Index(0))
)),
Box::new(Filter::Field("id".to_string()))
))
);
}
#[test]
fn test_chain_iterate_field() {
assert_eq!(
parse(".items[].id"),
Ok(Filter::Pipe(
Box::new(Filter::Pipe(
Box::new(Filter::Field("items".to_string())),
Box::new(Filter::Iterate)
)),
Box::new(Filter::Field("id".to_string()))
))
);
}
#[test]
fn test_chain_mixed_dot_bracket() {
assert_eq!(
parse(".foo[\"bar\"].baz"),
Ok(Filter::Pipe(
Box::new(Filter::Pipe(
Box::new(Filter::Field("foo".to_string())),
Box::new(Filter::Field("bar".to_string()))
)),
Box::new(Filter::Field("baz".to_string()))
))
);
}
#[test]
fn test_trailing_dot_error() {
assert!(parse(".foo.").is_err());
}
#[test]
fn test_digit_start_error() {
assert!(parse(".123").is_err());
}
#[test]
fn test_empty() {
assert!(parse("").is_err());
}
#[test]
fn test_invalid() {
assert!(parse("..").is_err());
assert!(parse("foo").is_err());
}
#[test]
fn test_unclosed_bracket() {
assert!(parse(".[").is_err());
assert!(parse(".[0").is_err());
}
#[test]
fn test_unclosed_string() {
assert!(parse(".[\"unclosed").is_err());
}
#[test]
fn test_single_quotes_error() {
assert!(parse(".['field']").is_err());
}
#[test]
fn test_float_error() {
assert!(parse(".[0.5]").is_err());
}
#[test]
fn test_integer_overflow() {
assert!(parse(".[99999999999999999999]").is_err());
}
#[test]
fn test_pipe_simple() {
assert_eq!(
parse(".foo | .bar"),
Ok(Filter::Pipe(
Box::new(Filter::Field("foo".to_string())),
Box::new(Filter::Field("bar".to_string()))
))
);
}
#[test]
fn test_pipe_with_whitespace() {
assert_eq!(
parse(".foo|.bar"),
Ok(Filter::Pipe(
Box::new(Filter::Field("foo".to_string())),
Box::new(Filter::Field("bar".to_string()))
))
);
assert_eq!(
parse(".foo | .bar"),
Ok(Filter::Pipe(
Box::new(Filter::Field("foo".to_string())),
Box::new(Filter::Field("bar".to_string()))
))
);
}
#[test]
fn test_pipe_triple() {
assert_eq!(
parse(".a | .b | .c"),
Ok(Filter::Pipe(
Box::new(Filter::Pipe(
Box::new(Filter::Field("a".to_string())),
Box::new(Filter::Field("b".to_string()))
)),
Box::new(Filter::Field("c".to_string()))
))
);
}
#[test]
fn test_pipe_with_iterate() {
assert_eq!(
parse(".users | .[]"),
Ok(Filter::Pipe(
Box::new(Filter::Field("users".to_string())),
Box::new(Filter::Iterate)
))
);
}
#[test]
fn test_pipe_with_index() {
assert_eq!(
parse(".items | .[0]"),
Ok(Filter::Pipe(
Box::new(Filter::Field("items".to_string())),
Box::new(Filter::Index(0))
))
);
}
#[test]
fn test_pipe_mixed_implicit_explicit() {
assert_eq!(
parse(".users[].name | .foo"),
Ok(Filter::Pipe(
Box::new(Filter::Pipe(
Box::new(Filter::Pipe(
Box::new(Filter::Field("users".to_string())),
Box::new(Filter::Iterate)
)),
Box::new(Filter::Field("name".to_string()))
)),
Box::new(Filter::Field("foo".to_string()))
))
);
}
#[test]
fn test_pipe_identity() {
assert_eq!(
parse(". | .foo"),
Ok(Filter::Pipe(
Box::new(Filter::Identity),
Box::new(Filter::Field("foo".to_string()))
))
);
}
#[test]
fn test_pipe_trailing_error() {
assert!(parse(".foo |").is_err());
}
#[test]
fn test_pipe_leading_error() {
assert!(parse("| .foo").is_err());
}
#[test]
fn test_pipe_double_error() {
assert!(parse(".foo || .bar").is_err());
}
#[test]
fn test_pipe_empty_segment_error() {
assert!(parse(".foo | | .bar").is_err());
}
#[test]
fn test_builtin_standalone() {
assert_eq!(parse("length"), Ok(Filter::Builtin(Builtin::Length)));
assert_eq!(parse("keys"), Ok(Filter::Builtin(Builtin::Keys)));
assert_eq!(parse("type"), Ok(Filter::Builtin(Builtin::Type)));
}
#[test]
fn test_builtin_with_whitespace() {
assert_eq!(parse(" length "), Ok(Filter::Builtin(Builtin::Length)));
}
#[test]
fn test_builtin_after_pipe() {
assert_eq!(
parse(".foo | length"),
Ok(Filter::Pipe(
Box::new(Filter::Field("foo".to_string())),
Box::new(Filter::Builtin(Builtin::Length))
))
);
}
#[test]
fn test_builtin_before_pipe() {
assert_eq!(
parse("length | .foo"),
Ok(Filter::Pipe(
Box::new(Filter::Builtin(Builtin::Length)),
Box::new(Filter::Field("foo".to_string()))
))
);
}
#[test]
fn test_builtin_chain() {
assert_eq!(
parse(".items | length"),
Ok(Filter::Pipe(
Box::new(Filter::Field("items".to_string())),
Box::new(Filter::Builtin(Builtin::Length))
))
);
}
#[test]
fn test_iterate_then_builtin() {
assert_eq!(
parse(".[] | length"),
Ok(Filter::Pipe(
Box::new(Filter::Iterate),
Box::new(Filter::Builtin(Builtin::Length))
))
);
}
#[test]
fn test_builtin_then_iterate() {
assert_eq!(
parse("keys | .[]"),
Ok(Filter::Pipe(
Box::new(Filter::Builtin(Builtin::Keys)),
Box::new(Filter::Iterate)
))
);
}
#[test]
fn test_multiple_builtins() {
assert_eq!(
parse("sort | reverse"),
Ok(Filter::Pipe(
Box::new(Filter::Builtin(Builtin::Sort)),
Box::new(Filter::Builtin(Builtin::Reverse))
))
);
}
#[test]
fn test_all_builtins_parse() {
assert_eq!(parse("length"), Ok(Filter::Builtin(Builtin::Length)));
assert_eq!(parse("keys"), Ok(Filter::Builtin(Builtin::Keys)));
assert_eq!(parse("values"), Ok(Filter::Builtin(Builtin::Values)));
assert_eq!(parse("type"), Ok(Filter::Builtin(Builtin::Type)));
assert_eq!(parse("first"), Ok(Filter::Builtin(Builtin::First)));
assert_eq!(parse("last"), Ok(Filter::Builtin(Builtin::Last)));
assert_eq!(parse("reverse"), Ok(Filter::Builtin(Builtin::Reverse)));
assert_eq!(parse("sort"), Ok(Filter::Builtin(Builtin::Sort)));
assert_eq!(parse("min"), Ok(Filter::Builtin(Builtin::Min)));
assert_eq!(parse("max"), Ok(Filter::Builtin(Builtin::Max)));
assert_eq!(parse("unique"), Ok(Filter::Builtin(Builtin::Unique)));
assert_eq!(parse("flatten"), Ok(Filter::Builtin(Builtin::Flatten)));
assert_eq!(parse("add"), Ok(Filter::Builtin(Builtin::Add)));
assert_eq!(parse("empty"), Ok(Filter::Builtin(Builtin::Empty)));
assert_eq!(parse("not"), Ok(Filter::Builtin(Builtin::Not)));
}
#[test]
fn test_unknown_identifier_error() {
assert!(parse("notabuiltin").is_err());
}
#[test]
fn test_builtin_as_field_name() {
assert_eq!(parse(".length"), Ok(Filter::Field("length".to_string())));
}
#[test]
fn test_array_construction_simple() {
assert_eq!(
parse("[.foo, .bar]"),
Ok(Filter::Array(vec![
Filter::Field("foo".to_string()),
Filter::Field("bar".to_string()),
]))
);
}
#[test]
fn test_array_construction_empty() {
assert_eq!(parse("[]"), Ok(Filter::Array(vec![])));
}
#[test]
fn test_array_construction_single() {
assert_eq!(
parse("[.foo]"),
Ok(Filter::Array(vec![Filter::Field("foo".to_string())]))
);
}
#[test]
fn test_array_construction_with_pipe() {
assert_eq!(
parse("[.foo | length]"),
Ok(Filter::Array(vec![Filter::Pipe(
Box::new(Filter::Field("foo".to_string())),
Box::new(Filter::Builtin(Builtin::Length))
)]))
);
}
#[test]
fn test_array_construction_with_iterate() {
assert_eq!(parse("[.[]]"), Ok(Filter::Array(vec![Filter::Iterate])));
}
#[test]
fn test_array_construction_nested() {
assert_eq!(
parse("[[.x], [.y]]"),
Ok(Filter::Array(vec![
Filter::Array(vec![Filter::Field("x".to_string())]),
Filter::Array(vec![Filter::Field("y".to_string())]),
]))
);
}
#[test]
fn test_array_construction_with_whitespace() {
assert_eq!(
parse("[ .foo , .bar ]"),
Ok(Filter::Array(vec![
Filter::Field("foo".to_string()),
Filter::Field("bar".to_string()),
]))
);
}
#[test]
fn test_object_construction_simple() {
assert_eq!(
parse("{name: .foo}"),
Ok(Filter::Object(vec![(
ObjectKey::Static("name".to_string()),
Filter::Field("foo".to_string())
)]))
);
}
#[test]
fn test_object_construction_empty() {
assert_eq!(parse("{}"), Ok(Filter::Object(vec![])));
}
#[test]
fn test_object_construction_multiple() {
assert_eq!(
parse("{name: .foo, age: .bar}"),
Ok(Filter::Object(vec![
(
ObjectKey::Static("name".to_string()),
Filter::Field("foo".to_string())
),
(
ObjectKey::Static("age".to_string()),
Filter::Field("bar".to_string())
),
]))
);
}
#[test]
fn test_object_construction_quoted_key() {
assert_eq!(
parse("{\"my-key\": .foo}"),
Ok(Filter::Object(vec![(
ObjectKey::Static("my-key".to_string()),
Filter::Field("foo".to_string())
)]))
);
}
#[test]
fn test_object_construction_dynamic_key() {
assert_eq!(
parse("{(.key): .value}"),
Ok(Filter::Object(vec![(
ObjectKey::Dynamic(Box::new(Filter::Field("key".to_string()))),
Filter::Field("value".to_string())
)]))
);
}
#[test]
fn test_object_construction_with_pipe() {
assert_eq!(
parse("{count: .items | length}"),
Ok(Filter::Object(vec![(
ObjectKey::Static("count".to_string()),
Filter::Pipe(
Box::new(Filter::Field("items".to_string())),
Box::new(Filter::Builtin(Builtin::Length))
)
)]))
);
}
#[test]
fn test_object_construction_with_whitespace() {
assert_eq!(
parse("{ name : .foo , age : .bar }"),
Ok(Filter::Object(vec![
(
ObjectKey::Static("name".to_string()),
Filter::Field("foo".to_string())
),
(
ObjectKey::Static("age".to_string()),
Filter::Field("bar".to_string())
),
]))
);
}
#[test]
fn test_nested_object_in_array() {
assert_eq!(
parse("[{a: .x}]"),
Ok(Filter::Array(vec![Filter::Object(vec![(
ObjectKey::Static("a".to_string()),
Filter::Field("x".to_string())
)])]))
);
}
#[test]
fn test_array_in_object() {
assert_eq!(
parse("{point: [.x, .y]}"),
Ok(Filter::Object(vec![(
ObjectKey::Static("point".to_string()),
Filter::Array(vec![
Filter::Field("x".to_string()),
Filter::Field("y".to_string()),
])
)]))
);
}
#[test]
fn test_optional_field() {
assert_eq!(
parse(".foo?"),
Ok(Filter::Optional(Box::new(Filter::Field("foo".to_string()))))
);
}
#[test]
fn test_optional_index() {
assert_eq!(
parse(".[0]?"),
Ok(Filter::Optional(Box::new(Filter::Index(0))))
);
}
#[test]
fn test_optional_iterate() {
assert_eq!(
parse(".[]?"),
Ok(Filter::Optional(Box::new(Filter::Iterate)))
);
}
#[test]
fn test_optional_chained() {
assert_eq!(
parse(".foo?.bar"),
Ok(Filter::Pipe(
Box::new(Filter::Optional(Box::new(Filter::Field("foo".to_string())))),
Box::new(Filter::Field("bar".to_string()))
))
);
}
#[test]
fn test_optional_both() {
assert_eq!(
parse(".foo?.bar?"),
Ok(Filter::Pipe(
Box::new(Filter::Optional(Box::new(Filter::Field("foo".to_string())))),
Box::new(Filter::Optional(Box::new(Filter::Field("bar".to_string()))))
))
);
}
#[test]
fn test_optional_quoted_field() {
assert_eq!(
parse(".[\"foo\"]?"),
Ok(Filter::Optional(Box::new(Filter::Field("foo".to_string()))))
);
}
#[test]
fn test_slice_both() {
assert_eq!(parse(".[2:5]"), Ok(Filter::Slice(Some(2), Some(5))));
}
#[test]
fn test_slice_start_only() {
assert_eq!(parse(".[2:]"), Ok(Filter::Slice(Some(2), None)));
}
#[test]
fn test_slice_end_only() {
assert_eq!(parse(".[:5]"), Ok(Filter::Slice(None, Some(5))));
}
#[test]
fn test_slice_neither() {
assert_eq!(parse(".[:]"), Ok(Filter::Slice(None, None)));
}
#[test]
fn test_slice_negative_start() {
assert_eq!(parse(".[-2:]"), Ok(Filter::Slice(Some(-2), None)));
}
#[test]
fn test_slice_negative_end() {
assert_eq!(parse(".[:-1]"), Ok(Filter::Slice(None, Some(-1))));
}
#[test]
fn test_slice_both_negative() {
assert_eq!(parse(".[-3:-1]"), Ok(Filter::Slice(Some(-3), Some(-1))));
}
#[test]
fn test_slice_with_whitespace() {
assert_eq!(parse(".[ 2 : 5 ]"), Ok(Filter::Slice(Some(2), Some(5))));
}
#[test]
fn test_slice_chained() {
assert_eq!(
parse(".[2:5][0]"),
Ok(Filter::Pipe(
Box::new(Filter::Slice(Some(2), Some(5))),
Box::new(Filter::Index(0))
))
);
}
#[test]
fn test_map_basic() {
assert_eq!(
parse("map(.foo)"),
Ok(Filter::Function(FunctionCall::Map(Box::new(
Filter::Field("foo".to_string())
))))
);
}
#[test]
fn test_map_with_pipe() {
assert_eq!(
parse("map(.a | .b)"),
Ok(Filter::Function(FunctionCall::Map(Box::new(Filter::Pipe(
Box::new(Filter::Field("a".to_string())),
Box::new(Filter::Field("b".to_string()))
)))))
);
}
#[test]
fn test_map_identity() {
assert_eq!(
parse("map(.)"),
Ok(Filter::Function(FunctionCall::Map(Box::new(
Filter::Identity
))))
);
}
#[test]
fn test_map_with_whitespace() {
assert_eq!(
parse("map( .foo )"),
Ok(Filter::Function(FunctionCall::Map(Box::new(
Filter::Field("foo".to_string())
))))
);
}
#[test]
fn test_select_basic() {
assert_eq!(
parse("select(.active)"),
Ok(Filter::Function(FunctionCall::Select(Box::new(
Filter::Field("active".to_string())
))))
);
}
#[test]
fn test_select_with_pipe() {
assert_eq!(
parse("select(.a | .b)"),
Ok(Filter::Function(FunctionCall::Select(Box::new(
Filter::Pipe(
Box::new(Filter::Field("a".to_string())),
Box::new(Filter::Field("b".to_string()))
)
))))
);
}
#[test]
fn test_function_in_pipe_chain() {
assert_eq!(
parse(".items | map(.name)"),
Ok(Filter::Pipe(
Box::new(Filter::Field("items".to_string())),
Box::new(Filter::Function(FunctionCall::Map(Box::new(
Filter::Field("name".to_string())
))))
))
);
}
#[test]
fn test_map_without_parens_fails() {
assert!(parse("map").is_err());
}
#[test]
fn test_select_without_parens_fails() {
assert!(parse("select").is_err());
}
#[test]
fn test_map_nested_in_array() {
assert_eq!(
parse("[map(.x)]"),
Ok(Filter::Array(vec![Filter::Function(FunctionCall::Map(
Box::new(Filter::Field("x".to_string()))
))]))
);
}
#[test]
fn test_sort_by_basic() {
assert_eq!(
parse("sort_by(.age)"),
Ok(Filter::Function(FunctionCall::SortBy(Box::new(
Filter::Field("age".to_string())
))))
);
}
#[test]
fn test_group_by_basic() {
assert_eq!(
parse("group_by(.type)"),
Ok(Filter::Function(FunctionCall::GroupBy(Box::new(
Filter::Field("type".to_string())
))))
);
}
#[test]
fn test_unique_by_basic() {
assert_eq!(
parse("unique_by(.id)"),
Ok(Filter::Function(FunctionCall::UniqueBy(Box::new(
Filter::Field("id".to_string())
))))
);
}
#[test]
fn test_min_by_basic() {
assert_eq!(
parse("min_by(.score)"),
Ok(Filter::Function(FunctionCall::MinBy(Box::new(
Filter::Field("score".to_string())
))))
);
}
#[test]
fn test_max_by_basic() {
assert_eq!(
parse("max_by(.score)"),
Ok(Filter::Function(FunctionCall::MaxBy(Box::new(
Filter::Field("score".to_string())
))))
);
}
#[test]
fn test_sort_by_in_pipe() {
assert_eq!(
parse(".items | sort_by(.name)"),
Ok(Filter::Pipe(
Box::new(Filter::Field("items".to_string())),
Box::new(Filter::Function(FunctionCall::SortBy(Box::new(
Filter::Field("name".to_string())
))))
))
);
}
#[test]
fn test_if_then_else_basic() {
assert_eq!(
parse("if .x then .a else .b end"),
Ok(Filter::IfThenElse {
condition: Box::new(Filter::Field("x".to_string())),
then_branch: Box::new(Filter::Field("a".to_string())),
else_branch: Box::new(Filter::Field("b".to_string())),
})
);
}
#[test]
fn test_if_then_else_with_comparison() {
use super::super::ast::CompareOp;
assert_eq!(
parse("if . > 0 then \"pos\" else \"neg\" end"),
Ok(Filter::IfThenElse {
condition: Box::new(Filter::Compare(
Box::new(Filter::Identity),
CompareOp::Gt,
Box::new(Filter::Literal(simd_json::json!(0)))
)),
then_branch: Box::new(Filter::Literal(simd_json::json!("pos"))),
else_branch: Box::new(Filter::Literal(simd_json::json!("neg"))),
})
);
}
#[test]
fn test_if_then_else_with_whitespace() {
assert!(parse("if . then . else . end").is_ok());
assert!(parse("if . then . else . end").is_ok());
}
#[test]
fn test_alternative_basic() {
assert_eq!(
parse(".x // \"default\""),
Ok(Filter::Alternative(
Box::new(Filter::Field("x".to_string())),
Box::new(Filter::Literal(simd_json::json!("default")))
))
);
}
#[test]
fn test_alternative_chain() {
assert_eq!(
parse(".a // .b // .c"),
Ok(Filter::Alternative(
Box::new(Filter::Alternative(
Box::new(Filter::Field("a".to_string())),
Box::new(Filter::Field("b".to_string()))
)),
Box::new(Filter::Field("c".to_string()))
))
);
}
#[test]
fn test_alternative_in_pipe() {
assert_eq!(
parse(".x | . // 0"),
Ok(Filter::Pipe(
Box::new(Filter::Field("x".to_string())),
Box::new(Filter::Alternative(
Box::new(Filter::Identity),
Box::new(Filter::Literal(simd_json::json!(0)))
))
))
);
}
#[test]
fn test_has_basic() {
assert_eq!(
parse("has(\"name\")"),
Ok(Filter::Function(FunctionCall::Has("name".to_string())))
);
}
#[test]
fn test_has_in_select() {
assert_eq!(
parse("select(has(\"x\"))"),
Ok(Filter::Function(FunctionCall::Select(Box::new(
Filter::Function(FunctionCall::Has("x".to_string()))
))))
);
}
#[test]
fn test_literal_number_int() {
assert_eq!(parse("5"), Ok(Filter::Literal(simd_json::json!(5))));
}
#[test]
fn test_literal_number_negative() {
assert_eq!(parse("-42"), Ok(Filter::Literal(simd_json::json!(-42))));
}
#[test]
fn test_literal_number_float() {
assert_eq!(parse("3.15"), Ok(Filter::Literal(simd_json::json!(3.15))));
}
#[test]
fn test_literal_string() {
assert_eq!(
parse("\"hello\""),
Ok(Filter::Literal(simd_json::json!("hello")))
);
}
#[test]
fn test_literal_true() {
assert_eq!(parse("true"), Ok(Filter::Literal(simd_json::json!(true))));
}
#[test]
fn test_literal_false() {
assert_eq!(parse("false"), Ok(Filter::Literal(simd_json::json!(false))));
}
#[test]
fn test_literal_null() {
assert_eq!(parse("null"), Ok(Filter::Literal(simd_json::json!(null))));
}
#[test]
fn test_compare_eq() {
use super::super::ast::CompareOp;
assert_eq!(
parse(". == 5"),
Ok(Filter::Compare(
Box::new(Filter::Identity),
CompareOp::Eq,
Box::new(Filter::Literal(simd_json::json!(5)))
))
);
}
#[test]
fn test_compare_ne() {
use super::super::ast::CompareOp;
assert_eq!(
parse(".x != \"foo\""),
Ok(Filter::Compare(
Box::new(Filter::Field("x".to_string())),
CompareOp::Ne,
Box::new(Filter::Literal(simd_json::json!("foo")))
))
);
}
#[test]
fn test_compare_lt() {
use super::super::ast::CompareOp;
assert_eq!(
parse(".age < 18"),
Ok(Filter::Compare(
Box::new(Filter::Field("age".to_string())),
CompareOp::Lt,
Box::new(Filter::Literal(simd_json::json!(18)))
))
);
}
#[test]
fn test_compare_le() {
use super::super::ast::CompareOp;
assert_eq!(
parse(". <= 100"),
Ok(Filter::Compare(
Box::new(Filter::Identity),
CompareOp::Le,
Box::new(Filter::Literal(simd_json::json!(100)))
))
);
}
#[test]
fn test_compare_gt() {
use super::super::ast::CompareOp;
assert_eq!(
parse(". > 0"),
Ok(Filter::Compare(
Box::new(Filter::Identity),
CompareOp::Gt,
Box::new(Filter::Literal(simd_json::json!(0)))
))
);
}
#[test]
fn test_compare_ge() {
use super::super::ast::CompareOp;
assert_eq!(
parse(". >= -5"),
Ok(Filter::Compare(
Box::new(Filter::Identity),
CompareOp::Ge,
Box::new(Filter::Literal(simd_json::json!(-5)))
))
);
}
#[test]
fn test_compare_in_pipe() {
use super::super::ast::CompareOp;
assert_eq!(
parse(".[] | . > 2"),
Ok(Filter::Pipe(
Box::new(Filter::Iterate),
Box::new(Filter::Compare(
Box::new(Filter::Identity),
CompareOp::Gt,
Box::new(Filter::Literal(simd_json::json!(2)))
))
))
);
}
#[test]
fn test_compare_field_to_field() {
use super::super::ast::CompareOp;
assert_eq!(
parse(".a == .b"),
Ok(Filter::Compare(
Box::new(Filter::Field("a".to_string())),
CompareOp::Eq,
Box::new(Filter::Field("b".to_string()))
))
);
}
#[test]
fn test_split() {
assert_eq!(
parse("split(\"/\")"),
Ok(Filter::Function(FunctionCall::Split("/".to_string())))
);
}
#[test]
fn test_split_empty_delim() {
assert_eq!(
parse("split(\"\")"),
Ok(Filter::Function(FunctionCall::Split("".to_string())))
);
}
#[test]
fn test_join() {
assert_eq!(
parse("join(\"-\")"),
Ok(Filter::Function(FunctionCall::Join("-".to_string())))
);
}
#[test]
fn test_startswith() {
assert_eq!(
parse("startswith(\"hello\")"),
Ok(Filter::Function(FunctionCall::StartsWith(
"hello".to_string()
)))
);
}
#[test]
fn test_endswith() {
assert_eq!(
parse("endswith(\".json\")"),
Ok(Filter::Function(FunctionCall::EndsWith(
".json".to_string()
)))
);
}
#[test]
fn test_contains_string() {
assert_eq!(
parse("contains(\"foo\")"),
Ok(Filter::Function(FunctionCall::Contains(Box::new(
Filter::Literal(simd_json::json!("foo"))
))))
);
}
#[test]
fn test_contains_array() {
assert_eq!(
parse("contains([1, 2])"),
Ok(Filter::Function(FunctionCall::Contains(Box::new(
Filter::Array(vec![
Filter::Literal(simd_json::json!(1)),
Filter::Literal(simd_json::json!(2)),
])
))))
);
}
#[test]
fn test_contains_object() {
assert_eq!(
parse("contains({\"a\": 1})"),
Ok(Filter::Function(FunctionCall::Contains(Box::new(
Filter::Object(vec![(
ObjectKey::Static("a".to_string()),
Filter::Literal(simd_json::json!(1))
)])
))))
);
}
#[test]
fn test_string_func_in_pipe() {
assert_eq!(
parse(". | split(\"/\")"),
Ok(Filter::Pipe(
Box::new(Filter::Identity),
Box::new(Filter::Function(FunctionCall::Split("/".to_string())))
))
);
}
#[test]
fn test_to_entries() {
assert_eq!(parse("to_entries"), Ok(Filter::Builtin(Builtin::ToEntries)));
}
#[test]
fn test_from_entries() {
assert_eq!(
parse("from_entries"),
Ok(Filter::Builtin(Builtin::FromEntries))
);
}
#[test]
fn test_with_entries() {
assert_eq!(
parse("with_entries(.)"),
Ok(Filter::Function(FunctionCall::WithEntries(Box::new(
Filter::Identity
))))
);
}
#[test]
fn test_with_entries_select() {
use super::super::ast::CompareOp;
assert_eq!(
parse("with_entries(select(.value > 1))"),
Ok(Filter::Function(FunctionCall::WithEntries(Box::new(
Filter::Function(FunctionCall::Select(Box::new(Filter::Compare(
Box::new(Filter::Field("value".to_string())),
CompareOp::Gt,
Box::new(Filter::Literal(simd_json::json!(1)))
))))
))))
);
}
#[test]
fn test_to_from_entries_pipe() {
assert_eq!(
parse("to_entries | from_entries"),
Ok(Filter::Pipe(
Box::new(Filter::Builtin(Builtin::ToEntries)),
Box::new(Filter::Builtin(Builtin::FromEntries))
))
);
}
}