Skip to content

Compiled path test suite #102

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
328 changes: 241 additions & 87 deletions jsonpath-ast/src/ast.rs

Large diffs are not rendered by default.

321 changes: 205 additions & 116 deletions jsonpath-ast/src/syn_parse.rs

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Test case: 00_count_function
// Tags: function, count
#[test]
fn test_00_count_function() {
let q_ast = ::jsonpath_rust_impl::json_query!($[?count(@..*)>2]);
let q_pest = ::jsonpath_ast::ast::Main::try_from_pest_parse(r#"$[?count(@..*)>2]"#).expect("failed to parse");
assert_eq!(q_pest, q_ast);
}

// Test case: 01_single_node_arg
// Tags: function, count
#[test]
fn test_01_single_node_arg() {
let q_ast = ::jsonpath_rust_impl::json_query!($[?count(@.a)>1]);
let q_pest = ::jsonpath_ast::ast::Main::try_from_pest_parse(r#"$[?count(@.a)>1]"#).expect("failed to parse");
assert_eq!(q_pest, q_ast);
}

// Test case: 02_multiple_selector_arg
// Tags: function, count
#[test]
fn test_02_multiple_selector_arg() {
let q_ast = ::jsonpath_rust_impl::json_query!($[?count(@["a","d"])>1]);
let q_pest_double = ::jsonpath_ast::ast::Main::try_from_pest_parse(r#"$[?count(@["a","d"])>1]"#).expect("failed to parse");
let q_pest_single = ::jsonpath_ast::ast::Main::try_from_pest_parse(r#"$[?count(@['a','d'])>1]"#).expect("failed to parse");
assert_eq!(q_pest_double, q_ast);
assert_eq!(q_pest_single, q_ast);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Test case: 03_non_query_arg_number
// Tags: function, count
#[test]
fn test_03_non_query_arg_number() {
// let q_ast = ::jsonpath_rust_impl::json_query!($[?count(1)>2]);
let _q_pest = ::jsonpath_ast::ast::Main::try_from_pest_parse(r#"$[?count(1)>2]"#).expect_err("should not parse");
}

// Test case: 04_non_query_arg_string
// Tags: function, count
#[test]
fn test_04_non_query_arg_string() {
// let q_ast = ::jsonpath_rust_impl::json_query!($[?count('string')>2]);
let _q_pest = ::jsonpath_ast::ast::Main::try_from_pest_parse(r#"$[?count('string')>2]"#).expect_err("should not parse");
}

// Test case: 05_non_query_arg_true
// Tags: function, count
#[test]
fn test_05_non_query_arg_true() {
// let q_ast = ::jsonpath_rust_impl::json_query!($[?count(true)>2]);
let _q_pest = ::jsonpath_ast::ast::Main::try_from_pest_parse(r#"$[?count(true)>2]"#).expect_err("should not parse");
}

// Test case: 06_non_query_arg_false
// Tags: function, count
#[test]
fn test_06_non_query_arg_false() {
// let q_ast = ::jsonpath_rust_impl::json_query!($[?count(false)>2]);
let _q_pest = ::jsonpath_ast::ast::Main::try_from_pest_parse(r#"$[?count(false)>2]"#).expect_err("should not parse");
}

// Test case: 07_non_query_arg_null
// Tags: function, count
#[test]
fn test_07_non_query_arg_null() {
// let q_ast = ::jsonpath_rust_impl::json_query!($[?count(null)>2]);
let _q_pest = ::jsonpath_ast::ast::Main::try_from_pest_parse(r#"$[?count(null)>2]"#).expect_err("should not parse");
}

// Test case: 08_result_must_be_compared
// Tags: function, count
#[test]
fn test_08_result_must_be_compared() {
// let q_ast = ::jsonpath_rust_impl::json_query!($[?count(@..*)]);
let _q_pest = ::jsonpath_ast::ast::Main::try_from_pest_parse(r#"$[?count(@..*)]"#).expect_err("should not parse");
}

// Test case: 09_no_params
// Tags: function, count
#[test]
fn test_09_no_params() {
// let q_ast = ::jsonpath_rust_impl::json_query!($[?count()==1]);
let _q_pest = ::jsonpath_ast::ast::Main::try_from_pest_parse(r#"$[?count()==1]"#).expect_err("should not parse");
}

// Test case: 10_too_many_params
// Tags: function, count
#[test]
fn test_10_too_many_params() {
// let q_ast = ::jsonpath_rust_impl::json_query!($[?count(@.a,@.b)==1]);
let _q_pest = ::jsonpath_ast::ast::Main::try_from_pest_parse(r#"$[?count(@.a,@.b)==1]"#).expect_err("should not parse");
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Test case: 03_non_query_arg_number
// Tags: function, count
fn test_03_non_query_arg_number() {
::jsonpath_rust_impl::json_query!($[?count(1)>2]);
}

// Test case: 04_non_query_arg_string
// Tags: function, count
fn test_04_non_query_arg_string() {
::jsonpath_rust_impl::json_query!($[?count('string')>2]);
}

// Test case: 05_non_query_arg_true
// Tags: function, count
fn test_05_non_query_arg_true() {
::jsonpath_rust_impl::json_query!($[?count(true)>2]);
}

// Test case: 06_non_query_arg_false
// Tags: function, count
fn test_06_non_query_arg_false() {
::jsonpath_rust_impl::json_query!($[?count(false)>2]);
}

// Test case: 07_non_query_arg_null
// Tags: function, count
fn test_07_non_query_arg_null() {
::jsonpath_rust_impl::json_query!($[?count(null)>2]);
}

// Test case: 08_result_must_be_compared
// Tags: function, count
fn test_08_result_must_be_compared() {
::jsonpath_rust_impl::json_query!($[?count(@..*)]);
}

// Test case: 09_no_params
// Tags: function, count
fn test_09_no_params() {
::jsonpath_rust_impl::json_query!($[?count()==1]);
}

// Test case: 10_too_many_params
// Tags: function, count
fn test_10_too_many_params() {
::jsonpath_rust_impl::json_query!($[?count(@.a,@.b)==1]);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pub(crate) mod compile_and_passes;
pub(crate) mod compile_but_expect_err;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub(crate) mod count;
1 change: 1 addition & 0 deletions jsonpath-rust-impl/tests/rfc9535_compile_tests/mod.rs
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
pub(crate) mod basic;
pub(crate) mod functions;
4 changes: 2 additions & 2 deletions jsonpath-rust-impl/tests/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ mod tests {

#[test]
fn scratch() {
let q_ast = json_query!($.values[?match(@, $.regex)]).into();
let q_ast = json_query!($.values[?match(@, $.regex)]);
json_query!( $..[1] );
json_query!( $[1,::] );

Expand Down Expand Up @@ -57,7 +57,7 @@ mod tests {

/// Common function to run trybuild for all in suite dir
fn trybuild(dir: &str) {
let t = ::trybuild::TestCases::new();
let t = trybuild::TestCases::new();
let fail_path = format!("tests/rfc9535_compile_tests/{}/does_not_compile.rs", dir);
t.compile_fail(fail_path);
}
Expand Down
17 changes: 10 additions & 7 deletions src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,11 +134,11 @@ pub fn selector(rule: Pair<Rule>) -> Parsed<Selector> {

pub fn function_expr(rule: Pair<Rule>) -> Parsed<TestFunction> {
let fn_str = rule.as_str();
let mut elems = rule.into_inner();
let name = elems
.next()
.map(|e| e.as_str())
.ok_or(JsonPathError::empty("function expression"))?;
let mut elems = rule.into_inner().next().ok_or(JsonPathError::empty("function rule was empty"))?;
match elems.as_rule() {
Rule::returns_logical_type =>
}
let name = todo!();

// Check if the function name is valid namely nothing between the name and the opening parenthesis
if fn_str
Expand All @@ -153,7 +153,7 @@ pub fn function_expr(rule: Pair<Rule>) -> Parsed<TestFunction> {
)))
} else {
let mut args = vec![];
for arg in elems {
for arg in elems.into_inner() {
let next = next_down(arg)?;
match next.as_rule() {
Rule::literal => args.push(FnArg::Literal(literal(next)?)),
Expand All @@ -173,7 +173,10 @@ pub fn test(rule: Pair<Rule>) -> Parsed<Test> {
match child.as_rule() {
Rule::jp_query => Ok(Test::AbsQuery(jp_query(child)?)),
Rule::rel_query => Ok(Test::RelQuery(rel_query(child)?)),
Rule::function_expr => Ok(Test::Function(Box::new(function_expr(child)?))),
Rule::function_expr => {
let func = function_expr(child).map(|f| f)?;
Ok(Test::Function(Box::new(func)))
},
_ => Err(child.into()),
}
}
Expand Down
63 changes: 53 additions & 10 deletions src/parser/grammar/json_path_9535.pest
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

During the course of testing functions I've realized that the current grammar isn't enforcing well typed arguments, meaning that a function call could make it through the grammar but still be considered invalid. This would seriously hamper the notion of being able compile paths because they would pest parse correctly then need to be checked again. I think tightening the grammar further to include a notion of the 3 types mentioned in the RFC would allow for a much more concise parser, as well as reduce validation logic during path parsing.

Copy link
Contributor Author

@thehiddenwaffle thehiddenwaffle Jun 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've read the RFC like 4 times now, and though I think I've got everything, but my head is starting to spin.

Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,65 @@ step = {":" ~ S ~ int?}
start = {int}
end = {int}
slice_selector = { start? ~ S ~ ":" ~ S ~ end? ~ S ~ step? }
filter_selector = {"?"~ S ~ logical_expr}
filter_selector = {"?" ~ S ~ logical_expr}
logical_expr = {logical_expr_and ~ S ~ ("||" ~ S ~ logical_expr_and)*}
logical_expr_and = {atom_expr ~ S ~ ("&&" ~ S ~ atom_expr)*}
atom_expr = {paren_expr | comp_expr| test_expr}
paren_expr = {not_op? ~ S ~ "(" ~ S ~ logical_expr ~ S ~ ")"}
comp_expr = { comparable ~ S ~ comp_op ~ S ~ comparable }
test_expr = {not_op? ~ S ~ test}
test = {rel_query | jp_query | function_expr}
rel_query = {curr ~ S ~ segments}
function_expr = { ( function_name_one_arg ~ one_arg ) | ( function_name_two_arg ~ two_arg ) }
function_name_one_arg = { "length" | "value" | "count" }
function_name_two_arg = { "search" | "match" | "in" | "nin" | "none_of" | "any_of" | "subset_of" }
function_argument = { literal | test | logical_expr }
one_arg = _{ "(" ~ S ~ function_argument ~ S ~ ")" }
two_arg = _{ "(" ~ S ~ function_argument ~ S ~ "," ~ S ~ function_argument ~ S ~ ")" }
comparable = { literal | singular_query | function_expr }
test = {
filter_query // existence/non-existence
// Per RFC: [function expressions may be used] As a test-expr in a logical expression:
// The function's declared result type is LogicalType or (giving rise to conversion as per Section 2.4.2) NodesType.
| ( &( returns_logical_type | returns_nodes_type ) ~ function_expr ) // LogicalType or NodesType
}
filter_query = _{ rel_query | jp_query }
rel_query = {curr ~ segments}

function_expr = { returns_value_type | returns_logical_type | returns_nodes_type }
// https://github.com/pest-parser/pest/issues/333 would be awesome for this but it doesn't exist yet and it's been 7 years
// Lookahead to peek the names and then homogenize them into the same rule till we refine the parser code
length_func_call = { "length" ~ "(" ~ S ~ value_type ~ S ~ ")" }
value_func_call = { "value" ~ "(" ~ S ~ nodes_type ~ S ~ ")" }
count_func_call = { "count" ~ "(" ~ S ~ nodes_type ~ S ~ ")" }
search_func_call = { "search" ~ "(" ~ S ~ value_type ~ S ~ "," ~ S ~ value_type ~ S ~ ")" }
match_func_call = { "match" ~ "(" ~ S ~ value_type ~ S ~ "," ~ S ~ value_type ~ S ~ ")" }
in_func_call = { "in" ~ "(" ~ S ~ value_type ~ S ~ "," ~ S ~ value_type ~ S ~ ")" }
nin_func_call = { "nin" ~ "(" ~ S ~ value_type ~ S ~ "," ~ S ~ value_type ~ S ~ ")" }
none_of_func_call = { "none_of" ~ "(" ~ S ~ value_type ~ S ~ "," ~ S ~ value_type ~ S ~ ")" }
any_of_func_call = { "any_of" ~ "(" ~ S ~ value_type ~ S ~ "," ~ S ~ value_type ~ S ~ ")" }
subset_of_func_call = { "subset_of" ~ "(" ~ S ~ value_type ~ S ~ "," ~ S ~ value_type ~ S ~ ")" }

returns_value_type = { length_func_call | value_func_call | count_func_call }
returns_logical_type = { search_func_call | match_func_call | in_func_call | nin_func_call | none_of_func_call | any_of_func_call | subset_of_func_call }
// Currently no functions return this, so never match for now. To add a node which returns NodesType, replace !ANY
returns_nodes_type = { !ANY }

// When the declared type of the parameter is ValueType and the argument is one of the following:
// - A value expressed as a literal.
// - A singular query. In this case:
// - If the query results in a nodelist consisting of a single node, the argument is the value of the node.
// - If the query results in an empty nodelist, the argument is the special result Nothing.
value_type = { literal | singular_query | returns_value_type }
// When the declared type of the parameter is LogicalType and the argument is one of the following:
// - A function expression with declared result type NodesType. In this case, the argument is converted to LogicalType as per Section 2.4.2.
// - A logical-expr that is not a function expression.
logical_type = {
logical_expr // TODO why is this not allowed to be a function_expr? we guarantee it's return is a logical type(or is coercible to one)
// | returns_logical_type // this case is actually covered as a subset of logical_expr
| nodes_type
}
// When the declared type of the parameter is NodesType and the argument is a query (which includes singular query).
nodes_type = { jp_query | returns_nodes_type }


// Removed, a literal is a ValueType, and a logical_expr is just a test with more rules around it, both are LogicalType
// function_argument = { literal | test | logical_expr }

// Per RFC: As a comparable in a comparison:
// The function's declared result type is ValueType.
comparable = { literal | singular_query | ( &returns_value_type ~ function_expr ) }
literal = { number | string | bool | null }
bool = {"true" | "false"}
null = {"null"}
Expand Down
Loading