diff --git a/.gitignore b/.gitignore index 0def922..4d143c2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /target +**/target .idea Cargo.lock .DS_Store diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..e2cfc58 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "rfc9535/test_suite/jsonpath-compliance-test-suite"] + path = rfc9535/test_suite/jsonpath-compliance-test-suite + url = https://github.com/jsonpath-standard/jsonpath-compliance-test-suite.git diff --git a/CHANGELOG.md b/CHANGELOG.md index 6026cd9..faa5127 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -59,4 +59,7 @@ - **`0.7.3`** - make some methods public - **`0.7.5`** - - add reference and reference_mut methods \ No newline at end of file + - add reference and reference_mut methods +- **`1.0.1`** + - introduced breaking changes to the API to make it compliant with the RFC9535 + \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 2e971a0..ae52be5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "jsonpath-rust" description = "The library provides the basic functionality to find the set of the data according to the filtering query." -version = "0.7.5" +version = "1.0.0" authors = ["BorisZhguchev "] edition = "2021" license = "MIT" @@ -14,14 +14,14 @@ categories = ["development-tools", "parsing", "text-processing"] [dependencies] serde_json = "1.0" regex = "1" -pest = "2.0" -pest_derive = "2.0" +pest = "2.7.15" +pest_derive = "2.7.15" thiserror = "2.0.9" [dev-dependencies] serde = { version = "1.0", features = ["derive"] } criterion = "0.5.1" -colored = "2" + [[bench]] name = "regex" diff --git a/README.md b/README.md index f1d41ed..bc8fc9f 100644 --- a/README.md +++ b/README.md @@ -4,169 +4,32 @@ [![docs.rs](https://img.shields.io/docsrs/jsonpath-rust)](https://docs.rs/jsonpath-rust/latest/jsonpath_rust) [![Rust CI](https://github.com/besok/jsonpath-rust/actions/workflows/ci.yml/badge.svg)](https://github.com/besok/jsonpath-rust/actions/workflows/ci.yml) -The library provides the basic functionality to find the set of the data according to the filtering query. The idea -comes from XPath for XML structures. The details can be found [there](https://goessner.net/articles/JsonPath/) -Therefore JsonPath is a query language for JSON, similar to XPath for XML. The JsonPath query is a set of assertions to -specify the JSON fields that need to be verified. +The library provides the extensive functionality to find data sets according to filtering queries. +Inspired by XPath for XML structures, JsonPath is a query language for JSON. +The specification is described in [RFC 9535](https://www.rfc-editor.org/rfc/rfc9535.html). -Python bindings ([jsonpath-rust-bindings](https://github.com/night-crawler/jsonpath-rust-bindings)) are available on -pypi: - -```bash -pip install jsonpath-rust-bindings -``` - -## Simple examples - -Let's suppose we have a following json: - -```json -{ - "shop": { - "orders": [ - { - "id": 1, - "active": true - }, - { - "id": 2 - }, - { - "id": 3 - }, - { - "id": 4, - "active": true - } - ] - } -} - ``` - -And we pursue to find all orders id having the field 'active'. We can construct the jsonpath instance like -that ```$.shop.orders[?(@.active)].id``` and get the result ``` [1,4] ``` - -## The jsonpath description +# Important note -### Functions +The version 1.0.0 has a breaking change. The library has been rewritten from scratch to provide compliance with the RFC9535. -#### Size +The changes are: -A function `length()` transforms the output of the filtered expression into a size of this element -It works with arrays, therefore it returns a length of a given array, otherwise null. +- The library is now fully compliant with the RFC 9535. +- New structures and apis were introduced to provide the compliance with the RFC 9535. + - `Queryable` instead of `JsonLike` + - `Queried` instead of `Result` + - `JsonPath#{query_with_path, query_only_path, query}` to operate with the `Queryable` structure + - `JsonPathError` instead of `JsonPathParserError` + - `QueryRef` to provide the reference to the value and path +- The functions in, nin, noneOf, anyOf, subsetOf are now implemented as custom filter expressions and renamed to `in`, + `nin`, `none_of`, `any_of`, `subset_of` respectively. +- The function length was removed (the size can be checked using rust native functions for using it in filter there is length expression). -`$.some_field.length()` +## The compliance with RFC 9535 -**To use it** for objects, the operator `[*]` can be used. -`$.object.[*].length()` +The library is fully compliant with the standard [RFC 9535](https://www.rfc-editor.org/rfc/rfc9535.html) +To check the compliance with the standard head to [rfc9535 subfolder](rfc9535/README.md) -### Operators - -| Operator | Description | Where to use | -|----------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------| -| `$` | Pointer to the root of the json. | It is gently advising to start every jsonpath from the root. Also, inside the filters to point out that the path is starting from the root. | -| `@` | Pointer to the current element inside the filter operations. | It is used inside the filter operations to iterate the collection. | -| `*` or `[*]` | Wildcard. It brings to the list all objects and elements regardless their names. | It is analogue a flatmap operation. | -| `<..>` | Descent operation. It brings to the list all objects, children of that objects and etc | It is analogue a flatmap operation. | -| `.` or `.['']` | the key pointing to the field of the object | It is used to obtain the specific field. | -| `['' (, '')]` | the list of keys | the same usage as for a single key but for list | -| `[]` | the filter getting the element by its index. | | -| `[ (, )]` | the list if elements of array according to their indexes representing these numbers. | | -| `[::]` | slice operator to get a list of element operating with their indexes. By default step = 1, start = 0, end = array len. The elements can be omitted ```[:]``` | | -| `[?()]` | the logical expression to filter elements in the list. | It is used with arrays preliminary. | - -### Filter expressions - -The expressions appear in the filter operator like that `[?(@.len > 0)]`. The expression in general consists of the -following elements: - -- Left and right operands, that is ,in turn, can be a static value,representing as a primitive type like a number, - string value `'value'`, array of them or another json path instance. -- Expression sign, denoting what action can be performed - -| Expression sign | Description | Where to use | -|-----------------|--------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------| -| `!` | Not | To negate the expression | -| `==` | Equal | To compare numbers or string literals | -| `!=` | Unequal | To compare numbers or string literals in opposite way to equals | -| `<` | Less | To compare numbers | -| `>` | Greater | To compare numbers | -| `<=` | Less or equal | To compare numbers | -| `>=` | Greater or equal | To compare numbers | -| `~=` | Regular expression | To find the incoming right side in the left side. | -| `in` | Find left element in the list of right elements. | | -| `nin` | The same one as saying above but carrying the opposite sense. | | -| `size` | The size of array on the left size should be corresponded to the number on the right side. | | -| `noneOf` | The left size has no intersection with right | | -| `anyOf` | The left size has at least one intersection with right | | -| `subsetOf` | The left is a subset of the right side | | -| `?` | Exists operator. | The operator checks the existence of the field depicted on the left side like that `[?(@.key.isActive)]` | - -Filter expressions can be chained using `||` and `&&` (logical or and logical and correspondingly) in the following way: - -```json -{ - "key": [ - { - "city": "London", - "capital": true, - "size": "big" - }, - { - "city": "Berlin", - "capital": true, - "size": "big" - }, - { - "city": "Tokyo", - "capital": true, - "size": "big" - }, - { - "city": "Moscow", - "capital": true, - "size": "big" - }, - { - "city": "Athlon", - "capital": false, - "size": "small" - }, - { - "city": "Dortmund", - "capital": false, - "size": "big" - }, - { - "city": "Dublin", - "capital": true, - "size": "small" - } - ] -} -``` - -The path ``` $.key[?(@.capital == false || @size == 'small')].city ``` will give the following result: - -```json -[ - "Athlon", - "Dublin", - "Dortmund" -] -``` - -And the path ``` $.key[?(@.capital == false && @size != 'small')].city ``` ,in its turn, will give the following result: - -```json -[ - "Dortmund" -] -``` - -By default, the operators have the different priority so `&&` has a higher priority so to change it the brackets can be -used. -``` $.[?((@.f == 0 || @.f == 1) && ($.x == 15))].city ``` ## Examples @@ -212,127 +75,162 @@ Given the json } ``` -| JsonPath | Result | -|--------------------------------------|:-------------------------------------------------------------| -| `$.store.book[*].author` | The authors of all books | -| `$..book[?(@.isbn)]` | All books with an ISBN number | -| `$.store.*` | All things, both books and bicycles | -| `$..author` | All authors | -| `$.store..price` | The price of everything | -| `$..book[2]` | The third book | -| `$..book[-2]` | The second to last book | -| `$..book[0,1]` | The first two books | -| `$..book[:2]` | All books from index 0 (inclusive) until index 2 (exclusive) | -| `$..book[1:2]` | All books from index 1 (inclusive) until index 2 (exclusive) | -| `$..book[-2:]` | Last two books | -| `$..book[2:]` | Book number two from tail | -| `$.store.book[?(@.price < 10)]` | All books in store cheaper than 10 | -| `$..book[?(@.price <= $.expensive)]` | All books in store that are not "expensive" | -| `$..book[?(@.author ~= '(?i)REES')]` | All books matching regex (ignore case) | -| `$..*` | Give me every thing | +| JsonPath | Result | +|------------------------------------|:-------------------------------------------------------------| +| `$.store.book[*].author` | The authors of all books | +| `$..book[?@.isbn]` | All books with an ISBN number | +| `$.store.*` | All things, both books and bicycles | +| `$..author` | All authors | +| `$.store..price` | The price of everything | +| `$..book[2]` | The third book | +| `$..book[-2]` | The second to last book | +| `$..book[0,1]` | The first two books | +| `$..book[:2]` | All books from index 0 (inclusive) until index 2 (exclusive) | +| `$..book[1:2]` | All books from index 1 (inclusive) until index 2 (exclusive) | +| `$..book[-2:]` | Last two books | +| `$..book[2:]` | Book number two from tail | +| `$.store.book[?@.price < 10]` | All books in store cheaper than 10 | +| `$..book[?@.price <= $.expensive]` | All books in store that are not "expensive" | +| `$..book[?@.author ~= '(?i)REES']` | All books matching regex (ignore case) | +| `$..*` | Give me every thing | ## Library Usage -The library intends to provide the basic functionality for ability to find the slices of data using the syntax, saying -above. The dependency can be found as following: -``` jsonpath-rust = *``` +### Extensions +The library provides the following extensions: -The basic example is the following one: +- **in** +Checks if the first argument is in the array provided as the second argument. Example: `$.elems[?in(@, $.list)]` +Returns elements from `$.elems` that are present in `$.list`. -The library returns a `json path value` as a result. -This is enum type which represents: +- **nin** +Checks if the first argument is not in the array provided as the second argument. Example: `$.elems[?nin(@, $.list)]` +Returns elements from `$.elems` that are not present in `$.list`. -- `Slice` - a point to the passed original json -- `NewValue` - a new json data that has been generated during the path( for instance length operator) -- `NoValue` - indicates there is no match between given json and jsonpath in the most cases due to absent fields or - inconsistent data. +- **none_of** +Checks if none of the elements in the first array are in the second array. Example: `$.elems[?none_of(@, $.list)]` +Returns arrays from `$.elems` that have no elements in common with `$.list`. -To extract data there are two methods, provided on the `value`: +- **any_of** +Checks if any of the elements in the first array are in the second array. Example: `$.elems[?any_of(@, $.list)]` +Returns arrays from `$.elems` that have at least one element in common with `$.list`. -```rust -let v:JsonPathValue =... -v.to_data(); -v.slice_or( & some_dafault_value) -``` +- **subset_of** +Checks if all elements in the first array are in the second array. Example: `$.elems[?subset_of(@, $.list)]` +Returns arrays from `$.elems` where all elements are present in `$.list`. -### Find -there are 4 different functions to find data inside a `value`. -All take references, to increase reusability. Especially json parsing and jsonpath parsing can take significant time, -compared to a simple find. +### Queryable -The methods `find`, `find_as_path`, `find_slice` and `find_slice_ptr` take the same inputs, but handle them differently -depending on your usecase. They are further described in -the [docs](https://docs.rs/jsonpath-rust/latest/jsonpath_rust/enum.JsonPath.html#implementations). +The library provides a trait `Queryable` that can be implemented for any type. +This allows you to use the `JsonPath` methods on your own types. + +### Queried with path ```rust -use jsonpath_rust::{JsonPath, JsonPathValue}; -use serde_json::json; -use std::str::FromStr; -fn main() { - let data = json!({"first":{"second":[{"active":1},{"passive":1}]}}); - let path = JsonPath::from_str("$.first.second[?(@.active)]").unwrap(); - let slice_of_data = path.find_slice(&data); +fn union() -> Queried<()> { + let json = json!([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]); + + // QueryRes is a tuple of (value, path) for references and just value for owned values + let vec: Vec> = json.query_with_path("$[1,5:7]")?; + assert_eq!( + vec, + vec![ + (&json!(1), "$[1]".to_string()).into(), + (&json!(5), "$[5]".to_string()).into(), + (&json!(6), "$[6]".to_string()).into(), + ] + ); - let expected_value = json!({"active":1}); - let expected_path = "$.['first'].['second'][0]".to_string(); + Ok(()) +} + +``` + +### Queried without path + +```rust +fn exp_no_error() -> Queried<()> { + let json = json!([ + { + "a": 100, + "d": "e" + }, + { + "a": 100.1, + "d": "f" + }, + { + "a": "100", + "d": "g" + } + ]); + let vec: Vec<&Value> = json.query("$[?@.a==1E2]")?; assert_eq!( - slice_of_data, - vec![JsonPathValue::Slice(&expected_value, expected_path)] + vec.iter().collect::>(), + vec![&json!({"a":100, "d":"e"})] ); + + Ok(()) } ``` -### The structure +### Queried with only path -The internal structure of the `JsonPath` can be found here: -https://docs.rs/jsonpath-rust/latest/jsonpath_rust/parser/model/enum.JsonPath.html +```rust +fn filter_data() -> Queried<()> { + let json = json!({ + "a": 1, + "b": 2, + "c": 3 + }); -The internal structure of the `JsonPathIndex` can be found here: -https://docs.rs/jsonpath-rust/latest/jsonpath_rust/parser/model/enum.JsonPathIndex.html + let vec: Vec = json + .query_only_path("$[?@<3]")? + .into_iter() + .map(Option::unwrap_or_default) + .collect(); -### JsonLike + assert_eq!(vec, vec!["$['a']".to_string(), "$['b']".to_string()]); -The library provides a trait `JsonLike` that can be implemented for any type. -This allows you to use the `JsonPath` methods on your own types. + Ok(()) +} +``` -### Update the JsonLike structure by path +### Update the Queryable structure by path The library does not provide the functionality to update the json structure in the query itself. Instead, the library provides the ability to update the json structure by the path. Thus, the user needs to find a path for the `JsonLike` structure and update it manually. -There are two methods in the `JsonLike` trait: +There are two methods in the `Queryable` trait: - `reference_mut` - returns a mutable reference to the element by the path - `reference` - returns a reference to the element by the path - They accept a `JsonPath` instance and return a `Option<&mut Value>` or `Option<&Value>` respectively. - The path is supported with the limited elements namely only the elements with the direct access: + +They accept a `JsonPath` instance and return a `Option<&mut Self>` or `Option<&Self>` respectively. + +The path is supported with the limited elements namely only the elements with the direct access: + - root - field - index - The path can be obtained manually or `find_as_path` method can be used. ```rust -#[test] -fn update_by_path_test() -> Result<(), JsonPathParserError> { + fn update_by_path_test() -> Queried<()> { let mut json = json!([ {"verb": "RUN","distance":[1]}, {"verb": "TEST"}, {"verb": "DO NOT RUN"} ]); - let path: Box = Box::from(JsonPath::try_from("$.[?(@.verb == 'RUN')]")?); - let elem = path - .find_as_path(&json) - .get(0) - .cloned() - .ok_or(JsonPathParserError::InvalidJsonPath("".to_string()))?; + let path = json.query_only_path("$.[?(@.verb == 'RUN')]")?; + let elem = path.first().unwrap_or_default(); if let Some(v) = json - .reference_mut(elem)? + .reference_mut(elem) .and_then(|v| v.as_object_mut()) .and_then(|v| v.get_mut("distance")) .and_then(|v| v.as_array_mut()) @@ -353,6 +251,15 @@ fn update_by_path_test() -> Result<(), JsonPathParserError> { } ``` +### Python bindings + +Python bindings ([jsonpath-rust-bindings](https://github.com/night-crawler/jsonpath-rust-bindings)) are available on +pypi: + +```bash +pip install jsonpath-rust-bindings +``` + ## How to contribute TBD diff --git a/benches/equal.rs b/benches/equal.rs index a549ca6..f8255ff 100644 --- a/benches/equal.rs +++ b/benches/equal.rs @@ -1,25 +1,28 @@ use criterion::{criterion_group, criterion_main, Criterion}; -use jsonpath_rust::{JsonPath, JsonPathQuery}; -use serde_json::json; -use std::str::FromStr; + +use jsonpath_rust::parser::model::JpQuery; +use jsonpath_rust::parser::parse_json_path; +use jsonpath_rust::query::state::State; +use jsonpath_rust::query::Query; +use jsonpath_rust::JsonPath; +use serde_json::{json, Value}; struct SearchData { - json: serde_json::Value, - path: JsonPath, + json: Value, + path: JpQuery, } -const PATH: &str = "$.[?(@.author == 'abcd(Rees)')]"; +const PATH: &str = "$[?@.author == 'abcd(Rees)']"; fn equal_perf_test_with_reuse(cfg: &SearchData) { - let _v = cfg.path.find(&cfg.json); + let _v = cfg.path.process(State::root(&cfg.json)).data; } - fn equal_perf_test_without_reuse() { let json = Box::new(json!({ "author":"abcd(Rees)", })); - let _v = json.path(PATH).expect("the path is correct"); + let _v = json.query(PATH).expect("the path is correct"); } pub fn criterion_benchmark(c: &mut Criterion) { @@ -27,7 +30,7 @@ pub fn criterion_benchmark(c: &mut Criterion) { json: json!({ "author":"abcd(Rees)", }), - path: JsonPath::from_str(PATH).unwrap(), + path: parse_json_path(PATH).unwrap(), }; c.bench_function("equal bench with reuse", |b| { b.iter(|| equal_perf_test_with_reuse(&data)) diff --git a/benches/regex.rs b/benches/regex.rs index 72f9090..8f9eaf4 100644 --- a/benches/regex.rs +++ b/benches/regex.rs @@ -1,17 +1,20 @@ use criterion::{criterion_group, criterion_main, Criterion}; -use jsonpath_rust::{JsonPath, JsonPathQuery}; +use jsonpath_rust::parser::model::JpQuery; +use jsonpath_rust::parser::parse_json_path; +use jsonpath_rust::query::state::State; +use jsonpath_rust::query::Query; +use jsonpath_rust::JsonPath; use serde_json::{json, Value}; -use std::str::FromStr; struct SearchData { - json: serde_json::Value, - path: JsonPath, + json: Value, + path: JpQuery, } -const PATH: &str = "$.[?(@.author ~= '.*(?i)d\\(Rees\\)')]"; +const PATH: &str = "$[?search(@.author,'.*(?i)d\\\\(Rees\\\\)')]"; fn regex_perf_test_with_reuse(cfg: &SearchData) { - let _v = cfg.path.find(&cfg.json); + let _v = cfg.path.process(State::root(&cfg.json)).data; } fn regex_perf_test_without_reuse() { @@ -19,11 +22,11 @@ fn regex_perf_test_without_reuse() { "author":"abcd(Rees)", })); - let _v = json.path(PATH).expect("the path is correct"); + let _v = json.query(PATH).expect("the path is correct"); } fn json_path_compiling() { - let _v = JsonPath::::from_str(PATH).unwrap(); + let _v = parse_json_path(PATH).unwrap(); } pub fn criterion_benchmark(c: &mut Criterion) { @@ -31,7 +34,7 @@ pub fn criterion_benchmark(c: &mut Criterion) { json: json!({ "author":"abcd(Rees)", }), - path: JsonPath::from_str(PATH).unwrap(), + path: parse_json_path(PATH).unwrap(), }; c.bench_function("regex bench with reuse", |b| { b.iter(|| regex_perf_test_with_reuse(&data)) diff --git a/examples/hello-world.rs b/examples/hello-world.rs deleted file mode 100644 index d4716d2..0000000 --- a/examples/hello-world.rs +++ /dev/null @@ -1,12 +0,0 @@ -use jsonpath_rust::JsonPath; -use serde_json::json; - -fn main() { - let data = json!({ - "Hello":"World", - "Good":"Bye", - }); - let path = JsonPath::try_from("$.Hello").unwrap(); - let search_result = path.find(&data); - println!("Hello, {}", search_result); -} diff --git a/rfc9535/Cargo.toml b/rfc9535/Cargo.toml new file mode 100644 index 0000000..0166a95 --- /dev/null +++ b/rfc9535/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "jsonpath-rust-rfc9535" +description = "The tests to check the compliance with RFC 9535" +version = "0.0.1" +edition = "2021" +license = "MIT" +readme = "README.md" + + +[dependencies] +jsonpath-rust = { path = "../" } +serde_json = "1.0" +serde = { version = "1.0.217", features = ["derive"] } +colored = "2.0" +chrono = "0.4.39" diff --git a/rfc9535/README.md b/rfc9535/README.md new file mode 100644 index 0000000..3d61431 --- /dev/null +++ b/rfc9535/README.md @@ -0,0 +1,31 @@ +# Tests for RFC9535 + +This directory contains tests for the [RFC9535](https://www.rfc-editor.org/info/rfc9535) implementation. +The tests can be downloaded using `prepare.sh` script. + +## Usage +Run the main.rs. +It will print the test results in the console with the following format: +``` +... +Skipping test case: `` because of reason: `reason` +... +Failed tests: + +------- ------- + + +... + +RFC9535 Compliance tests: +Total: 671 +Passed: 209 +Failed: 462 +Skipped: 25 where 18 to fix in 5 issues + +``` + +The results will be saved in the `results.csv` file. + +The cases can be filtered using `filtered_cases.json` file. +The file should contain json array with the test case names that should be filtered out and the reason. diff --git a/rfc9535/src/console.rs b/rfc9535/src/console.rs new file mode 100644 index 0000000..701b9c8 --- /dev/null +++ b/rfc9535/src/console.rs @@ -0,0 +1,79 @@ +use crate::suite::TestFailure; +use chrono::Local; +use colored::Colorize; +use std::fs::{File, OpenOptions}; +use std::io::Write; +use std::io::{BufRead, BufReader, Error}; +pub fn process_results( + results: Vec, + skipped_cases: usize, + skipped_to_fix: usize, + issues: usize, +) -> Result<(), Error> { + let (passed, failed): (Vec<_>, Vec<_>) = results.into_iter().partition(TestResult::is_ok); + let total = passed.len() + failed.len() + skipped_cases; + let passed_count = passed.len(); + let failed_count = failed.len(); + let date = Local::now().format("%Y-%m-%d %H:%M:%S").to_string(); + + if failed_count > 0 { + println!("\n{}:", "Failed tests".bold()); + println!("\n"); + } + for failure in failed.iter() { + if let Err(TestFailure(case, reason)) = failure { + println!(" ------- {} -------", case.name.bold()); + println!("{}", reason.bold().red()); + } + } + + let mut file = OpenOptions::new() + .create(true) + .append(true) + .open("test_suite/results.csv")?; + writeln!( + file, + "{}; {}; {}; {}", + total, passed_count, failed_count, date + )?; + + clean_file(10)?; + + println!( + "\n{}:\n{}\n{}\n{}\n{}", + format!("RFC9535 Compliance tests").underline().bold(), + format!("Total: {}", total).bold(), + format!("Passed: {}", passed_count).green().bold(), + format!("Failed: {}", failed_count).red().bold(), + format!( + "Skipped: {} where {} to fix in {} issues", + skipped_cases, skipped_to_fix, issues + ) + .bold() + ); + Ok(()) +} + +fn clean_file(limit: usize) -> Result<(), Error> { + let file_path = "test_suite/results.csv"; + let file = File::open(file_path)?; + let reader = BufReader::new(file); + let lines: Vec = reader.lines().collect::>()?; + + if lines.len() > limit { + let header = &lines[0]; + let trimmed_lines = [&[header.clone()], &lines[lines.len() - limit..]].concat(); + + let mut file = OpenOptions::new() + .write(true) + .truncate(true) + .open(file_path)?; + for line in trimmed_lines { + writeln!(file, "{}", line)?; + } + } + + Ok(()) +} + +pub type TestResult<'a> = Result<(), TestFailure<'a>>; diff --git a/rfc9535/src/main.rs b/rfc9535/src/main.rs new file mode 100644 index 0000000..83e9c15 --- /dev/null +++ b/rfc9535/src/main.rs @@ -0,0 +1,23 @@ + +mod console; +mod suite; +use crate::suite::get_suite; +use colored::Colorize; +use console::TestResult; +use std::io::Error; +use std::io::Write; +use std::str::FromStr; + +fn main() -> Result<(), Error> { + let (cases, skipped, skipped_to_fix, issues) = get_suite()?; + console::process_results( + cases + .iter() + .map(suite::handle_test_case) + .collect::>(), + skipped, + skipped_to_fix, + issues, + ) +} + diff --git a/rfc9535/src/suite.rs b/rfc9535/src/suite.rs new file mode 100644 index 0000000..6dba7f0 --- /dev/null +++ b/rfc9535/src/suite.rs @@ -0,0 +1,161 @@ +use crate::console::TestResult; +use colored::Colorize; +use jsonpath_rust::parser::parse_json_path; +use jsonpath_rust::JsonPath; +use serde_json::Value; +use std::str::FromStr; + +type SkippedCases = usize; +type SkippedCasesToFix = usize; +type Issues = usize; +fn escape_control_chars(s: &str) -> String { + s.replace("\n", "\\n") + .replace("\t", "\\t") + .replace("\r", "\\r") +} +pub fn get_suite( +) -> Result<(Vec, SkippedCases, SkippedCasesToFix, Issues), std::io::Error> { + let file = std::fs::File::open("test_suite/jsonpath-compliance-test-suite/cts.json")?; + let suite: TestCases = serde_json::from_reader(std::io::BufReader::new(file))?; + let suite: Vec = suite.tests; + + let filter = std::fs::File::open("test_suite/filtered_cases.json")?; + let filter: Vec = serde_json::from_reader(std::io::BufReader::new(filter))?; + let mut skipped_cases = 0; + let mut skipped_cases_to_fix = 0; + let mut issues = vec![]; + Ok(( + suite + .into_iter() + .filter(|case| { + if let Some(f) = filter.iter().find(|filter| case.name == filter.name) { + println!( + r#"Skipping test case:`{}` with the reason: `{}`"#, + escape_control_chars(&case.name).green(), + escape_control_chars(&f.reason).green() + ); + skipped_cases += 1; + if f.expected_to_fix { + skipped_cases_to_fix += 1; + if !issues.contains(&f.issue) { + issues.push(f.issue); + } + } + false + } else { + true + } + }) + .collect(), + skipped_cases, + skipped_cases_to_fix, + issues.len(), + )) +} +pub fn handle_test_case(case: &TestCase) -> TestResult { + let jspath = parse_json_path(case.selector.as_str()); + + if case.invalid_selector { + if jspath.is_ok() { + Err(TestFailure::invalid(case)) + } else { + Ok(()) + } + } else { + if let Some(doc) = case.document.as_ref() { + let p = case.selector.as_str(); + let result = doc.query(p).map(|vs| { + vs.into_iter() + .map(|v| (*v).clone()) + .collect::>() + .into() + }); + + if result.is_err() { + println!("---- Parsing error: '{}'", case.name); + println!("reason: {}", result.as_ref().err().unwrap()); + println!("selector: {}", case.selector); + println!("document: {}", doc); + return Err(TestFailure::invalid(case)); + } + let result = result.unwrap(); + + match (case.result.as_ref(), case.results.as_ref()) { + (Some(expected), _) => { + if result == *expected { + Ok(()) + } else { + Err(TestFailure::match_one(case, &result)) + } + } + (None, Some(expected)) => { + if expected.iter().any(|exp| result == *exp) { + Ok(()) + } else { + Err(TestFailure::match_any(case, &result)) + } + } + _ => Ok(()), + } + } else { + Ok(()) + } + } +} + +#[derive(serde::Deserialize)] +struct FilterCase { + name: String, + reason: String, + expected_to_fix: bool, + issue: usize, +} + +#[derive(serde::Deserialize)] +pub struct TestCase { + pub(crate) name: String, + pub(crate) selector: String, + pub(crate) document: Option, + pub(crate) result: Option, + pub(crate) results: Option>, + #[serde(default)] + pub(crate) invalid_selector: bool, +} +#[derive(serde::Deserialize)] +pub struct TestCases { + pub(crate) description: String, + pub(crate) tests: Vec, +} + +pub struct TestFailure<'a>(pub &'a TestCase, pub String); + +impl<'a> TestFailure<'a> { + pub(crate) fn invalid(case: &'a TestCase) -> Self { + TestFailure( + case, + format!( + "The path should have been considered invalid: {}", + case.selector + ), + ) + } + + pub(crate) fn match_one(case: &'a TestCase, actual: &Value) -> Self { + TestFailure( + case, + format!( + "Actual did not match expected. Actual: {:?}, Expected: {:?}", + actual, &case.result + ), + ) + } + pub(crate) fn match_any(case: &'a TestCase, actual: &Value) -> Self { + TestFailure( + case, + format!( + "Actual did not match expected. Actual: {:?}, Expected: {:?}", + actual, &case.results + ), + ) + } +} diff --git a/rfc9535/test_suite/filtered_cases.json b/rfc9535/test_suite/filtered_cases.json new file mode 100644 index 0000000..5451cc3 --- /dev/null +++ b/rfc9535/test_suite/filtered_cases.json @@ -0,0 +1,219 @@ +[ + { + "name": "functions, value, result must be compared", + "reason": "To handle it later either at the parser level or during execution", + "expected_to_fix": true, + "issue": 1 + }, + { + "name": "functions, length, result must be compared", + "reason": "To handle it later either at the parser level or during execution", + "expected_to_fix": true, + "issue": 1 + }, + { + "name": "functions, count, result must be compared", + "reason": "To handle it later either at the parser level or during execution", + "expected_to_fix": true, + "issue": 1 + }, + { + "name": "functions, length, non-singular query arg", + "reason": "To handle it later either at the parser level or during execution", + "expected_to_fix": true, + "issue": 1 + }, + + { + "name": "functions, search, dot matcher on \\u2029", + "reason": "Rust by default handles \r as a symbol and rfc9485 it is not a symbol(\\n, \\r, \\u2028, and \\u2029).)", + "expected_to_fix": true, + "issue": 2 + }, + { + "name": "functions, search, dot matcher on \\u2028", + "reason": "Rust by default handles \r as a symbol and rfc9485 it is not a symbol(\\n, \\r, \\u2028, and \\u2029).)", + "expected_to_fix": true, + "issue": 2 + }, + { + "name": "functions, match, dot matcher on \\u2029", + "reason": "Rust by default handles \r as a symbol and rfc9485 it is not a symbol(\\n, \\r, \\u2028, and \\u2029).)", + "expected_to_fix": true, + "issue": 2 + }, + { + "name": "functions, match, dot matcher on \\u2028", + "reason": "Rust by default handles \r as a symbol and rfc9485 it is not a symbol(\\n, \\r, \\u2028, and \\u2029).)", + "expected_to_fix": true, + "issue": 2 + }, + + { + "name": "basic, descendant segment, multiple selectors", + "reason": "The result is correct but the order differs from expected since we process selector by selector", + "expected_to_fix": false, + "issue": 0 + } , + { + "name": "basic, descendant segment, object traversal, multiple selectors", + "reason": "The result is correct but the order differs from expected since we process selector by selector", + "expected_to_fix": false, + "issue": 0 + }, + + { + "name": "name selector, double quotes, escaped ☺, lower case hex", + "reason": "This case fails without quotes '..' and there is a bug to fix in the parser to handle it", + "expected_to_fix": true, + "issue": 3 + }, + { + "name": "name selector, single quotes, escaped ☺, lower case hex", + "reason": "This case fails without quotes '..' and there is a bug to fix in the parser to handle it", + "expected_to_fix": true, + "issue": 3 + }, + + { + "name": "filter, string literal, escaped single quote in single quotes", + "reason": "Should be extra logic to handle this special case", + "expected_to_fix": true, + "issue": 4 + }, + + { + "name": "filter, string literal, escaped double quote in double quotes", + "reason": "Should be extra logic to handle this special case", + "expected_to_fix": true, + "issue": 4 + }, + + { + "name": "name selector, single quotes, surrogate pair \uD83D\uDE00", + "reason": "As test it works but in the doc it has double \\", + "expected_to_fix": false, + "issue": 0 + } , + { + "name": "name selector, single quotes, surrogate pair \uD834\uDD1E", + "reason": "As test it works but in the doc it has double \\", + "expected_to_fix": false, + "issue": 0 + }, + { + "name": "name selector, single quotes, escaped tab", + "reason": "As rust test it passes", + "expected_to_fix": false, + "issue": 0 + }, + { + "name": "name selector, single quotes, escaped ☺, upper case hex", + "reason": "As rust test it passes", + "expected_to_fix": false, + "issue": 0 + }, + { + "name": "name selector, single quotes, escaped carriage return", + "reason": "As rust test it passes", + "expected_to_fix": false, + "issue": 0 + }, + + { + "name": "name selector, double quotes, escaped double quote", + "reason": "Figure out if it is a bug in the parser or in the test", + "expected_to_fix": true, + "issue": 5 + }, + { + "name": "name selector, double quotes, escaped backspace", + "reason": "Figure out if it is a bug in the parser or in the test", + "expected_to_fix": true, + "issue": 5 + }, + { + "name": "name selector, double quotes, escaped form feed", + "reason": "Figure out if it is a bug in the parser or in the test", + "expected_to_fix": true, + "issue": 5 + }, + { + "name": "name selector, double quotes, escaped line feed", + "reason": "Figure out if it is a bug in the parser or in the test", + "expected_to_fix": true, + "issue": 5 + }, + { + "name": "name selector, double quotes, escaped carriage return", + "reason": "Figure out if it is a bug in the parser or in the test", + "expected_to_fix": true, + "issue": 5 + }, + { + "name": "name selector, double quotes, escaped tab", + "reason": "Figure out if it is a bug in the parser or in the test", + "expected_to_fix": true, + "issue": 5 + }, + { + "name": "name selector, single quotes, escaped backspace", + "reason": "Figure out if it is a bug in the parser or in the test", + "expected_to_fix": true, + "issue": 5 + }, + { + "name": "name selector, single quotes, escaped form feed", + "reason": "Figure out if it is a bug in the parser or in the test", + "expected_to_fix": true, + "issue": 5 + }, + { + "name": "name selector, single quotes, escaped line feed", + "reason": "Figure out if it is a bug in the parser or in the test", + "expected_to_fix": true, + "issue": 5 + }, + { + "name": "name selector, double quotes, escaped ☺, upper case hex", + "reason": "Figure out if it is a bug in the parser or in the test", + "expected_to_fix": true, + "issue": 5 + }, + { + "name": "name selector, double quotes, surrogate pair \uD834\uDD1E", + "reason": "Figure out if it is a bug in the parser or in the test", + "expected_to_fix": true, + "issue": 6 + }, + { + "name": "name selector, double quotes, surrogate pair \uD83D\uDE00", + "reason": "Figure out if it is a bug in the parser or in the test", + "expected_to_fix": true, + "issue": 6 + }, + { + "name": "name selector, double quotes, before high surrogates", + "reason": "Figure out if it is a bug in the parser or in the test", + "expected_to_fix": true, + "issue": 6 + }, + { + "name": "name selector, double quotes, after low surrogates", + "reason": "Figure out if it is a bug in the parser or in the test", + "expected_to_fix": true, + "issue": 6 + }, + { + "name": "name selector, single quotes, escaped single quote", + "reason": "Figure out if it is a bug in the parser or in the test", + "expected_to_fix": true, + "issue": 7 + }, + { + "name": "filter, nested", + "reason": "Figure out if it is a bug in the parser or in the test", + "expected_to_fix": true, + "issue": 8 + } +] \ No newline at end of file diff --git a/rfc9535/test_suite/jsonpath-compliance-test-suite b/rfc9535/test_suite/jsonpath-compliance-test-suite new file mode 160000 index 0000000..05f6cac --- /dev/null +++ b/rfc9535/test_suite/jsonpath-compliance-test-suite @@ -0,0 +1 @@ +Subproject commit 05f6cac786bf0cce95437e6f1adedc3186d54a71 diff --git a/rfc9535/test_suite/results.csv b/rfc9535/test_suite/results.csv new file mode 100644 index 0000000..4a80f9a --- /dev/null +++ b/rfc9535/test_suite/results.csv @@ -0,0 +1,11 @@ +Total; Passed; Failed; Date +687; 652; 15; 2025-03-18 22:27:40 +687; 652; 10; 2025-03-18 22:29:42 +687; 652; 5; 2025-03-18 22:34:48 +687; 652; 4; 2025-03-18 22:40:55 +687; 652; 2; 2025-03-18 22:41:43 +687; 652; 1; 2025-03-18 22:41:53 +687; 652; 1; 2025-03-18 22:49:15 +687; 652; 1; 2025-03-18 22:52:05 +687; 652; 0; 2025-03-18 22:56:39 +687; 652; 0; 2025-03-18 22:57:01 diff --git a/src/fixtures/prepare.sh b/src/fixtures/prepare.sh deleted file mode 100755 index 1bc5537..0000000 --- a/src/fixtures/prepare.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env bash - -# Use this script to download the RFC9535 compliance suite and prepare it for -# use in tests. - -script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" &>/dev/null && pwd -P) - -url="https://raw.githubusercontent.com/jsonpath-standard/jsonpath-compliance-test-suite/refs/heads/main/cts.json" - -curl -s $url | jq -r '.tests' > "$script_dir/rfc9535-cts.json" \ No newline at end of file diff --git a/src/fixtures/rfc9535-cts.json b/src/fixtures/rfc9535-cts.json deleted file mode 100644 index 1376487..0000000 --- a/src/fixtures/rfc9535-cts.json +++ /dev/null @@ -1,10866 +0,0 @@ -[ - { - "name": "basic, root", - "selector": "$", - "document": [ - "first", - "second" - ], - "result": [ - [ - "first", - "second" - ] - ] - }, - { - "name": "basic, no leading whitespace", - "selector": " $", - "invalid_selector": true, - "tags": [ - "whitespace" - ] - }, - { - "name": "basic, no trailing whitespace", - "selector": "$ ", - "invalid_selector": true, - "tags": [ - "whitespace" - ] - }, - { - "name": "basic, name shorthand", - "selector": "$.a", - "document": { - "a": "A", - "b": "B" - }, - "result": [ - "A" - ] - }, - { - "name": "basic, name shorthand, extended unicode ☺", - "selector": "$.☺", - "document": { - "☺": "A", - "b": "B" - }, - "result": [ - "A" - ] - }, - { - "name": "basic, name shorthand, underscore", - "selector": "$._", - "document": { - "_": "A", - "_foo": "B" - }, - "result": [ - "A" - ] - }, - { - "name": "basic, name shorthand, symbol", - "selector": "$.&", - "invalid_selector": true - }, - { - "name": "basic, name shorthand, number", - "selector": "$.1", - "invalid_selector": true - }, - { - "name": "basic, name shorthand, absent data", - "selector": "$.c", - "document": { - "a": "A", - "b": "B" - }, - "result": [] - }, - { - "name": "basic, name shorthand, array data", - "selector": "$.a", - "document": [ - "first", - "second" - ], - "result": [] - }, - { - "name": "basic, wildcard shorthand, object data", - "selector": "$.*", - "document": { - "a": "A", - "b": "B" - }, - "results": [ - [ - "A", - "B" - ], - [ - "B", - "A" - ] - ] - }, - { - "name": "basic, wildcard shorthand, array data", - "selector": "$.*", - "document": [ - "first", - "second" - ], - "result": [ - "first", - "second" - ] - }, - { - "name": "basic, wildcard selector, array data", - "selector": "$[*]", - "document": [ - "first", - "second" - ], - "result": [ - "first", - "second" - ] - }, - { - "name": "basic, wildcard shorthand, then name shorthand", - "selector": "$.*.a", - "document": { - "x": { - "a": "Ax", - "b": "Bx" - }, - "y": { - "a": "Ay", - "b": "By" - } - }, - "results": [ - [ - "Ax", - "Ay" - ], - [ - "Ay", - "Ax" - ] - ] - }, - { - "name": "basic, multiple selectors", - "selector": "$[0,2]", - "document": [ - 0, - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9 - ], - "result": [ - 0, - 2 - ] - }, - { - "name": "basic, multiple selectors, space instead of comma", - "selector": "$[0 2]", - "invalid_selector": true, - "tags": [ - "whitespace" - ] - }, - { - "name": "basic, selector, leading comma", - "selector": "$[,0]", - "invalid_selector": true - }, - { - "name": "basic, selector, trailing comma", - "selector": "$[0,]", - "invalid_selector": true - }, - { - "name": "basic, multiple selectors, name and index, array data", - "selector": "$['a',1]", - "document": [ - 0, - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9 - ], - "result": [ - 1 - ] - }, - { - "name": "basic, multiple selectors, name and index, object data", - "selector": "$['a',1]", - "document": { - "a": 1, - "b": 2 - }, - "result": [ - 1 - ] - }, - { - "name": "basic, multiple selectors, index and slice", - "selector": "$[1,5:7]", - "document": [ - 0, - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9 - ], - "result": [ - 1, - 5, - 6 - ] - }, - { - "name": "basic, multiple selectors, index and slice, overlapping", - "selector": "$[1,0:3]", - "document": [ - 0, - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9 - ], - "result": [ - 1, - 0, - 1, - 2 - ] - }, - { - "name": "basic, multiple selectors, duplicate index", - "selector": "$[1,1]", - "document": [ - 0, - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9 - ], - "result": [ - 1, - 1 - ] - }, - { - "name": "basic, multiple selectors, wildcard and index", - "selector": "$[*,1]", - "document": [ - 0, - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9 - ], - "result": [ - 0, - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 1 - ] - }, - { - "name": "basic, multiple selectors, wildcard and name", - "selector": "$[*,'a']", - "document": { - "a": "A", - "b": "B" - }, - "results": [ - [ - "A", - "B", - "A" - ], - [ - "B", - "A", - "A" - ] - ] - }, - { - "name": "basic, multiple selectors, wildcard and slice", - "selector": "$[*,0:2]", - "document": [ - 0, - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9 - ], - "result": [ - 0, - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 0, - 1 - ] - }, - { - "name": "basic, multiple selectors, multiple wildcards", - "selector": "$[*,*]", - "document": [ - 0, - 1, - 2 - ], - "result": [ - 0, - 1, - 2, - 0, - 1, - 2 - ] - }, - { - "name": "basic, empty segment", - "selector": "$[]", - "invalid_selector": true - }, - { - "name": "basic, descendant segment, index", - "selector": "$..[1]", - "document": { - "o": [ - 0, - 1, - [ - 2, - 3 - ] - ] - }, - "result": [ - 1, - 3 - ] - }, - { - "name": "basic, descendant segment, name shorthand", - "selector": "$..a", - "document": { - "o": [ - { - "a": "b" - }, - { - "a": "c" - } - ] - }, - "result": [ - "b", - "c" - ] - }, - { - "name": "basic, descendant segment, wildcard shorthand, array data", - "selector": "$..*", - "document": [ - 0, - 1 - ], - "result": [ - 0, - 1 - ] - }, - { - "name": "basic, descendant segment, wildcard selector, array data", - "selector": "$..[*]", - "document": [ - 0, - 1 - ], - "result": [ - 0, - 1 - ] - }, - { - "name": "basic, descendant segment, wildcard selector, nested arrays", - "selector": "$..[*]", - "document": [ - [ - [ - 1 - ] - ], - [ - 2 - ] - ], - "results": [ - [ - [ - [ - 1 - ] - ], - [ - 2 - ], - [ - 1 - ], - 1, - 2 - ], - [ - [ - [ - 1 - ] - ], - [ - 2 - ], - [ - 1 - ], - 2, - 1 - ] - ] - }, - { - "name": "basic, descendant segment, wildcard selector, nested objects", - "selector": "$..[*]", - "document": { - "a": { - "c": { - "e": 1 - } - }, - "b": { - "d": 2 - } - }, - "results": [ - [ - { - "c": { - "e": 1 - } - }, - { - "d": 2 - }, - { - "e": 1 - }, - 1, - 2 - ], - [ - { - "c": { - "e": 1 - } - }, - { - "d": 2 - }, - { - "e": 1 - }, - 2, - 1 - ], - [ - { - "c": { - "e": 1 - } - }, - { - "d": 2 - }, - 2, - { - "e": 1 - }, - 1 - ], - [ - { - "d": 2 - }, - { - "c": { - "e": 1 - } - }, - { - "e": 1 - }, - 1, - 2 - ], - [ - { - "d": 2 - }, - { - "c": { - "e": 1 - } - }, - { - "e": 1 - }, - 2, - 1 - ], - [ - { - "d": 2 - }, - { - "c": { - "e": 1 - } - }, - 2, - { - "e": 1 - }, - 1 - ] - ] - }, - { - "name": "basic, descendant segment, wildcard shorthand, object data", - "selector": "$..*", - "document": { - "a": "b" - }, - "result": [ - "b" - ] - }, - { - "name": "basic, descendant segment, wildcard shorthand, nested data", - "selector": "$..*", - "document": { - "o": [ - { - "a": "b" - } - ] - }, - "result": [ - [ - { - "a": "b" - } - ], - { - "a": "b" - }, - "b" - ] - }, - { - "name": "basic, descendant segment, multiple selectors", - "selector": "$..['a','d']", - "document": [ - { - "a": "b", - "d": "e" - }, - { - "a": "c", - "d": "f" - } - ], - "result": [ - "b", - "e", - "c", - "f" - ] - }, - { - "name": "basic, descendant segment, object traversal, multiple selectors", - "selector": "$..['a','d']", - "document": { - "x": { - "a": "b", - "d": "e" - }, - "y": { - "a": "c", - "d": "f" - } - }, - "results": [ - [ - "b", - "e", - "c", - "f" - ], - [ - "c", - "f", - "b", - "e" - ] - ] - }, - { - "name": "basic, bald descendant segment", - "selector": "$..", - "invalid_selector": true - }, - { - "name": "filter, existence, without segments", - "selector": "$[?@]", - "document": { - "a": 1, - "b": null - }, - "results": [ - [ - 1, - null - ], - [ - null, - 1 - ] - ] - }, - { - "name": "filter, existence", - "selector": "$[?@.a]", - "document": [ - { - "a": "b", - "d": "e" - }, - { - "b": "c", - "d": "f" - } - ], - "result": [ - { - "a": "b", - "d": "e" - } - ] - }, - { - "name": "filter, existence, present with null", - "selector": "$[?@.a]", - "document": [ - { - "a": null, - "d": "e" - }, - { - "b": "c", - "d": "f" - } - ], - "result": [ - { - "a": null, - "d": "e" - } - ] - }, - { - "name": "filter, equals string, single quotes", - "selector": "$[?@.a=='b']", - "document": [ - { - "a": "b", - "d": "e" - }, - { - "a": "c", - "d": "f" - } - ], - "result": [ - { - "a": "b", - "d": "e" - } - ] - }, - { - "name": "filter, equals numeric string, single quotes", - "selector": "$[?@.a=='1']", - "document": [ - { - "a": "1", - "d": "e" - }, - { - "a": 1, - "d": "f" - } - ], - "result": [ - { - "a": "1", - "d": "e" - } - ] - }, - { - "name": "filter, equals string, double quotes", - "selector": "$[?@.a==\"b\"]", - "document": [ - { - "a": "b", - "d": "e" - }, - { - "a": "c", - "d": "f" - } - ], - "result": [ - { - "a": "b", - "d": "e" - } - ] - }, - { - "name": "filter, equals numeric string, double quotes", - "selector": "$[?@.a==\"1\"]", - "document": [ - { - "a": "1", - "d": "e" - }, - { - "a": 1, - "d": "f" - } - ], - "result": [ - { - "a": "1", - "d": "e" - } - ] - }, - { - "name": "filter, equals number", - "selector": "$[?@.a==1]", - "document": [ - { - "a": 1, - "d": "e" - }, - { - "a": "c", - "d": "f" - }, - { - "a": 2, - "d": "f" - }, - { - "a": "1", - "d": "f" - } - ], - "result": [ - { - "a": 1, - "d": "e" - } - ] - }, - { - "name": "filter, equals null", - "selector": "$[?@.a==null]", - "document": [ - { - "a": null, - "d": "e" - }, - { - "a": "c", - "d": "f" - } - ], - "result": [ - { - "a": null, - "d": "e" - } - ] - }, - { - "name": "filter, equals null, absent from data", - "selector": "$[?@.a==null]", - "document": [ - { - "d": "e" - }, - { - "a": "c", - "d": "f" - } - ], - "result": [] - }, - { - "name": "filter, equals true", - "selector": "$[?@.a==true]", - "document": [ - { - "a": true, - "d": "e" - }, - { - "a": "c", - "d": "f" - } - ], - "result": [ - { - "a": true, - "d": "e" - } - ] - }, - { - "name": "filter, equals false", - "selector": "$[?@.a==false]", - "document": [ - { - "a": false, - "d": "e" - }, - { - "a": "c", - "d": "f" - } - ], - "result": [ - { - "a": false, - "d": "e" - } - ] - }, - { - "name": "filter, equals self", - "selector": "$[?@==@]", - "document": [ - 1, - null, - true, - { - "a": "b" - }, - [ - false - ] - ], - "result": [ - 1, - null, - true, - { - "a": "b" - }, - [ - false - ] - ] - }, - { - "name": "filter, deep equality, arrays", - "selector": "$[?@.a==@.b]", - "document": [ - { - "a": false, - "b": [ - 1, - 2 - ] - }, - { - "a": [ - [ - 1, - [ - 2 - ] - ] - ], - "b": [ - [ - 1, - [ - 2 - ] - ] - ] - }, - { - "a": [ - [ - 1, - [ - 2 - ] - ] - ], - "b": [ - [ - [ - 2 - ], - 1 - ] - ] - }, - { - "a": [ - [ - 1, - [ - 2 - ] - ] - ], - "b": [ - [ - 1, - 2 - ] - ] - } - ], - "result": [ - { - "a": [ - [ - 1, - [ - 2 - ] - ] - ], - "b": [ - [ - 1, - [ - 2 - ] - ] - ] - } - ] - }, - { - "name": "filter, deep equality, objects", - "selector": "$[?@.a==@.b]", - "document": [ - { - "a": false, - "b": { - "x": 1, - "y": { - "z": 1 - } - } - }, - { - "a": { - "x": 1, - "y": { - "z": 1 - } - }, - "b": { - "x": 1, - "y": { - "z": 1 - } - } - }, - { - "a": { - "x": 1, - "y": { - "z": 1 - } - }, - "b": { - "y": { - "z": 1 - }, - "x": 1 - } - }, - { - "a": { - "x": 1, - "y": { - "z": 1 - } - }, - "b": { - "x": 1 - } - }, - { - "a": { - "x": 1, - "y": { - "z": 1 - } - }, - "b": { - "x": 1, - "y": { - "z": 2 - } - } - } - ], - "result": [ - { - "a": { - "x": 1, - "y": { - "z": 1 - } - }, - "b": { - "x": 1, - "y": { - "z": 1 - } - } - }, - { - "a": { - "x": 1, - "y": { - "z": 1 - } - }, - "b": { - "y": { - "z": 1 - }, - "x": 1 - } - } - ] - }, - { - "name": "filter, not-equals string, single quotes", - "selector": "$[?@.a!='b']", - "document": [ - { - "a": "b", - "d": "e" - }, - { - "a": "c", - "d": "f" - } - ], - "result": [ - { - "a": "c", - "d": "f" - } - ] - }, - { - "name": "filter, not-equals numeric string, single quotes", - "selector": "$[?@.a!='1']", - "document": [ - { - "a": "1", - "d": "e" - }, - { - "a": 1, - "d": "f" - } - ], - "result": [ - { - "a": 1, - "d": "f" - } - ] - }, - { - "name": "filter, not-equals string, single quotes, different type", - "selector": "$[?@.a!='b']", - "document": [ - { - "a": "b", - "d": "e" - }, - { - "a": 1, - "d": "f" - } - ], - "result": [ - { - "a": 1, - "d": "f" - } - ] - }, - { - "name": "filter, not-equals string, double quotes", - "selector": "$[?@.a!=\"b\"]", - "document": [ - { - "a": "b", - "d": "e" - }, - { - "a": "c", - "d": "f" - } - ], - "result": [ - { - "a": "c", - "d": "f" - } - ] - }, - { - "name": "filter, not-equals numeric string, double quotes", - "selector": "$[?@.a!=\"1\"]", - "document": [ - { - "a": "1", - "d": "e" - }, - { - "a": 1, - "d": "f" - } - ], - "result": [ - { - "a": 1, - "d": "f" - } - ] - }, - { - "name": "filter, not-equals string, double quotes, different types", - "selector": "$[?@.a!=\"b\"]", - "document": [ - { - "a": "b", - "d": "e" - }, - { - "a": 1, - "d": "f" - } - ], - "result": [ - { - "a": 1, - "d": "f" - } - ] - }, - { - "name": "filter, not-equals number", - "selector": "$[?@.a!=1]", - "document": [ - { - "a": 1, - "d": "e" - }, - { - "a": 2, - "d": "f" - }, - { - "a": "1", - "d": "f" - } - ], - "result": [ - { - "a": 2, - "d": "f" - }, - { - "a": "1", - "d": "f" - } - ] - }, - { - "name": "filter, not-equals number, different types", - "selector": "$[?@.a!=1]", - "document": [ - { - "a": 1, - "d": "e" - }, - { - "a": "c", - "d": "f" - } - ], - "result": [ - { - "a": "c", - "d": "f" - } - ] - }, - { - "name": "filter, not-equals null", - "selector": "$[?@.a!=null]", - "document": [ - { - "a": null, - "d": "e" - }, - { - "a": "c", - "d": "f" - } - ], - "result": [ - { - "a": "c", - "d": "f" - } - ] - }, - { - "name": "filter, not-equals null, absent from data", - "selector": "$[?@.a!=null]", - "document": [ - { - "d": "e" - }, - { - "a": "c", - "d": "f" - } - ], - "result": [ - { - "d": "e" - }, - { - "a": "c", - "d": "f" - } - ] - }, - { - "name": "filter, not-equals true", - "selector": "$[?@.a!=true]", - "document": [ - { - "a": true, - "d": "e" - }, - { - "a": "c", - "d": "f" - } - ], - "result": [ - { - "a": "c", - "d": "f" - } - ] - }, - { - "name": "filter, not-equals false", - "selector": "$[?@.a!=false]", - "document": [ - { - "a": false, - "d": "e" - }, - { - "a": "c", - "d": "f" - } - ], - "result": [ - { - "a": "c", - "d": "f" - } - ] - }, - { - "name": "filter, less than string, single quotes", - "selector": "$[?@.a<'c']", - "document": [ - { - "a": "b", - "d": "e" - }, - { - "a": "c", - "d": "f" - } - ], - "result": [ - { - "a": "b", - "d": "e" - } - ] - }, - { - "name": "filter, less than string, double quotes", - "selector": "$[?@.a<\"c\"]", - "document": [ - { - "a": "b", - "d": "e" - }, - { - "a": "c", - "d": "f" - } - ], - "result": [ - { - "a": "b", - "d": "e" - } - ] - }, - { - "name": "filter, less than number", - "selector": "$[?@.a<10]", - "document": [ - { - "a": 1, - "d": "e" - }, - { - "a": 10, - "d": "e" - }, - { - "a": "c", - "d": "f" - }, - { - "a": 20, - "d": "f" - } - ], - "result": [ - { - "a": 1, - "d": "e" - } - ] - }, - { - "name": "filter, less than null", - "selector": "$[?@.a'c']", - "document": [ - { - "a": "b", - "d": "e" - }, - { - "a": "c", - "d": "f" - }, - { - "a": "d", - "d": "f" - } - ], - "result": [ - { - "a": "d", - "d": "f" - } - ] - }, - { - "name": "filter, greater than string, double quotes", - "selector": "$[?@.a>\"c\"]", - "document": [ - { - "a": "b", - "d": "e" - }, - { - "a": "c", - "d": "f" - }, - { - "a": "d", - "d": "f" - } - ], - "result": [ - { - "a": "d", - "d": "f" - } - ] - }, - { - "name": "filter, greater than number", - "selector": "$[?@.a>10]", - "document": [ - { - "a": 1, - "d": "e" - }, - { - "a": 10, - "d": "e" - }, - { - "a": "c", - "d": "f" - }, - { - "a": 20, - "d": "f" - } - ], - "result": [ - { - "a": 20, - "d": "f" - } - ] - }, - { - "name": "filter, greater than null", - "selector": "$[?@.a>null]", - "document": [ - { - "a": null, - "d": "e" - }, - { - "a": "c", - "d": "f" - } - ], - "result": [] - }, - { - "name": "filter, greater than true", - "selector": "$[?@.a>true]", - "document": [ - { - "a": true, - "d": "e" - }, - { - "a": "c", - "d": "f" - } - ], - "result": [] - }, - { - "name": "filter, greater than false", - "selector": "$[?@.a>false]", - "document": [ - { - "a": false, - "d": "e" - }, - { - "a": "c", - "d": "f" - } - ], - "result": [] - }, - { - "name": "filter, greater than or equal to string, single quotes", - "selector": "$[?@.a>='c']", - "document": [ - { - "a": "b", - "d": "e" - }, - { - "a": "c", - "d": "f" - }, - { - "a": "d", - "d": "f" - } - ], - "result": [ - { - "a": "c", - "d": "f" - }, - { - "a": "d", - "d": "f" - } - ] - }, - { - "name": "filter, greater than or equal to string, double quotes", - "selector": "$[?@.a>=\"c\"]", - "document": [ - { - "a": "b", - "d": "e" - }, - { - "a": "c", - "d": "f" - }, - { - "a": "d", - "d": "f" - } - ], - "result": [ - { - "a": "c", - "d": "f" - }, - { - "a": "d", - "d": "f" - } - ] - }, - { - "name": "filter, greater than or equal to number", - "selector": "$[?@.a>=10]", - "document": [ - { - "a": 1, - "d": "e" - }, - { - "a": 10, - "d": "e" - }, - { - "a": "c", - "d": "f" - }, - { - "a": 20, - "d": "f" - } - ], - "result": [ - { - "a": 10, - "d": "e" - }, - { - "a": 20, - "d": "f" - } - ] - }, - { - "name": "filter, greater than or equal to null", - "selector": "$[?@.a>=null]", - "document": [ - { - "a": null, - "d": "e" - }, - { - "a": "c", - "d": "f" - } - ], - "result": [ - { - "a": null, - "d": "e" - } - ] - }, - { - "name": "filter, greater than or equal to true", - "selector": "$[?@.a>=true]", - "document": [ - { - "a": true, - "d": "e" - }, - { - "a": "c", - "d": "f" - } - ], - "result": [ - { - "a": true, - "d": "e" - } - ] - }, - { - "name": "filter, greater than or equal to false", - "selector": "$[?@.a>=false]", - "document": [ - { - "a": false, - "d": "e" - }, - { - "a": "c", - "d": "f" - } - ], - "result": [ - { - "a": false, - "d": "e" - } - ] - }, - { - "name": "filter, exists and not-equals null, absent from data", - "selector": "$[?@.a&&@.a!=null]", - "document": [ - { - "d": "e" - }, - { - "a": "c", - "d": "f" - } - ], - "result": [ - { - "a": "c", - "d": "f" - } - ] - }, - { - "name": "filter, exists and exists, data false", - "selector": "$[?@.a&&@.b]", - "document": [ - { - "a": false, - "b": false - }, - { - "b": false - }, - { - "c": false - } - ], - "result": [ - { - "a": false, - "b": false - } - ] - }, - { - "name": "filter, exists or exists, data false", - "selector": "$[?@.a||@.b]", - "document": [ - { - "a": false, - "b": false - }, - { - "b": false - }, - { - "c": false - } - ], - "result": [ - { - "a": false, - "b": false - }, - { - "b": false - } - ] - }, - { - "name": "filter, and", - "selector": "$[?@.a>0&&@.a<10]", - "document": [ - { - "a": -10, - "d": "e" - }, - { - "a": 5, - "d": "f" - }, - { - "a": 20, - "d": "f" - } - ], - "result": [ - { - "a": 5, - "d": "f" - } - ] - }, - { - "name": "filter, or", - "selector": "$[?@.a=='b'||@.a=='d']", - "document": [ - { - "a": "a", - "d": "e" - }, - { - "a": "b", - "d": "f" - }, - { - "a": "c", - "d": "f" - }, - { - "a": "d", - "d": "f" - } - ], - "result": [ - { - "a": "b", - "d": "f" - }, - { - "a": "d", - "d": "f" - } - ] - }, - { - "name": "filter, not expression", - "selector": "$[?!(@.a=='b')]", - "document": [ - { - "a": "a", - "d": "e" - }, - { - "a": "b", - "d": "f" - }, - { - "a": "d", - "d": "f" - } - ], - "result": [ - { - "a": "a", - "d": "e" - }, - { - "a": "d", - "d": "f" - } - ] - }, - { - "name": "filter, not exists", - "selector": "$[?!@.a]", - "document": [ - { - "a": "a", - "d": "e" - }, - { - "d": "f" - }, - { - "a": "d", - "d": "f" - } - ], - "result": [ - { - "d": "f" - } - ] - }, - { - "name": "filter, not exists, data null", - "selector": "$[?!@.a]", - "document": [ - { - "a": null, - "d": "e" - }, - { - "d": "f" - }, - { - "a": "d", - "d": "f" - } - ], - "result": [ - { - "d": "f" - } - ] - }, - { - "name": "filter, non-singular existence, wildcard", - "selector": "$[?@.*]", - "document": [ - 1, - [], - [ - 2 - ], - {}, - { - "a": 3 - } - ], - "result": [ - [ - 2 - ], - { - "a": 3 - } - ] - }, - { - "name": "filter, non-singular existence, multiple", - "selector": "$[?@[0, 0, 'a']]", - "document": [ - 1, - [], - [ - 2 - ], - [ - 2, - 3 - ], - { - "a": 3 - }, - { - "b": 4 - }, - { - "a": 3, - "b": 4 - } - ], - "result": [ - [ - 2 - ], - [ - 2, - 3 - ], - { - "a": 3 - }, - { - "a": 3, - "b": 4 - } - ] - }, - { - "name": "filter, non-singular existence, slice", - "selector": "$[?@[0:2]]", - "document": [ - 1, - [], - [ - 2 - ], - [ - 2, - 3, - 4 - ], - {}, - { - "a": 3 - } - ], - "result": [ - [ - 2 - ], - [ - 2, - 3, - 4 - ] - ] - }, - { - "name": "filter, non-singular existence, negated", - "selector": "$[?!@.*]", - "document": [ - 1, - [], - [ - 2 - ], - {}, - { - "a": 3 - } - ], - "result": [ - 1, - [], - {} - ] - }, - { - "name": "filter, non-singular query in comparison, slice", - "selector": "$[?@[0:0]==0]", - "invalid_selector": true - }, - { - "name": "filter, non-singular query in comparison, all children", - "selector": "$[?@[*]==0]", - "invalid_selector": true - }, - { - "name": "filter, non-singular query in comparison, descendants", - "selector": "$[?@..a==0]", - "invalid_selector": true - }, - { - "name": "filter, non-singular query in comparison, combined", - "selector": "$[?@.a[*].a==0]", - "invalid_selector": true - }, - { - "name": "filter, nested", - "selector": "$[?@[?@>1]]", - "document": [ - [ - 0 - ], - [ - 0, - 1 - ], - [ - 0, - 1, - 2 - ], - [ - 42 - ] - ], - "result": [ - [ - 0, - 1, - 2 - ], - [ - 42 - ] - ] - }, - { - "name": "filter, name segment on primitive, selects nothing", - "selector": "$[?@.a == 1]", - "document": { - "a": 1 - }, - "result": [] - }, - { - "name": "filter, name segment on array, selects nothing", - "selector": "$[?@['0'] == 5]", - "document": [ - [ - 5, - 6 - ] - ], - "result": [] - }, - { - "name": "filter, index segment on object, selects nothing", - "selector": "$[?@[0] == 5]", - "document": [ - { - "0": 5 - } - ], - "result": [] - }, - { - "name": "filter, relative non-singular query, index, equal", - "selector": "$[?(@[0, 0]==42)]", - "invalid_selector": true, - "tags": [ - "whitespace" - ] - }, - { - "name": "filter, relative non-singular query, index, not equal", - "selector": "$[?(@[0, 0]!=42)]", - "invalid_selector": true, - "tags": [ - "whitespace" - ] - }, - { - "name": "filter, relative non-singular query, index, less-or-equal", - "selector": "$[?(@[0, 0]<=42)]", - "invalid_selector": true, - "tags": [ - "whitespace" - ] - }, - { - "name": "filter, relative non-singular query, name, equal", - "selector": "$[?(@['a', 'a']==42)]", - "invalid_selector": true, - "tags": [ - "whitespace" - ] - }, - { - "name": "filter, relative non-singular query, name, not equal", - "selector": "$[?(@['a', 'a']!=42)]", - "invalid_selector": true, - "tags": [ - "whitespace" - ] - }, - { - "name": "filter, relative non-singular query, name, less-or-equal", - "selector": "$[?(@['a', 'a']<=42)]", - "invalid_selector": true, - "tags": [ - "whitespace" - ] - }, - { - "name": "filter, relative non-singular query, combined, equal", - "selector": "$[?(@[0, '0']==42)]", - "invalid_selector": true, - "tags": [ - "whitespace" - ] - }, - { - "name": "filter, relative non-singular query, combined, not equal", - "selector": "$[?(@[0, '0']!=42)]", - "invalid_selector": true, - "tags": [ - "whitespace" - ] - }, - { - "name": "filter, relative non-singular query, combined, less-or-equal", - "selector": "$[?(@[0, '0']<=42)]", - "invalid_selector": true, - "tags": [ - "whitespace" - ] - }, - { - "name": "filter, relative non-singular query, wildcard, equal", - "selector": "$[?(@.*==42)]", - "invalid_selector": true - }, - { - "name": "filter, relative non-singular query, wildcard, not equal", - "selector": "$[?(@.*!=42)]", - "invalid_selector": true - }, - { - "name": "filter, relative non-singular query, wildcard, less-or-equal", - "selector": "$[?(@.*<=42)]", - "invalid_selector": true - }, - { - "name": "filter, relative non-singular query, slice, equal", - "selector": "$[?(@[0:0]==42)]", - "invalid_selector": true - }, - { - "name": "filter, relative non-singular query, slice, not equal", - "selector": "$[?(@[0:0]!=42)]", - "invalid_selector": true - }, - { - "name": "filter, relative non-singular query, slice, less-or-equal", - "selector": "$[?(@[0:0]<=42)]", - "invalid_selector": true - }, - { - "name": "filter, absolute non-singular query, index, equal", - "selector": "$[?($[0, 0]==42)]", - "invalid_selector": true, - "tags": [ - "whitespace" - ] - }, - { - "name": "filter, absolute non-singular query, index, not equal", - "selector": "$[?($[0, 0]!=42)]", - "invalid_selector": true, - "tags": [ - "whitespace" - ] - }, - { - "name": "filter, absolute non-singular query, index, less-or-equal", - "selector": "$[?($[0, 0]<=42)]", - "invalid_selector": true, - "tags": [ - "whitespace" - ] - }, - { - "name": "filter, absolute non-singular query, name, equal", - "selector": "$[?($['a', 'a']==42)]", - "invalid_selector": true, - "tags": [ - "whitespace" - ] - }, - { - "name": "filter, absolute non-singular query, name, not equal", - "selector": "$[?($['a', 'a']!=42)]", - "invalid_selector": true, - "tags": [ - "whitespace" - ] - }, - { - "name": "filter, absolute non-singular query, name, less-or-equal", - "selector": "$[?($['a', 'a']<=42)]", - "invalid_selector": true, - "tags": [ - "whitespace" - ] - }, - { - "name": "filter, absolute non-singular query, combined, equal", - "selector": "$[?($[0, '0']==42)]", - "invalid_selector": true, - "tags": [ - "whitespace" - ] - }, - { - "name": "filter, absolute non-singular query, combined, not equal", - "selector": "$[?($[0, '0']!=42)]", - "invalid_selector": true, - "tags": [ - "whitespace" - ] - }, - { - "name": "filter, absolute non-singular query, combined, less-or-equal", - "selector": "$[?($[0, '0']<=42)]", - "invalid_selector": true, - "tags": [ - "whitespace" - ] - }, - { - "name": "filter, absolute non-singular query, wildcard, equal", - "selector": "$[?($.*==42)]", - "invalid_selector": true - }, - { - "name": "filter, absolute non-singular query, wildcard, not equal", - "selector": "$[?($.*!=42)]", - "invalid_selector": true - }, - { - "name": "filter, absolute non-singular query, wildcard, less-or-equal", - "selector": "$[?($.*<=42)]", - "invalid_selector": true - }, - { - "name": "filter, absolute non-singular query, slice, equal", - "selector": "$[?($[0:0]==42)]", - "invalid_selector": true - }, - { - "name": "filter, absolute non-singular query, slice, not equal", - "selector": "$[?($[0:0]!=42)]", - "invalid_selector": true - }, - { - "name": "filter, absolute non-singular query, slice, less-or-equal", - "selector": "$[?($[0:0]<=42)]", - "invalid_selector": true - }, - { - "name": "filter, multiple selectors", - "selector": "$[?@.a,?@.b]", - "document": [ - { - "a": "b", - "d": "e" - }, - { - "b": "c", - "d": "f" - } - ], - "result": [ - { - "a": "b", - "d": "e" - }, - { - "b": "c", - "d": "f" - } - ] - }, - { - "name": "filter, multiple selectors, comparison", - "selector": "$[?@.a=='b',?@.b=='x']", - "document": [ - { - "a": "b", - "d": "e" - }, - { - "b": "c", - "d": "f" - } - ], - "result": [ - { - "a": "b", - "d": "e" - } - ] - }, - { - "name": "filter, multiple selectors, overlapping", - "selector": "$[?@.a,?@.d]", - "document": [ - { - "a": "b", - "d": "e" - }, - { - "b": "c", - "d": "f" - } - ], - "result": [ - { - "a": "b", - "d": "e" - }, - { - "a": "b", - "d": "e" - }, - { - "b": "c", - "d": "f" - } - ] - }, - { - "name": "filter, multiple selectors, filter and index", - "selector": "$[?@.a,1]", - "document": [ - { - "a": "b", - "d": "e" - }, - { - "b": "c", - "d": "f" - } - ], - "result": [ - { - "a": "b", - "d": "e" - }, - { - "b": "c", - "d": "f" - } - ] - }, - { - "name": "filter, multiple selectors, filter and wildcard", - "selector": "$[?@.a,*]", - "document": [ - { - "a": "b", - "d": "e" - }, - { - "b": "c", - "d": "f" - } - ], - "result": [ - { - "a": "b", - "d": "e" - }, - { - "a": "b", - "d": "e" - }, - { - "b": "c", - "d": "f" - } - ] - }, - { - "name": "filter, multiple selectors, filter and slice", - "selector": "$[?@.a,1:]", - "document": [ - { - "a": "b", - "d": "e" - }, - { - "b": "c", - "d": "f" - }, - { - "g": "h" - } - ], - "result": [ - { - "a": "b", - "d": "e" - }, - { - "b": "c", - "d": "f" - }, - { - "g": "h" - } - ] - }, - { - "name": "filter, multiple selectors, comparison filter, index and slice", - "selector": "$[1, ?@.a=='b', 1:]", - "document": [ - { - "a": "b", - "d": "e" - }, - { - "b": "c", - "d": "f" - } - ], - "result": [ - { - "b": "c", - "d": "f" - }, - { - "a": "b", - "d": "e" - }, - { - "b": "c", - "d": "f" - } - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "filter, equals number, zero and negative zero", - "selector": "$[?@.a==0]", - "document": [ - { - "a": 0, - "d": "e" - }, - { - "a": 0.1, - "d": "f" - }, - { - "a": "0", - "d": "g" - } - ], - "result": [ - { - "a": 0, - "d": "e" - } - ] - }, - { - "name": "filter, equals number, negative zero and zero", - "selector": "$[?@.a==-0]", - "document": [ - { - "a": 0, - "d": "e" - }, - { - "a": 0.1, - "d": "f" - }, - { - "a": "0", - "d": "g" - } - ], - "result": [ - { - "a": 0, - "d": "e" - } - ] - }, - { - "name": "filter, equals number, with and without decimal fraction", - "selector": "$[?@.a==1.0]", - "document": [ - { - "a": 1, - "d": "e" - }, - { - "a": 2, - "d": "f" - }, - { - "a": "1", - "d": "g" - } - ], - "result": [ - { - "a": 1, - "d": "e" - } - ] - }, - { - "name": "filter, equals number, exponent", - "selector": "$[?@.a==1e2]", - "document": [ - { - "a": 100, - "d": "e" - }, - { - "a": 100.1, - "d": "f" - }, - { - "a": "100", - "d": "g" - } - ], - "result": [ - { - "a": 100, - "d": "e" - } - ] - }, - { - "name": "filter, equals number, exponent upper e", - "selector": "$[?@.a==1E2]", - "document": [ - { - "a": 100, - "d": "e" - }, - { - "a": 100.1, - "d": "f" - }, - { - "a": "100", - "d": "g" - } - ], - "result": [ - { - "a": 100, - "d": "e" - } - ] - }, - { - "name": "filter, equals number, positive exponent", - "selector": "$[?@.a==1e+2]", - "document": [ - { - "a": 100, - "d": "e" - }, - { - "a": 100.1, - "d": "f" - }, - { - "a": "100", - "d": "g" - } - ], - "result": [ - { - "a": 100, - "d": "e" - } - ] - }, - { - "name": "filter, equals number, negative exponent", - "selector": "$[?@.a==1e-2]", - "document": [ - { - "a": 0.01, - "d": "e" - }, - { - "a": 0.02, - "d": "f" - }, - { - "a": "0.01", - "d": "g" - } - ], - "result": [ - { - "a": 0.01, - "d": "e" - } - ] - }, - { - "name": "filter, equals number, exponent 0", - "selector": "$[?@.a==1e0]", - "document": [ - { - "a": 1, - "d": "e" - }, - { - "a": 2, - "d": "f" - }, - { - "a": "1", - "d": "g" - } - ], - "result": [ - { - "a": 1, - "d": "e" - } - ] - }, - { - "name": "filter, equals number, exponent -0", - "selector": "$[?@.a==1e-0]", - "document": [ - { - "a": 1, - "d": "e" - }, - { - "a": 2, - "d": "f" - }, - { - "a": "1", - "d": "g" - } - ], - "result": [ - { - "a": 1, - "d": "e" - } - ] - }, - { - "name": "filter, equals number, exponent +0", - "selector": "$[?@.a==1e+0]", - "document": [ - { - "a": 1, - "d": "e" - }, - { - "a": 2, - "d": "f" - }, - { - "a": "1", - "d": "g" - } - ], - "result": [ - { - "a": 1, - "d": "e" - } - ] - }, - { - "name": "filter, equals number, exponent leading -0", - "selector": "$[?@.a==1e-02]", - "document": [ - { - "a": 0.01, - "d": "e" - }, - { - "a": 0.02, - "d": "f" - }, - { - "a": "0.01", - "d": "g" - } - ], - "result": [ - { - "a": 0.01, - "d": "e" - } - ] - }, - { - "name": "filter, equals number, exponent +00", - "selector": "$[?@.a==1e+00]", - "document": [ - { - "a": 1, - "d": "e" - }, - { - "a": 2, - "d": "f" - }, - { - "a": "1", - "d": "g" - } - ], - "result": [ - { - "a": 1, - "d": "e" - } - ] - }, - { - "name": "filter, equals number, decimal fraction", - "selector": "$[?@.a==1.1]", - "document": [ - { - "a": 1.1, - "d": "e" - }, - { - "a": 1, - "d": "f" - }, - { - "a": "1.1", - "d": "g" - } - ], - "result": [ - { - "a": 1.1, - "d": "e" - } - ] - }, - { - "name": "filter, equals number, decimal fraction, trailing 0", - "selector": "$[?@.a==1.10]", - "document": [ - { - "a": 1.1, - "d": "e" - }, - { - "a": 1, - "d": "f" - }, - { - "a": "1.1", - "d": "g" - } - ], - "result": [ - { - "a": 1.1, - "d": "e" - } - ] - }, - { - "name": "filter, equals number, decimal fraction, exponent", - "selector": "$[?@.a==1.1e2]", - "document": [ - { - "a": 110, - "d": "e" - }, - { - "a": 110.1, - "d": "f" - }, - { - "a": "110", - "d": "g" - } - ], - "result": [ - { - "a": 110, - "d": "e" - } - ] - }, - { - "name": "filter, equals number, decimal fraction, positive exponent", - "selector": "$[?@.a==1.1e+2]", - "document": [ - { - "a": 110, - "d": "e" - }, - { - "a": 110.1, - "d": "f" - }, - { - "a": "110", - "d": "g" - } - ], - "result": [ - { - "a": 110, - "d": "e" - } - ] - }, - { - "name": "filter, equals number, decimal fraction, negative exponent", - "selector": "$[?@.a==1.1e-2]", - "document": [ - { - "a": 0.011, - "d": "e" - }, - { - "a": 0.012, - "d": "f" - }, - { - "a": "0.011", - "d": "g" - } - ], - "result": [ - { - "a": 0.011, - "d": "e" - } - ] - }, - { - "name": "filter, equals number, invalid plus", - "selector": "$[?@.a==+1]", - "invalid_selector": true - }, - { - "name": "filter, equals number, invalid minus space", - "selector": "$[?@.a==- 1]", - "invalid_selector": true - }, - { - "name": "filter, equals number, invalid double minus", - "selector": "$[?@.a==--1]", - "invalid_selector": true - }, - { - "name": "filter, equals number, invalid no int digit", - "selector": "$[?@.a==.1]", - "invalid_selector": true - }, - { - "name": "filter, equals number, invalid minus no int digit", - "selector": "$[?@.a==-.1]", - "invalid_selector": true - }, - { - "name": "filter, equals number, invalid 00", - "selector": "$[?@.a==00]", - "invalid_selector": true - }, - { - "name": "filter, equals number, invalid leading 0", - "selector": "$[?@.a==01]", - "invalid_selector": true - }, - { - "name": "filter, equals number, invalid no fractional digit", - "selector": "$[?@.a==1.]", - "invalid_selector": true - }, - { - "name": "filter, equals number, invalid middle minus", - "selector": "$[?@.a==1.-1]", - "invalid_selector": true - }, - { - "name": "filter, equals number, invalid no fractional digit e", - "selector": "$[?@.a==1.e1]", - "invalid_selector": true - }, - { - "name": "filter, equals number, invalid no e digit", - "selector": "$[?@.a==1e]", - "invalid_selector": true - }, - { - "name": "filter, equals number, invalid no e digit minus", - "selector": "$[?@.a==1e-]", - "invalid_selector": true - }, - { - "name": "filter, equals number, invalid double e", - "selector": "$[?@.a==1eE1]", - "invalid_selector": true - }, - { - "name": "filter, equals number, invalid e digit double minus", - "selector": "$[?@.a==1e--1]", - "invalid_selector": true - }, - { - "name": "filter, equals number, invalid e digit plus minus", - "selector": "$[?@.a==1e+-1]", - "invalid_selector": true - }, - { - "name": "filter, equals number, invalid e decimal", - "selector": "$[?@.a==1e2.3]", - "invalid_selector": true - }, - { - "name": "filter, equals number, invalid multi e", - "selector": "$[?@.a==1e2e3]", - "invalid_selector": true - }, - { - "name": "filter, equals, special nothing", - "selector": "$.values[?length(@.a) == value($..c)]", - "document": { - "c": "cd", - "values": [ - { - "a": "ab" - }, - { - "c": "d" - }, - { - "a": null - } - ] - }, - "result": [ - { - "c": "d" - }, - { - "a": null - } - ] - }, - { - "name": "filter, equals, empty node list and empty node list", - "selector": "$[?@.a == @.b]", - "document": [ - { - "a": 1 - }, - { - "b": 2 - }, - { - "c": 3 - } - ], - "result": [ - { - "c": 3 - } - ] - }, - { - "name": "filter, equals, empty node list and special nothing", - "selector": "$[?@.a == length(@.b)]", - "document": [ - { - "a": 1 - }, - { - "b": 2 - }, - { - "c": 3 - } - ], - "result": [ - { - "b": 2 - }, - { - "c": 3 - } - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "filter, object data", - "selector": "$[?@<3]", - "document": { - "a": 1, - "b": 2, - "c": 3 - }, - "results": [ - [ - 1, - 2 - ], - [ - 2, - 1 - ] - ] - }, - { - "name": "filter, and binds more tightly than or", - "selector": "$[?@.a || @.b && @.c]", - "document": [ - { - "a": 1 - }, - { - "b": 2, - "c": 3 - }, - { - "c": 3 - }, - { - "b": 2 - }, - { - "a": 1, - "b": 2, - "c": 3 - } - ], - "result": [ - { - "a": 1 - }, - { - "b": 2, - "c": 3 - }, - { - "a": 1, - "b": 2, - "c": 3 - } - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "filter, left to right evaluation", - "selector": "$[?@.a && @.b || @.c]", - "document": [ - { - "a": 1 - }, - { - "b": 2 - }, - { - "a": 1, - "b": 2 - }, - { - "a": 1, - "c": 3 - }, - { - "b": 1, - "c": 3 - }, - { - "c": 3 - }, - { - "a": 1, - "b": 2, - "c": 3 - } - ], - "result": [ - { - "a": 1, - "b": 2 - }, - { - "a": 1, - "c": 3 - }, - { - "b": 1, - "c": 3 - }, - { - "c": 3 - }, - { - "a": 1, - "b": 2, - "c": 3 - } - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "filter, group terms, left", - "selector": "$[?(@.a || @.b) && @.c]", - "document": [ - { - "a": 1, - "b": 2 - }, - { - "a": 1, - "c": 3 - }, - { - "b": 2, - "c": 3 - }, - { - "a": 1 - }, - { - "b": 2 - }, - { - "c": 3 - }, - { - "a": 1, - "b": 2, - "c": 3 - } - ], - "result": [ - { - "a": 1, - "c": 3 - }, - { - "b": 2, - "c": 3 - }, - { - "a": 1, - "b": 2, - "c": 3 - } - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "filter, group terms, right", - "selector": "$[?@.a && (@.b || @.c)]", - "document": [ - { - "a": 1 - }, - { - "a": 1, - "b": 2 - }, - { - "a": 1, - "c": 2 - }, - { - "b": 2 - }, - { - "c": 2 - }, - { - "a": 1, - "b": 2, - "c": 3 - } - ], - "result": [ - { - "a": 1, - "b": 2 - }, - { - "a": 1, - "c": 2 - }, - { - "a": 1, - "b": 2, - "c": 3 - } - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "filter, string literal, single quote in double quotes", - "selector": "$[?@ == \"quoted' literal\"]", - "document": [ - "quoted' literal", - "a", - "quoted\\' literal" - ], - "result": [ - "quoted' literal" - ] - }, - { - "name": "filter, string literal, double quote in single quotes", - "selector": "$[?@ == 'quoted\" literal']", - "document": [ - "quoted\" literal", - "a", - "quoted\\\" literal", - "'quoted\" literal'" - ], - "result": [ - "quoted\" literal" - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "filter, string literal, escaped single quote in single quotes", - "selector": "$[?@ == 'quoted\\' literal']", - "document": [ - "quoted' literal", - "a", - "quoted\\' literal", - "'quoted\" literal'" - ], - "result": [ - "quoted' literal" - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "filter, string literal, escaped double quote in double quotes", - "selector": "$[?@ == \"quoted\\\" literal\"]", - "document": [ - "quoted\" literal", - "a", - "quoted\\\" literal", - "'quoted\" literal'" - ], - "result": [ - "quoted\" literal" - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "filter, literal true must be compared", - "selector": "$[?true]", - "invalid_selector": true - }, - { - "name": "filter, literal false must be compared", - "selector": "$[?false]", - "invalid_selector": true - }, - { - "name": "filter, literal string must be compared", - "selector": "$[?'abc']", - "invalid_selector": true - }, - { - "name": "filter, literal int must be compared", - "selector": "$[?2]", - "invalid_selector": true - }, - { - "name": "filter, literal float must be compared", - "selector": "$[?2.2]", - "invalid_selector": true - }, - { - "name": "filter, literal null must be compared", - "selector": "$[?null]", - "invalid_selector": true - }, - { - "name": "filter, and, literals must be compared", - "selector": "$[?true && false]", - "invalid_selector": true, - "tags": [ - "whitespace" - ] - }, - { - "name": "filter, or, literals must be compared", - "selector": "$[?true || false]", - "invalid_selector": true, - "tags": [ - "whitespace" - ] - }, - { - "name": "filter, and, right hand literal must be compared", - "selector": "$[?true == false && false]", - "invalid_selector": true, - "tags": [ - "whitespace" - ] - }, - { - "name": "filter, or, right hand literal must be compared", - "selector": "$[?true == false || false]", - "invalid_selector": true, - "tags": [ - "whitespace" - ] - }, - { - "name": "filter, and, left hand literal must be compared", - "selector": "$[?false && true == false]", - "invalid_selector": true, - "tags": [ - "whitespace" - ] - }, - { - "name": "filter, or, left hand literal must be compared", - "selector": "$[?false || true == false]", - "invalid_selector": true, - "tags": [ - "whitespace" - ] - }, - { - "name": "filter, true, incorrectly capitalized", - "selector": "$[?@==True]", - "invalid_selector": true, - "tags": [ - "case" - ] - }, - { - "name": "filter, false, incorrectly capitalized", - "selector": "$[?@==False]", - "invalid_selector": true, - "tags": [ - "case" - ] - }, - { - "name": "filter, null, incorrectly capitalized", - "selector": "$[?@==Null]", - "invalid_selector": true, - "tags": [ - "case" - ] - }, - { - "name": "index selector, first element", - "selector": "$[0]", - "document": [ - "first", - "second" - ], - "result": [ - "first" - ], - "tags": [ - "index" - ] - }, - { - "name": "index selector, second element", - "selector": "$[1]", - "document": [ - "first", - "second" - ], - "result": [ - "second" - ], - "tags": [ - "index" - ] - }, - { - "name": "index selector, out of bound", - "selector": "$[2]", - "document": [ - "first", - "second" - ], - "result": [], - "tags": [ - "boundary", - "index" - ] - }, - { - "name": "index selector, min exact index", - "selector": "$[-9007199254740991]", - "document": [ - "first", - "second" - ], - "result": [], - "tags": [ - "boundary", - "index" - ] - }, - { - "name": "index selector, max exact index", - "selector": "$[9007199254740991]", - "document": [ - "first", - "second" - ], - "result": [], - "tags": [ - "boundary", - "index" - ] - }, - { - "name": "index selector, min exact index - 1", - "selector": "$[-9007199254740992]", - "invalid_selector": true, - "tags": [ - "boundary", - "index" - ] - }, - { - "name": "index selector, max exact index + 1", - "selector": "$[9007199254740992]", - "invalid_selector": true, - "tags": [ - "boundary", - "index" - ] - }, - { - "name": "index selector, overflowing index", - "selector": "$[231584178474632390847141970017375815706539969331281128078915168015826259279872]", - "invalid_selector": true, - "tags": [ - "boundary", - "index" - ] - }, - { - "name": "index selector, not actually an index, overflowing index leads into general text", - "selector": "$[231584178474632390847141970017375815706539969331281128078915168SomeRandomText]", - "invalid_selector": true, - "tags": [ - "index" - ] - }, - { - "name": "index selector, negative", - "selector": "$[-1]", - "document": [ - "first", - "second" - ], - "result": [ - "second" - ], - "tags": [ - "index" - ] - }, - { - "name": "index selector, more negative", - "selector": "$[-2]", - "document": [ - "first", - "second" - ], - "result": [ - "first" - ], - "tags": [ - "index" - ] - }, - { - "name": "index selector, negative out of bound", - "selector": "$[-3]", - "document": [ - "first", - "second" - ], - "result": [], - "tags": [ - "boundary", - "index" - ] - }, - { - "name": "index selector, on object", - "selector": "$[0]", - "document": { - "foo": 1 - }, - "result": [], - "tags": [ - "index" - ] - }, - { - "name": "index selector, leading 0", - "selector": "$[01]", - "invalid_selector": true, - "tags": [ - "index" - ] - }, - { - "name": "index selector, decimal", - "selector": "$[1.0]", - "invalid_selector": true, - "tags": [ - "index" - ] - }, - { - "name": "index selector, plus", - "selector": "$[+1]", - "invalid_selector": true, - "tags": [ - "index" - ] - }, - { - "name": "index selector, minus space", - "selector": "$[- 1]", - "invalid_selector": true, - "tags": [ - "index", - "whitespace" - ] - }, - { - "name": "index selector, -0", - "selector": "$[-0]", - "invalid_selector": true, - "tags": [ - "index" - ] - }, - { - "name": "index selector, leading -0", - "selector": "$[-01]", - "invalid_selector": true, - "tags": [ - "index" - ] - }, - { - "name": "name selector, double quotes", - "selector": "$[\"a\"]", - "document": { - "a": "A", - "b": "B" - }, - "result": [ - "A" - ] - }, - { - "name": "name selector, double quotes, absent data", - "selector": "$[\"c\"]", - "document": { - "a": "A", - "b": "B" - }, - "result": [] - }, - { - "name": "name selector, double quotes, array data", - "selector": "$[\"a\"]", - "document": [ - "first", - "second" - ], - "result": [] - }, - { - "name": "name selector, double quotes, embedded U+0000", - "selector": "$[\"\u0000\"]", - "invalid_selector": true, - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, double quotes, embedded U+0001", - "selector": "$[\"\u0001\"]", - "invalid_selector": true, - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, double quotes, embedded U+0002", - "selector": "$[\"\u0002\"]", - "invalid_selector": true, - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, double quotes, embedded U+0003", - "selector": "$[\"\u0003\"]", - "invalid_selector": true, - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, double quotes, embedded U+0004", - "selector": "$[\"\u0004\"]", - "invalid_selector": true, - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, double quotes, embedded U+0005", - "selector": "$[\"\u0005\"]", - "invalid_selector": true, - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, double quotes, embedded U+0006", - "selector": "$[\"\u0006\"]", - "invalid_selector": true, - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, double quotes, embedded U+0007", - "selector": "$[\"\u0007\"]", - "invalid_selector": true, - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, double quotes, embedded U+0008", - "selector": "$[\"\b\"]", - "invalid_selector": true, - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, double quotes, embedded U+0009", - "selector": "$[\"\t\"]", - "invalid_selector": true, - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, double quotes, embedded U+000A", - "selector": "$[\"\n\"]", - "invalid_selector": true, - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, double quotes, embedded U+000B", - "selector": "$[\"\u000b\"]", - "invalid_selector": true, - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, double quotes, embedded U+000C", - "selector": "$[\"\f\"]", - "invalid_selector": true, - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, double quotes, embedded U+000D", - "selector": "$[\"\r\"]", - "invalid_selector": true, - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, double quotes, embedded U+000E", - "selector": "$[\"\u000e\"]", - "invalid_selector": true, - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, double quotes, embedded U+000F", - "selector": "$[\"\u000f\"]", - "invalid_selector": true, - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, double quotes, embedded U+0010", - "selector": "$[\"\u0010\"]", - "invalid_selector": true, - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, double quotes, embedded U+0011", - "selector": "$[\"\u0011\"]", - "invalid_selector": true, - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, double quotes, embedded U+0012", - "selector": "$[\"\u0012\"]", - "invalid_selector": true, - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, double quotes, embedded U+0013", - "selector": "$[\"\u0013\"]", - "invalid_selector": true, - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, double quotes, embedded U+0014", - "selector": "$[\"\u0014\"]", - "invalid_selector": true, - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, double quotes, embedded U+0015", - "selector": "$[\"\u0015\"]", - "invalid_selector": true, - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, double quotes, embedded U+0016", - "selector": "$[\"\u0016\"]", - "invalid_selector": true, - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, double quotes, embedded U+0017", - "selector": "$[\"\u0017\"]", - "invalid_selector": true, - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, double quotes, embedded U+0018", - "selector": "$[\"\u0018\"]", - "invalid_selector": true, - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, double quotes, embedded U+0019", - "selector": "$[\"\u0019\"]", - "invalid_selector": true, - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, double quotes, embedded U+001A", - "selector": "$[\"\u001a\"]", - "invalid_selector": true, - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, double quotes, embedded U+001B", - "selector": "$[\"\u001b\"]", - "invalid_selector": true, - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, double quotes, embedded U+001C", - "selector": "$[\"\u001c\"]", - "invalid_selector": true, - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, double quotes, embedded U+001D", - "selector": "$[\"\u001d\"]", - "invalid_selector": true, - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, double quotes, embedded U+001E", - "selector": "$[\"\u001e\"]", - "invalid_selector": true, - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, double quotes, embedded U+001F", - "selector": "$[\"\u001f\"]", - "invalid_selector": true, - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, double quotes, embedded U+0020", - "selector": "$[\" \"]", - "document": { - " ": "A" - }, - "result": [ - "A" - ], - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, double quotes, embedded U+007F", - "selector": "$[\"\u007f\"]", - "document": { - "\u007f": "A" - }, - "result": [ - "A" - ], - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, double quotes, supplementary plane character", - "selector": "$[\"𝄞\"]", - "document": { - "𝄞": "A" - }, - "result": [ - "A" - ], - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, double quotes, escaped double quote", - "selector": "$[\"\\\"\"]", - "document": { - "\"": "A" - }, - "result": [ - "A" - ] - }, - { - "name": "name selector, double quotes, escaped reverse solidus", - "selector": "$[\"\\\\\"]", - "document": { - "\\": "A" - }, - "result": [ - "A" - ] - }, - { - "name": "name selector, double quotes, escaped solidus", - "selector": "$[\"\\/\"]", - "document": { - "/": "A" - }, - "result": [ - "A" - ] - }, - { - "name": "name selector, double quotes, escaped backspace", - "selector": "$[\"\\b\"]", - "document": { - "\b": "A" - }, - "result": [ - "A" - ] - }, - { - "name": "name selector, double quotes, escaped form feed", - "selector": "$[\"\\f\"]", - "document": { - "\f": "A" - }, - "result": [ - "A" - ] - }, - { - "name": "name selector, double quotes, escaped line feed", - "selector": "$[\"\\n\"]", - "document": { - "\n": "A" - }, - "result": [ - "A" - ] - }, - { - "name": "name selector, double quotes, escaped carriage return", - "selector": "$[\"\\r\"]", - "document": { - "\r": "A" - }, - "result": [ - "A" - ] - }, - { - "name": "name selector, double quotes, escaped tab", - "selector": "$[\"\\t\"]", - "document": { - "\t": "A" - }, - "result": [ - "A" - ] - }, - { - "name": "name selector, double quotes, escaped ☺, upper case hex", - "selector": "$[\"\\u263A\"]", - "document": { - "☺": "A" - }, - "result": [ - "A" - ], - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, double quotes, escaped ☺, lower case hex", - "selector": "$[\"\\u263a\"]", - "document": { - "☺": "A" - }, - "result": [ - "A" - ], - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, double quotes, surrogate pair 𝄞", - "selector": "$[\"\\uD834\\uDD1E\"]", - "document": { - "𝄞": "A" - }, - "result": [ - "A" - ], - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, double quotes, surrogate pair 😀", - "selector": "$[\"\\uD83D\\uDE00\"]", - "document": { - "😀": "A" - }, - "result": [ - "A" - ], - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, double quotes, before high surrogates", - "selector": "$[\"\\uD7FF\\uD7FF\"]", - "document": { - "퟿퟿": "A" - }, - "result": [ - "A" - ], - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, double quotes, after low surrogates", - "selector": "$[\"\\uE000\\uE000\"]", - "document": { - "": "A" - }, - "result": [ - "A" - ], - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, double quotes, invalid escaped single quote", - "selector": "$[\"\\'\"]", - "invalid_selector": true - }, - { - "name": "name selector, double quotes, embedded double quote", - "selector": "$[\"\"\"]", - "invalid_selector": true - }, - { - "name": "name selector, double quotes, incomplete escape", - "selector": "$[\"\\\"]", - "invalid_selector": true - }, - { - "name": "name selector, double quotes, escape at end of line", - "selector": "$[\"\\\n\"]", - "invalid_selector": true - }, - { - "name": "name selector, double quotes, question mark escape", - "selector": "$[\"\\?\"]", - "invalid_selector": true - }, - { - "name": "name selector, double quotes, bell escape", - "selector": "$[\"\\a\"]", - "invalid_selector": true - }, - { - "name": "name selector, double quotes, vertical tab escape", - "selector": "$[\"\\v\"]", - "invalid_selector": true - }, - { - "name": "name selector, double quotes, 0 escape", - "selector": "$[\"\\0\"]", - "invalid_selector": true - }, - { - "name": "name selector, double quotes, x escape", - "selector": "$[\"\\x12\"]", - "invalid_selector": true - }, - { - "name": "name selector, double quotes, n escape", - "selector": "$[\"\\N{LATIN CAPITAL LETTER A}\"]", - "invalid_selector": true, - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, double quotes, unicode escape no hex", - "selector": "$[\"\\u\"]", - "invalid_selector": true, - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, double quotes, unicode escape too few hex", - "selector": "$[\"\\u123\"]", - "invalid_selector": true, - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, double quotes, unicode escape upper u", - "selector": "$[\"\\U1234\"]", - "invalid_selector": true, - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, double quotes, unicode escape upper u long", - "selector": "$[\"\\U0010FFFF\"]", - "invalid_selector": true, - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, double quotes, unicode escape plus", - "selector": "$[\"\\u+1234\"]", - "invalid_selector": true, - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, double quotes, unicode escape brackets", - "selector": "$[\"\\u{1234}\"]", - "invalid_selector": true, - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, double quotes, unicode escape brackets long", - "selector": "$[\"\\u{10ffff}\"]", - "invalid_selector": true, - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, double quotes, single high surrogate", - "selector": "$[\"\\uD800\"]", - "invalid_selector": true, - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, double quotes, single low surrogate", - "selector": "$[\"\\uDC00\"]", - "invalid_selector": true, - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, double quotes, high high surrogate", - "selector": "$[\"\\uD800\\uD800\"]", - "invalid_selector": true, - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, double quotes, low low surrogate", - "selector": "$[\"\\uDC00\\uDC00\"]", - "invalid_selector": true, - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, double quotes, surrogate non-surrogate", - "selector": "$[\"\\uD800\\u1234\"]", - "invalid_selector": true, - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, double quotes, non-surrogate surrogate", - "selector": "$[\"\\u1234\\uDC00\"]", - "invalid_selector": true, - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, double quotes, surrogate supplementary", - "selector": "$[\"\\uD800𝄞\"]", - "invalid_selector": true, - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, double quotes, supplementary surrogate", - "selector": "$[\"𝄞\\uDC00\"]", - "invalid_selector": true, - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, double quotes, surrogate incomplete low", - "selector": "$[\"\\uD800\\uDC0\"]", - "invalid_selector": true, - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, single quotes", - "selector": "$['a']", - "document": { - "a": "A", - "b": "B" - }, - "result": [ - "A" - ] - }, - { - "name": "name selector, single quotes, absent data", - "selector": "$['c']", - "document": { - "a": "A", - "b": "B" - }, - "result": [] - }, - { - "name": "name selector, single quotes, array data", - "selector": "$['a']", - "document": [ - "first", - "second" - ], - "result": [] - }, - { - "name": "name selector, single quotes, embedded U+0000", - "selector": "$['\u0000']", - "invalid_selector": true, - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, single quotes, embedded U+0001", - "selector": "$['\u0001']", - "invalid_selector": true, - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, single quotes, embedded U+0002", - "selector": "$['\u0002']", - "invalid_selector": true, - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, single quotes, embedded U+0003", - "selector": "$['\u0003']", - "invalid_selector": true, - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, single quotes, embedded U+0004", - "selector": "$['\u0004']", - "invalid_selector": true, - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, single quotes, embedded U+0005", - "selector": "$['\u0005']", - "invalid_selector": true, - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, single quotes, embedded U+0006", - "selector": "$['\u0006']", - "invalid_selector": true, - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, single quotes, embedded U+0007", - "selector": "$['\u0007']", - "invalid_selector": true, - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, single quotes, embedded U+0008", - "selector": "$['\b']", - "invalid_selector": true, - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, single quotes, embedded U+0009", - "selector": "$['\t']", - "invalid_selector": true, - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, single quotes, embedded U+000A", - "selector": "$['\n']", - "invalid_selector": true, - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, single quotes, embedded U+000B", - "selector": "$['\u000b']", - "invalid_selector": true, - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, single quotes, embedded U+000C", - "selector": "$['\f']", - "invalid_selector": true, - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, single quotes, embedded U+000D", - "selector": "$['\r']", - "invalid_selector": true, - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, single quotes, embedded U+000E", - "selector": "$['\u000e']", - "invalid_selector": true, - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, single quotes, embedded U+000F", - "selector": "$['\u000f']", - "invalid_selector": true, - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, single quotes, embedded U+0010", - "selector": "$['\u0010']", - "invalid_selector": true, - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, single quotes, embedded U+0011", - "selector": "$['\u0011']", - "invalid_selector": true, - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, single quotes, embedded U+0012", - "selector": "$['\u0012']", - "invalid_selector": true, - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, single quotes, embedded U+0013", - "selector": "$['\u0013']", - "invalid_selector": true, - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, single quotes, embedded U+0014", - "selector": "$['\u0014']", - "invalid_selector": true, - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, single quotes, embedded U+0015", - "selector": "$['\u0015']", - "invalid_selector": true, - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, single quotes, embedded U+0016", - "selector": "$['\u0016']", - "invalid_selector": true, - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, single quotes, embedded U+0017", - "selector": "$['\u0017']", - "invalid_selector": true, - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, single quotes, embedded U+0018", - "selector": "$['\u0018']", - "invalid_selector": true, - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, single quotes, embedded U+0019", - "selector": "$['\u0019']", - "invalid_selector": true, - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, single quotes, embedded U+001A", - "selector": "$['\u001a']", - "invalid_selector": true, - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, single quotes, embedded U+001B", - "selector": "$['\u001b']", - "invalid_selector": true, - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, single quotes, embedded U+001C", - "selector": "$['\u001c']", - "invalid_selector": true, - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, single quotes, embedded U+001D", - "selector": "$['\u001d']", - "invalid_selector": true, - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, single quotes, embedded U+001E", - "selector": "$['\u001e']", - "invalid_selector": true, - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, single quotes, embedded U+001F", - "selector": "$['\u001f']", - "invalid_selector": true, - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, single quotes, embedded U+0020", - "selector": "$[' ']", - "document": { - " ": "A" - }, - "result": [ - "A" - ], - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, single quotes, escaped single quote", - "selector": "$['\\'']", - "document": { - "'": "A" - }, - "result": [ - "A" - ] - }, - { - "name": "name selector, single quotes, escaped reverse solidus", - "selector": "$['\\\\']", - "document": { - "\\": "A" - }, - "result": [ - "A" - ] - }, - { - "name": "name selector, single quotes, escaped solidus", - "selector": "$['\\/']", - "document": { - "/": "A" - }, - "result": [ - "A" - ], - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, single quotes, escaped backspace", - "selector": "$['\\b']", - "document": { - "\b": "A" - }, - "result": [ - "A" - ] - }, - { - "name": "name selector, single quotes, escaped form feed", - "selector": "$['\\f']", - "document": { - "\f": "A" - }, - "result": [ - "A" - ] - }, - { - "name": "name selector, single quotes, escaped line feed", - "selector": "$['\\n']", - "document": { - "\n": "A" - }, - "result": [ - "A" - ] - }, - { - "name": "name selector, single quotes, escaped carriage return", - "selector": "$['\\r']", - "document": { - "\r": "A" - }, - "result": [ - "A" - ] - }, - { - "name": "name selector, single quotes, escaped tab", - "selector": "$['\\t']", - "document": { - "\t": "A" - }, - "result": [ - "A" - ] - }, - { - "name": "name selector, single quotes, escaped ☺, upper case hex", - "selector": "$['\\u263A']", - "document": { - "☺": "A" - }, - "result": [ - "A" - ], - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, single quotes, escaped ☺, lower case hex", - "selector": "$['\\u263a']", - "document": { - "☺": "A" - }, - "result": [ - "A" - ], - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, single quotes, surrogate pair 𝄞", - "selector": "$['\\uD834\\uDD1E']", - "document": { - "𝄞": "A" - }, - "result": [ - "A" - ], - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, single quotes, surrogate pair 😀", - "selector": "$['\\uD83D\\uDE00']", - "document": { - "😀": "A" - }, - "result": [ - "A" - ], - "tags": [ - "unicode" - ] - }, - { - "name": "name selector, single quotes, invalid escaped double quote", - "selector": "$['\\\"']", - "invalid_selector": true - }, - { - "name": "name selector, single quotes, embedded single quote", - "selector": "$[''']", - "invalid_selector": true - }, - { - "name": "name selector, single quotes, incomplete escape", - "selector": "$['\\']", - "invalid_selector": true - }, - { - "name": "name selector, double quotes, empty", - "selector": "$[\"\"]", - "document": { - "a": "A", - "b": "B", - "": "C" - }, - "result": [ - "C" - ] - }, - { - "name": "name selector, single quotes, empty", - "selector": "$['']", - "document": { - "a": "A", - "b": "B", - "": "C" - }, - "result": [ - "C" - ] - }, - { - "name": "slice selector, slice selector", - "selector": "$[1:3]", - "document": [ - 0, - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9 - ], - "result": [ - 1, - 2 - ], - "tags": [ - "slice" - ] - }, - { - "name": "slice selector, slice selector with step", - "selector": "$[1:6:2]", - "document": [ - 0, - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9 - ], - "result": [ - 1, - 3, - 5 - ], - "tags": [ - "slice" - ] - }, - { - "name": "slice selector, slice selector with everything omitted, short form", - "selector": "$[:]", - "document": [ - 0, - 1, - 2, - 3 - ], - "result": [ - 0, - 1, - 2, - 3 - ], - "tags": [ - "slice" - ] - }, - { - "name": "slice selector, slice selector with everything omitted, long form", - "selector": "$[::]", - "document": [ - 0, - 1, - 2, - 3 - ], - "result": [ - 0, - 1, - 2, - 3 - ], - "tags": [ - "slice" - ] - }, - { - "name": "slice selector, slice selector with start omitted", - "selector": "$[:2]", - "document": [ - 0, - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9 - ], - "result": [ - 0, - 1 - ], - "tags": [ - "slice" - ] - }, - { - "name": "slice selector, slice selector with start and end omitted", - "selector": "$[::2]", - "document": [ - 0, - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9 - ], - "result": [ - 0, - 2, - 4, - 6, - 8 - ], - "tags": [ - "slice" - ] - }, - { - "name": "slice selector, negative step with default start and end", - "selector": "$[::-1]", - "document": [ - 0, - 1, - 2, - 3 - ], - "result": [ - 3, - 2, - 1, - 0 - ], - "tags": [ - "slice" - ] - }, - { - "name": "slice selector, negative step with default start", - "selector": "$[:0:-1]", - "document": [ - 0, - 1, - 2, - 3 - ], - "result": [ - 3, - 2, - 1 - ], - "tags": [ - "slice" - ] - }, - { - "name": "slice selector, negative step with default end", - "selector": "$[2::-1]", - "document": [ - 0, - 1, - 2, - 3 - ], - "result": [ - 2, - 1, - 0 - ], - "tags": [ - "slice" - ] - }, - { - "name": "slice selector, larger negative step", - "selector": "$[::-2]", - "document": [ - 0, - 1, - 2, - 3 - ], - "result": [ - 3, - 1 - ], - "tags": [ - "slice" - ] - }, - { - "name": "slice selector, negative range with default step", - "selector": "$[-1:-3]", - "document": [ - 0, - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9 - ], - "result": [], - "tags": [ - "slice" - ] - }, - { - "name": "slice selector, negative range with negative step", - "selector": "$[-1:-3:-1]", - "document": [ - 0, - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9 - ], - "result": [ - 9, - 8 - ], - "tags": [ - "slice" - ] - }, - { - "name": "slice selector, negative range with larger negative step", - "selector": "$[-1:-6:-2]", - "document": [ - 0, - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9 - ], - "result": [ - 9, - 7, - 5 - ], - "tags": [ - "slice" - ] - }, - { - "name": "slice selector, larger negative range with larger negative step", - "selector": "$[-1:-7:-2]", - "document": [ - 0, - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9 - ], - "result": [ - 9, - 7, - 5 - ], - "tags": [ - "slice" - ] - }, - { - "name": "slice selector, negative from, positive to", - "selector": "$[-5:7]", - "document": [ - 0, - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9 - ], - "result": [ - 5, - 6 - ], - "tags": [ - "slice" - ] - }, - { - "name": "slice selector, negative from", - "selector": "$[-2:]", - "document": [ - 0, - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9 - ], - "result": [ - 8, - 9 - ], - "tags": [ - "slice" - ] - }, - { - "name": "slice selector, positive from, negative to", - "selector": "$[1:-1]", - "document": [ - 0, - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9 - ], - "result": [ - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8 - ], - "tags": [ - "slice" - ] - }, - { - "name": "slice selector, negative from, positive to, negative step", - "selector": "$[-1:1:-1]", - "document": [ - 0, - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9 - ], - "result": [ - 9, - 8, - 7, - 6, - 5, - 4, - 3, - 2 - ], - "tags": [ - "slice" - ] - }, - { - "name": "slice selector, positive from, negative to, negative step", - "selector": "$[7:-5:-1]", - "document": [ - 0, - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9 - ], - "result": [ - 7, - 6 - ], - "tags": [ - "slice" - ] - }, - { - "name": "slice selector, too many colons", - "selector": "$[1:2:3:4]", - "invalid_selector": true, - "tags": [ - "slice" - ] - }, - { - "name": "slice selector, non-integer array index", - "selector": "$[1:2:a]", - "invalid_selector": true, - "tags": [ - "slice" - ] - }, - { - "name": "slice selector, zero step", - "selector": "$[1:2:0]", - "document": [ - 0, - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9 - ], - "result": [], - "tags": [ - "slice" - ] - }, - { - "name": "slice selector, empty range", - "selector": "$[2:2]", - "document": [ - 0, - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9 - ], - "result": [], - "tags": [ - "slice" - ] - }, - { - "name": "slice selector, slice selector with everything omitted with empty array", - "selector": "$[:]", - "document": [], - "result": [], - "tags": [ - "slice" - ] - }, - { - "name": "slice selector, negative step with empty array", - "selector": "$[::-1]", - "document": [], - "result": [], - "tags": [ - "slice" - ] - }, - { - "name": "slice selector, maximal range with positive step", - "selector": "$[0:10]", - "document": [ - 0, - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9 - ], - "result": [ - 0, - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9 - ], - "tags": [ - "slice" - ] - }, - { - "name": "slice selector, maximal range with negative step", - "selector": "$[9:0:-1]", - "document": [ - 0, - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9 - ], - "result": [ - 9, - 8, - 7, - 6, - 5, - 4, - 3, - 2, - 1 - ], - "tags": [ - "slice" - ] - }, - { - "name": "slice selector, excessively large to value", - "selector": "$[2:113667776004]", - "document": [ - 0, - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9 - ], - "result": [ - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9 - ], - "tags": [ - "boundary", - "slice" - ] - }, - { - "name": "slice selector, excessively small from value", - "selector": "$[-113667776004:1]", - "document": [ - 0, - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9 - ], - "result": [ - 0 - ], - "tags": [ - "boundary", - "slice" - ] - }, - { - "name": "slice selector, excessively large from value with negative step", - "selector": "$[113667776004:0:-1]", - "document": [ - 0, - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9 - ], - "result": [ - 9, - 8, - 7, - 6, - 5, - 4, - 3, - 2, - 1 - ], - "tags": [ - "boundary", - "slice" - ] - }, - { - "name": "slice selector, excessively small to value with negative step", - "selector": "$[3:-113667776004:-1]", - "document": [ - 0, - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9 - ], - "result": [ - 3, - 2, - 1, - 0 - ], - "tags": [ - "boundary", - "slice" - ] - }, - { - "name": "slice selector, excessively large step", - "selector": "$[1:10:113667776004]", - "document": [ - 0, - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9 - ], - "result": [ - 1 - ], - "tags": [ - "boundary", - "slice" - ] - }, - { - "name": "slice selector, excessively small step", - "selector": "$[-1:-10:-113667776004]", - "document": [ - 0, - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9 - ], - "result": [ - 9 - ], - "tags": [ - "boundary", - "slice" - ] - }, - { - "name": "slice selector, start, min exact", - "selector": "$[-9007199254740991::]", - "document": [], - "result": [], - "tags": [ - "boundary", - "slice" - ] - }, - { - "name": "slice selector, start, max exact", - "selector": "$[9007199254740991::]", - "document": [], - "result": [], - "tags": [ - "boundary", - "slice" - ] - }, - { - "name": "slice selector, start, min exact - 1", - "selector": "$[-9007199254740992::]", - "invalid_selector": true, - "tags": [ - "boundary", - "slice" - ] - }, - { - "name": "slice selector, start, max exact + 1", - "selector": "$[9007199254740992::]", - "invalid_selector": true, - "tags": [ - "boundary", - "slice" - ] - }, - { - "name": "slice selector, end, min exact", - "selector": "$[:-9007199254740991:]", - "document": [], - "result": [], - "tags": [ - "boundary", - "slice" - ] - }, - { - "name": "slice selector, end, max exact", - "selector": "$[:9007199254740991:]", - "document": [], - "result": [], - "tags": [ - "boundary", - "slice" - ] - }, - { - "name": "slice selector, end, min exact - 1", - "selector": "$[:-9007199254740992:]", - "invalid_selector": true, - "tags": [ - "boundary", - "slice" - ] - }, - { - "name": "slice selector, end, max exact + 1", - "selector": "$[:9007199254740992:]", - "invalid_selector": true, - "tags": [ - "boundary", - "slice" - ] - }, - { - "name": "slice selector, step, min exact", - "selector": "$[::-9007199254740991]", - "document": [], - "result": [], - "tags": [ - "boundary", - "slice" - ] - }, - { - "name": "slice selector, step, max exact", - "selector": "$[::9007199254740991]", - "document": [], - "result": [], - "tags": [ - "boundary", - "slice" - ] - }, - { - "name": "slice selector, step, min exact - 1", - "selector": "$[::-9007199254740992]", - "invalid_selector": true, - "tags": [ - "boundary", - "slice" - ] - }, - { - "name": "slice selector, step, max exact + 1", - "selector": "$[::9007199254740992]", - "invalid_selector": true, - "tags": [ - "boundary", - "slice" - ] - }, - { - "name": "slice selector, overflowing to value", - "selector": "$[2:231584178474632390847141970017375815706539969331281128078915168015826259279872]", - "invalid_selector": true, - "tags": [ - "boundary", - "slice" - ] - }, - { - "name": "slice selector, underflowing from value", - "selector": "$[-231584178474632390847141970017375815706539969331281128078915168015826259279872:1]", - "invalid_selector": true, - "tags": [ - "boundary", - "slice" - ] - }, - { - "name": "slice selector, overflowing from value with negative step", - "selector": "$[231584178474632390847141970017375815706539969331281128078915168015826259279872:0:-1]", - "invalid_selector": true, - "tags": [ - "boundary", - "slice" - ] - }, - { - "name": "slice selector, underflowing to value with negative step", - "selector": "$[3:-231584178474632390847141970017375815706539969331281128078915168015826259279872:-1]", - "invalid_selector": true, - "tags": [ - "boundary", - "slice" - ] - }, - { - "name": "slice selector, overflowing step", - "selector": "$[1:10:231584178474632390847141970017375815706539969331281128078915168015826259279872]", - "invalid_selector": true, - "tags": [ - "boundary", - "slice" - ] - }, - { - "name": "slice selector, underflowing step", - "selector": "$[-1:-10:-231584178474632390847141970017375815706539969331281128078915168015826259279872]", - "invalid_selector": true, - "tags": [ - "boundary", - "slice" - ] - }, - { - "name": "slice selector, start, leading 0", - "selector": "$[01::]", - "invalid_selector": true, - "tags": [ - "slice" - ] - }, - { - "name": "slice selector, start, decimal", - "selector": "$[1.0::]", - "invalid_selector": true, - "tags": [ - "slice" - ] - }, - { - "name": "slice selector, start, plus", - "selector": "$[+1::]", - "invalid_selector": true, - "tags": [ - "slice" - ] - }, - { - "name": "slice selector, start, minus space", - "selector": "$[- 1::]", - "invalid_selector": true, - "tags": [ - "slice" - ] - }, - { - "name": "slice selector, start, -0", - "selector": "$[-0::]", - "invalid_selector": true, - "tags": [ - "slice" - ] - }, - { - "name": "slice selector, start, leading -0", - "selector": "$[-01::]", - "invalid_selector": true, - "tags": [ - "slice" - ] - }, - { - "name": "slice selector, end, leading 0", - "selector": "$[:01:]", - "invalid_selector": true, - "tags": [ - "slice" - ] - }, - { - "name": "slice selector, end, decimal", - "selector": "$[:1.0:]", - "invalid_selector": true, - "tags": [ - "slice" - ] - }, - { - "name": "slice selector, end, plus", - "selector": "$[:+1:]", - "invalid_selector": true, - "tags": [ - "slice" - ] - }, - { - "name": "slice selector, end, minus space", - "selector": "$[:- 1:]", - "invalid_selector": true, - "tags": [ - "slice" - ] - }, - { - "name": "slice selector, end, -0", - "selector": "$[:-0:]", - "invalid_selector": true, - "tags": [ - "slice" - ] - }, - { - "name": "slice selector, end, leading -0", - "selector": "$[:-01:]", - "invalid_selector": true, - "tags": [ - "slice" - ] - }, - { - "name": "slice selector, step, leading 0", - "selector": "$[::01]", - "invalid_selector": true, - "tags": [ - "slice" - ] - }, - { - "name": "slice selector, step, decimal", - "selector": "$[::1.0]", - "invalid_selector": true, - "tags": [ - "slice" - ] - }, - { - "name": "slice selector, step, plus", - "selector": "$[::+1]", - "invalid_selector": true, - "tags": [ - "slice" - ] - }, - { - "name": "slice selector, step, minus space", - "selector": "$[::- 1]", - "invalid_selector": true, - "tags": [ - "slice" - ] - }, - { - "name": "slice selector, step, -0", - "selector": "$[::-0]", - "invalid_selector": true, - "tags": [ - "slice" - ] - }, - { - "name": "slice selector, step, leading -0", - "selector": "$[::-01]", - "invalid_selector": true, - "tags": [ - "slice" - ] - }, - { - "name": "functions, count, count function", - "selector": "$[?count(@..*)>2]", - "document": [ - { - "a": [ - 1, - 2, - 3 - ] - }, - { - "a": [ - 1 - ], - "d": "f" - }, - { - "a": 1, - "d": "f" - } - ], - "result": [ - { - "a": [ - 1, - 2, - 3 - ] - }, - { - "a": [ - 1 - ], - "d": "f" - } - ], - "tags": [ - "count", - "function" - ] - }, - { - "name": "functions, count, single-node arg", - "selector": "$[?count(@.a)>1]", - "document": [ - { - "a": [ - 1, - 2, - 3 - ] - }, - { - "a": [ - 1 - ], - "d": "f" - }, - { - "a": 1, - "d": "f" - } - ], - "result": [], - "tags": [ - "count", - "function" - ] - }, - { - "name": "functions, count, multiple-selector arg", - "selector": "$[?count(@['a','d'])>1]", - "document": [ - { - "a": [ - 1, - 2, - 3 - ] - }, - { - "a": [ - 1 - ], - "d": "f" - }, - { - "a": 1, - "d": "f" - } - ], - "result": [ - { - "a": [ - 1 - ], - "d": "f" - }, - { - "a": 1, - "d": "f" - } - ], - "tags": [ - "count", - "function" - ] - }, - { - "name": "functions, count, non-query arg, number", - "selector": "$[?count(1)>2]", - "invalid_selector": true, - "tags": [ - "count", - "function" - ] - }, - { - "name": "functions, count, non-query arg, string", - "selector": "$[?count('string')>2]", - "invalid_selector": true, - "tags": [ - "count", - "function" - ] - }, - { - "name": "functions, count, non-query arg, true", - "selector": "$[?count(true)>2]", - "invalid_selector": true, - "tags": [ - "count", - "function" - ] - }, - { - "name": "functions, count, non-query arg, false", - "selector": "$[?count(false)>2]", - "invalid_selector": true, - "tags": [ - "count", - "function" - ] - }, - { - "name": "functions, count, non-query arg, null", - "selector": "$[?count(null)>2]", - "invalid_selector": true, - "tags": [ - "count", - "function" - ] - }, - { - "name": "functions, count, result must be compared", - "selector": "$[?count(@..*)]", - "invalid_selector": true, - "tags": [ - "count", - "function" - ] - }, - { - "name": "functions, count, no params", - "selector": "$[?count()==1]", - "invalid_selector": true, - "tags": [ - "count", - "function" - ] - }, - { - "name": "functions, count, too many params", - "selector": "$[?count(@.a,@.b)==1]", - "invalid_selector": true, - "tags": [ - "count", - "function" - ] - }, - { - "name": "functions, length, string data", - "selector": "$[?length(@.a)>=2]", - "document": [ - { - "a": "ab" - }, - { - "a": "d" - } - ], - "result": [ - { - "a": "ab" - } - ], - "tags": [ - "function", - "length" - ] - }, - { - "name": "functions, length, string data, unicode", - "selector": "$[?length(@)==2]", - "document": [ - "☺", - "☺☺", - "☺☺☺", - "ж", - "жж", - "жжж", - "磨", - "阿美", - "形声字" - ], - "result": [ - "☺☺", - "жж", - "阿美" - ], - "tags": [ - "function", - "length" - ] - }, - { - "name": "functions, length, array data", - "selector": "$[?length(@.a)>=2]", - "document": [ - { - "a": [ - 1, - 2, - 3 - ] - }, - { - "a": [ - 1 - ] - } - ], - "result": [ - { - "a": [ - 1, - 2, - 3 - ] - } - ], - "tags": [ - "function", - "length" - ] - }, - { - "name": "functions, length, missing data", - "selector": "$[?length(@.a)>=2]", - "document": [ - { - "d": "f" - } - ], - "result": [], - "tags": [ - "function", - "length" - ] - }, - { - "name": "functions, length, number arg", - "selector": "$[?length(1)>=2]", - "document": [ - { - "d": "f" - } - ], - "result": [], - "tags": [ - "function", - "length" - ] - }, - { - "name": "functions, length, true arg", - "selector": "$[?length(true)>=2]", - "document": [ - { - "d": "f" - } - ], - "result": [], - "tags": [ - "function", - "length" - ] - }, - { - "name": "functions, length, false arg", - "selector": "$[?length(false)>=2]", - "document": [ - { - "d": "f" - } - ], - "result": [], - "tags": [ - "function", - "length" - ] - }, - { - "name": "functions, length, null arg", - "selector": "$[?length(null)>=2]", - "document": [ - { - "d": "f" - } - ], - "result": [], - "tags": [ - "function", - "length" - ] - }, - { - "name": "functions, length, result must be compared", - "selector": "$[?length(@.a)]", - "invalid_selector": true, - "tags": [ - "function", - "length" - ] - }, - { - "name": "functions, length, no params", - "selector": "$[?length()==1]", - "invalid_selector": true, - "tags": [ - "function", - "length" - ] - }, - { - "name": "functions, length, too many params", - "selector": "$[?length(@.a,@.b)==1]", - "invalid_selector": true, - "tags": [ - "function", - "length" - ] - }, - { - "name": "functions, length, non-singular query arg", - "selector": "$[?length(@.*)<3]", - "invalid_selector": true, - "tags": [ - "function", - "length" - ] - }, - { - "name": "functions, length, arg is a function expression", - "selector": "$.values[?length(@.a)==length(value($..c))]", - "document": { - "c": "cd", - "values": [ - { - "a": "ab" - }, - { - "a": "d" - } - ] - }, - "result": [ - { - "a": "ab" - } - ], - "tags": [ - "function", - "length" - ] - }, - { - "name": "functions, length, arg is special nothing", - "selector": "$[?length(value(@.a))>0]", - "document": [ - { - "a": "ab" - }, - { - "c": "d" - }, - { - "a": null - } - ], - "result": [ - { - "a": "ab" - } - ], - "tags": [ - "function", - "length" - ] - }, - { - "name": "functions, match, found match", - "selector": "$[?match(@.a, 'a.*')]", - "document": [ - { - "a": "ab" - } - ], - "result": [ - { - "a": "ab" - } - ], - "tags": [ - "function", - "match" - ] - }, - { - "name": "functions, match, double quotes", - "selector": "$[?match(@.a, \"a.*\")]", - "document": [ - { - "a": "ab" - } - ], - "result": [ - { - "a": "ab" - } - ], - "tags": [ - "function", - "match" - ] - }, - { - "name": "functions, match, regex from the document", - "selector": "$.values[?match(@, $.regex)]", - "document": { - "regex": "b.?b", - "values": [ - "abc", - "bcd", - "bab", - "bba", - "bbab", - "b", - true, - [], - {} - ] - }, - "result": [ - "bab" - ], - "tags": [ - "function", - "match" - ] - }, - { - "name": "functions, match, don't select match", - "selector": "$[?!match(@.a, 'a.*')]", - "document": [ - { - "a": "ab" - } - ], - "result": [], - "tags": [ - "function", - "match" - ] - }, - { - "name": "functions, match, not a match", - "selector": "$[?match(@.a, 'a.*')]", - "document": [ - { - "a": "bc" - } - ], - "result": [], - "tags": [ - "function", - "match" - ] - }, - { - "name": "functions, match, select non-match", - "selector": "$[?!match(@.a, 'a.*')]", - "document": [ - { - "a": "bc" - } - ], - "result": [ - { - "a": "bc" - } - ], - "tags": [ - "function", - "match" - ] - }, - { - "name": "functions, match, non-string first arg", - "selector": "$[?match(1, 'a.*')]", - "document": [ - { - "a": "bc" - } - ], - "result": [], - "tags": [ - "function", - "match" - ] - }, - { - "name": "functions, match, non-string second arg", - "selector": "$[?match(@.a, 1)]", - "document": [ - { - "a": "bc" - } - ], - "result": [], - "tags": [ - "function", - "match" - ] - }, - { - "name": "functions, match, filter, match function, unicode char class, uppercase", - "selector": "$[?match(@, '\\\\p{Lu}')]", - "document": [ - "ж", - "Ж", - "1", - "жЖ", - true, - [], - {} - ], - "result": [ - "Ж" - ], - "tags": [ - "function", - "match" - ] - }, - { - "name": "functions, match, filter, match function, unicode char class negated, uppercase", - "selector": "$[?match(@, '\\\\P{Lu}')]", - "document": [ - "ж", - "Ж", - "1", - true, - [], - {} - ], - "result": [ - "ж", - "1" - ], - "tags": [ - "function", - "match" - ] - }, - { - "name": "functions, match, filter, match function, unicode, surrogate pair", - "selector": "$[?match(@, 'a.b')]", - "document": [ - "a𐄁b", - "ab", - "1", - true, - [], - {} - ], - "result": [ - "a𐄁b" - ], - "tags": [ - "function", - "match" - ] - }, - { - "name": "functions, match, dot matcher on \\u2028", - "selector": "$[?match(@, '.')]", - "document": [ - "
", - "\r", - "\n", - true, - [], - {} - ], - "result": [ - "
" - ], - "tags": [ - "function", - "match" - ] - }, - { - "name": "functions, match, dot matcher on \\u2029", - "selector": "$[?match(@, '.')]", - "document": [ - "
", - "\r", - "\n", - true, - [], - {} - ], - "result": [ - "
" - ], - "tags": [ - "function", - "match" - ] - }, - { - "name": "functions, match, result cannot be compared", - "selector": "$[?match(@.a, 'a.*')==true]", - "invalid_selector": true, - "tags": [ - "function", - "match" - ] - }, - { - "name": "functions, match, too few params", - "selector": "$[?match(@.a)==1]", - "invalid_selector": true, - "tags": [ - "function", - "match" - ] - }, - { - "name": "functions, match, too many params", - "selector": "$[?match(@.a,@.b,@.c)==1]", - "invalid_selector": true, - "tags": [ - "function", - "match" - ] - }, - { - "name": "functions, match, arg is a function expression", - "selector": "$.values[?match(@.a, value($..['regex']))]", - "document": { - "regex": "a.*", - "values": [ - { - "a": "ab" - }, - { - "a": "ba" - } - ] - }, - "result": [ - { - "a": "ab" - } - ], - "tags": [ - "function", - "match" - ] - }, - { - "name": "functions, match, dot in character class", - "selector": "$[?match(@, 'a[.b]c')]", - "document": [ - "abc", - "a.c", - "axc" - ], - "result": [ - "abc", - "a.c" - ], - "tags": [ - "function", - "match" - ] - }, - { - "name": "functions, match, escaped dot", - "selector": "$[?match(@, 'a\\\\.c')]", - "document": [ - "abc", - "a.c", - "axc" - ], - "result": [ - "a.c" - ], - "tags": [ - "function", - "match" - ] - }, - { - "name": "functions, match, escaped backslash before dot", - "selector": "$[?match(@, 'a\\\\\\\\.c')]", - "document": [ - "abc", - "a.c", - "axc", - "a\\
c" - ], - "result": [ - "a\\
c" - ], - "tags": [ - "function", - "match" - ] - }, - { - "name": "functions, match, escaped left square bracket", - "selector": "$[?match(@, 'a\\\\[.c')]", - "document": [ - "abc", - "a.c", - "a[
c" - ], - "result": [ - "a[
c" - ], - "tags": [ - "function", - "match" - ] - }, - { - "name": "functions, match, escaped right square bracket", - "selector": "$[?match(@, 'a[\\\\].]c')]", - "document": [ - "abc", - "a.c", - "a
c", - "a]c" - ], - "result": [ - "a.c", - "a]c" - ], - "tags": [ - "function", - "match" - ] - }, - { - "name": "functions, match, explicit caret", - "selector": "$[?match(@, '^ab.*')]", - "document": [ - "abc", - "axc", - "ab", - "xab" - ], - "result": [ - "abc", - "ab" - ], - "tags": [ - "function", - "match" - ] - }, - { - "name": "functions, match, explicit dollar", - "selector": "$[?match(@, '.*bc$')]", - "document": [ - "abc", - "axc", - "ab", - "abcx" - ], - "result": [ - "abc" - ], - "tags": [ - "function", - "match" - ] - }, - { - "name": "functions, search, at the end", - "selector": "$[?search(@.a, 'a.*')]", - "document": [ - { - "a": "the end is ab" - } - ], - "result": [ - { - "a": "the end is ab" - } - ], - "tags": [ - "function", - "search" - ] - }, - { - "name": "functions, search, double quotes", - "selector": "$[?search(@.a, \"a.*\")]", - "document": [ - { - "a": "the end is ab" - } - ], - "result": [ - { - "a": "the end is ab" - } - ], - "tags": [ - "function", - "search" - ] - }, - { - "name": "functions, search, at the start", - "selector": "$[?search(@.a, 'a.*')]", - "document": [ - { - "a": "ab is at the start" - } - ], - "result": [ - { - "a": "ab is at the start" - } - ], - "tags": [ - "function", - "search" - ] - }, - { - "name": "functions, search, in the middle", - "selector": "$[?search(@.a, 'a.*')]", - "document": [ - { - "a": "contains two matches" - } - ], - "result": [ - { - "a": "contains two matches" - } - ], - "tags": [ - "function", - "search" - ] - }, - { - "name": "functions, search, regex from the document", - "selector": "$.values[?search(@, $.regex)]", - "document": { - "regex": "b.?b", - "values": [ - "abc", - "bcd", - "bab", - "bba", - "bbab", - "b", - true, - [], - {} - ] - }, - "result": [ - "bab", - "bba", - "bbab" - ], - "tags": [ - "function", - "search" - ] - }, - { - "name": "functions, search, don't select match", - "selector": "$[?!search(@.a, 'a.*')]", - "document": [ - { - "a": "contains two matches" - } - ], - "result": [], - "tags": [ - "function", - "search" - ] - }, - { - "name": "functions, search, not a match", - "selector": "$[?search(@.a, 'a.*')]", - "document": [ - { - "a": "bc" - } - ], - "result": [], - "tags": [ - "function", - "search" - ] - }, - { - "name": "functions, search, select non-match", - "selector": "$[?!search(@.a, 'a.*')]", - "document": [ - { - "a": "bc" - } - ], - "result": [ - { - "a": "bc" - } - ], - "tags": [ - "function", - "search" - ] - }, - { - "name": "functions, search, non-string first arg", - "selector": "$[?search(1, 'a.*')]", - "document": [ - { - "a": "bc" - } - ], - "result": [], - "tags": [ - "function", - "search" - ] - }, - { - "name": "functions, search, non-string second arg", - "selector": "$[?search(@.a, 1)]", - "document": [ - { - "a": "bc" - } - ], - "result": [], - "tags": [ - "function", - "search" - ] - }, - { - "name": "functions, search, filter, search function, unicode char class, uppercase", - "selector": "$[?search(@, '\\\\p{Lu}')]", - "document": [ - "ж", - "Ж", - "1", - "жЖ", - true, - [], - {} - ], - "result": [ - "Ж", - "жЖ" - ], - "tags": [ - "function", - "search" - ] - }, - { - "name": "functions, search, filter, search function, unicode char class negated, uppercase", - "selector": "$[?search(@, '\\\\P{Lu}')]", - "document": [ - "ж", - "Ж", - "1", - true, - [], - {} - ], - "result": [ - "ж", - "1" - ], - "tags": [ - "function", - "search" - ] - }, - { - "name": "functions, search, filter, search function, unicode, surrogate pair", - "selector": "$[?search(@, 'a.b')]", - "document": [ - "a𐄁bc", - "abc", - "1", - true, - [], - {} - ], - "result": [ - "a𐄁bc" - ], - "tags": [ - "function", - "search" - ] - }, - { - "name": "functions, search, dot matcher on \\u2028", - "selector": "$[?search(@, '.')]", - "document": [ - "
", - "\r
\n", - "\r", - "\n", - true, - [], - {} - ], - "result": [ - "
", - "\r
\n" - ], - "tags": [ - "function", - "search" - ] - }, - { - "name": "functions, search, dot matcher on \\u2029", - "selector": "$[?search(@, '.')]", - "document": [ - "
", - "\r
\n", - "\r", - "\n", - true, - [], - {} - ], - "result": [ - "
", - "\r
\n" - ], - "tags": [ - "function", - "search" - ] - }, - { - "name": "functions, search, result cannot be compared", - "selector": "$[?search(@.a, 'a.*')==true]", - "invalid_selector": true, - "tags": [ - "function", - "search" - ] - }, - { - "name": "functions, search, too few params", - "selector": "$[?search(@.a)]", - "invalid_selector": true, - "tags": [ - "function", - "search" - ] - }, - { - "name": "functions, search, too many params", - "selector": "$[?search(@.a,@.b,@.c)]", - "invalid_selector": true, - "tags": [ - "function", - "search" - ] - }, - { - "name": "functions, search, arg is a function expression", - "selector": "$.values[?search(@, value($..['regex']))]", - "document": { - "regex": "b.?b", - "values": [ - "abc", - "bcd", - "bab", - "bba", - "bbab", - "b", - true, - [], - {} - ] - }, - "result": [ - "bab", - "bba", - "bbab" - ], - "tags": [ - "function", - "search" - ] - }, - { - "name": "functions, search, dot in character class", - "selector": "$[?search(@, 'a[.b]c')]", - "document": [ - "x abc y", - "x a.c y", - "x axc y" - ], - "result": [ - "x abc y", - "x a.c y" - ], - "tags": [ - "function", - "search" - ] - }, - { - "name": "functions, search, escaped dot", - "selector": "$[?search(@, 'a\\\\.c')]", - "document": [ - "x abc y", - "x a.c y", - "x axc y" - ], - "result": [ - "x a.c y" - ], - "tags": [ - "function", - "search" - ] - }, - { - "name": "functions, search, escaped backslash before dot", - "selector": "$[?search(@, 'a\\\\\\\\.c')]", - "document": [ - "x abc y", - "x a.c y", - "x axc y", - "x a\\
c y" - ], - "result": [ - "x a\\
c y" - ], - "tags": [ - "function", - "search" - ] - }, - { - "name": "functions, search, escaped left square bracket", - "selector": "$[?search(@, 'a\\\\[.c')]", - "document": [ - "x abc y", - "x a.c y", - "x a[
c y" - ], - "result": [ - "x a[
c y" - ], - "tags": [ - "function", - "search" - ] - }, - { - "name": "functions, search, escaped right square bracket", - "selector": "$[?search(@, 'a[\\\\].]c')]", - "document": [ - "x abc y", - "x a.c y", - "x a
c y", - "x a]c y" - ], - "result": [ - "x a.c y", - "x a]c y" - ], - "tags": [ - "function", - "search" - ] - }, - { - "name": "functions, value, single-value nodelist", - "selector": "$[?value(@.*)==4]", - "document": [ - [ - 4 - ], - { - "foo": 4 - }, - [ - 5 - ], - { - "foo": 5 - }, - 4 - ], - "result": [ - [ - 4 - ], - { - "foo": 4 - } - ], - "tags": [ - "function", - "value" - ] - }, - { - "name": "functions, value, multi-value nodelist", - "selector": "$[?value(@.*)==4]", - "document": [ - [ - 4, - 4 - ], - { - "foo": 4, - "bar": 4 - } - ], - "result": [], - "tags": [ - "function", - "value" - ] - }, - { - "name": "functions, value, too few params", - "selector": "$[?value()==4]", - "invalid_selector": true, - "tags": [ - "function", - "value" - ] - }, - { - "name": "functions, value, too many params", - "selector": "$[?value(@.a,@.b)==4]", - "invalid_selector": true, - "tags": [ - "function", - "value" - ] - }, - { - "name": "functions, value, result must be compared", - "selector": "$[?value(@.a)]", - "invalid_selector": true, - "tags": [ - "function", - "value" - ] - }, - { - "name": "whitespace, filter, space between question mark and expression", - "selector": "$[? @.a]", - "document": [ - { - "a": "b", - "d": "e" - }, - { - "b": "c", - "d": "f" - } - ], - "result": [ - { - "a": "b", - "d": "e" - } - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, filter, newline between question mark and expression", - "selector": "$[?\n@.a]", - "document": [ - { - "a": "b", - "d": "e" - }, - { - "b": "c", - "d": "f" - } - ], - "result": [ - { - "a": "b", - "d": "e" - } - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, filter, tab between question mark and expression", - "selector": "$[?\t@.a]", - "document": [ - { - "a": "b", - "d": "e" - }, - { - "b": "c", - "d": "f" - } - ], - "result": [ - { - "a": "b", - "d": "e" - } - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, filter, return between question mark and expression", - "selector": "$[?\r@.a]", - "document": [ - { - "a": "b", - "d": "e" - }, - { - "b": "c", - "d": "f" - } - ], - "result": [ - { - "a": "b", - "d": "e" - } - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, filter, space between question mark and parenthesized expression", - "selector": "$[? (@.a)]", - "document": [ - { - "a": "b", - "d": "e" - }, - { - "b": "c", - "d": "f" - } - ], - "result": [ - { - "a": "b", - "d": "e" - } - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, filter, newline between question mark and parenthesized expression", - "selector": "$[?\n(@.a)]", - "document": [ - { - "a": "b", - "d": "e" - }, - { - "b": "c", - "d": "f" - } - ], - "result": [ - { - "a": "b", - "d": "e" - } - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, filter, tab between question mark and parenthesized expression", - "selector": "$[?\t(@.a)]", - "document": [ - { - "a": "b", - "d": "e" - }, - { - "b": "c", - "d": "f" - } - ], - "result": [ - { - "a": "b", - "d": "e" - } - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, filter, return between question mark and parenthesized expression", - "selector": "$[?\r(@.a)]", - "document": [ - { - "a": "b", - "d": "e" - }, - { - "b": "c", - "d": "f" - } - ], - "result": [ - { - "a": "b", - "d": "e" - } - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, filter, space between parenthesized expression and bracket", - "selector": "$[?(@.a) ]", - "document": [ - { - "a": "b", - "d": "e" - }, - { - "b": "c", - "d": "f" - } - ], - "result": [ - { - "a": "b", - "d": "e" - } - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, filter, newline between parenthesized expression and bracket", - "selector": "$[?(@.a)\n]", - "document": [ - { - "a": "b", - "d": "e" - }, - { - "b": "c", - "d": "f" - } - ], - "result": [ - { - "a": "b", - "d": "e" - } - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, filter, tab between parenthesized expression and bracket", - "selector": "$[?(@.a)\t]", - "document": [ - { - "a": "b", - "d": "e" - }, - { - "b": "c", - "d": "f" - } - ], - "result": [ - { - "a": "b", - "d": "e" - } - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, filter, return between parenthesized expression and bracket", - "selector": "$[?(@.a)\r]", - "document": [ - { - "a": "b", - "d": "e" - }, - { - "b": "c", - "d": "f" - } - ], - "result": [ - { - "a": "b", - "d": "e" - } - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, filter, space between bracket and question mark", - "selector": "$[ ?@.a]", - "document": [ - { - "a": "b", - "d": "e" - }, - { - "b": "c", - "d": "f" - } - ], - "result": [ - { - "a": "b", - "d": "e" - } - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, filter, newline between bracket and question mark", - "selector": "$[\n?@.a]", - "document": [ - { - "a": "b", - "d": "e" - }, - { - "b": "c", - "d": "f" - } - ], - "result": [ - { - "a": "b", - "d": "e" - } - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, filter, tab between bracket and question mark", - "selector": "$[\t?@.a]", - "document": [ - { - "a": "b", - "d": "e" - }, - { - "b": "c", - "d": "f" - } - ], - "result": [ - { - "a": "b", - "d": "e" - } - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, filter, return between bracket and question mark", - "selector": "$[\r?@.a]", - "document": [ - { - "a": "b", - "d": "e" - }, - { - "b": "c", - "d": "f" - } - ], - "result": [ - { - "a": "b", - "d": "e" - } - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, functions, space between function name and parenthesis", - "selector": "$[?count (@.*)==1]", - "invalid_selector": true, - "tags": [ - "count", - "function", - "whitespace" - ] - }, - { - "name": "whitespace, functions, newline between function name and parenthesis", - "selector": "$[?count\n(@.*)==1]", - "invalid_selector": true, - "tags": [ - "count", - "function", - "whitespace" - ] - }, - { - "name": "whitespace, functions, tab between function name and parenthesis", - "selector": "$[?count\t(@.*)==1]", - "invalid_selector": true, - "tags": [ - "count", - "function", - "whitespace" - ] - }, - { - "name": "whitespace, functions, return between function name and parenthesis", - "selector": "$[?count\r(@.*)==1]", - "invalid_selector": true, - "tags": [ - "count", - "function", - "whitespace" - ] - }, - { - "name": "whitespace, functions, space between parenthesis and arg", - "selector": "$[?count( @.*)==1]", - "document": [ - { - "a": 1 - }, - { - "b": 2 - }, - { - "a": 2, - "b": 1 - } - ], - "result": [ - { - "a": 1 - }, - { - "b": 2 - } - ], - "tags": [ - "count", - "function", - "whitespace" - ] - }, - { - "name": "whitespace, functions, newline between parenthesis and arg", - "selector": "$[?count(\n@.*)==1]", - "document": [ - { - "a": 1 - }, - { - "b": 2 - }, - { - "a": 2, - "b": 1 - } - ], - "result": [ - { - "a": 1 - }, - { - "b": 2 - } - ], - "tags": [ - "count", - "function", - "whitespace" - ] - }, - { - "name": "whitespace, functions, tab between parenthesis and arg", - "selector": "$[?count(\t@.*)==1]", - "document": [ - { - "a": 1 - }, - { - "b": 2 - }, - { - "a": 2, - "b": 1 - } - ], - "result": [ - { - "a": 1 - }, - { - "b": 2 - } - ], - "tags": [ - "count", - "function", - "whitespace" - ] - }, - { - "name": "whitespace, functions, return between parenthesis and arg", - "selector": "$[?count(\r@.*)==1]", - "document": [ - { - "a": 1 - }, - { - "b": 2 - }, - { - "a": 2, - "b": 1 - } - ], - "result": [ - { - "a": 1 - }, - { - "b": 2 - } - ], - "tags": [ - "count", - "function", - "whitespace" - ] - }, - { - "name": "whitespace, functions, space between arg and comma", - "selector": "$[?search(@ ,'[a-z]+')]", - "document": [ - "foo", - "123" - ], - "result": [ - "foo" - ], - "tags": [ - "function", - "search", - "whitespace" - ] - }, - { - "name": "whitespace, functions, newline between arg and comma", - "selector": "$[?search(@\n,'[a-z]+')]", - "document": [ - "foo", - "123" - ], - "result": [ - "foo" - ], - "tags": [ - "function", - "search", - "whitespace" - ] - }, - { - "name": "whitespace, functions, tab between arg and comma", - "selector": "$[?search(@\t,'[a-z]+')]", - "document": [ - "foo", - "123" - ], - "result": [ - "foo" - ], - "tags": [ - "function", - "search", - "whitespace" - ] - }, - { - "name": "whitespace, functions, return between arg and comma", - "selector": "$[?search(@\r,'[a-z]+')]", - "document": [ - "foo", - "123" - ], - "result": [ - "foo" - ], - "tags": [ - "function", - "search", - "whitespace" - ] - }, - { - "name": "whitespace, functions, space between comma and arg", - "selector": "$[?search(@, '[a-z]+')]", - "document": [ - "foo", - "123" - ], - "result": [ - "foo" - ], - "tags": [ - "function", - "search", - "whitespace" - ] - }, - { - "name": "whitespace, functions, newline between comma and arg", - "selector": "$[?search(@,\n'[a-z]+')]", - "document": [ - "foo", - "123" - ], - "result": [ - "foo" - ], - "tags": [ - "function", - "search", - "whitespace" - ] - }, - { - "name": "whitespace, functions, tab between comma and arg", - "selector": "$[?search(@,\t'[a-z]+')]", - "document": [ - "foo", - "123" - ], - "result": [ - "foo" - ], - "tags": [ - "function", - "search", - "whitespace" - ] - }, - { - "name": "whitespace, functions, return between comma and arg", - "selector": "$[?search(@,\r'[a-z]+')]", - "document": [ - "foo", - "123" - ], - "result": [ - "foo" - ], - "tags": [ - "function", - "search", - "whitespace" - ] - }, - { - "name": "whitespace, functions, space between arg and parenthesis", - "selector": "$[?count(@.* )==1]", - "document": [ - { - "a": 1 - }, - { - "b": 2 - }, - { - "a": 2, - "b": 1 - } - ], - "result": [ - { - "a": 1 - }, - { - "b": 2 - } - ], - "tags": [ - "function", - "search", - "whitespace" - ] - }, - { - "name": "whitespace, functions, newline between arg and parenthesis", - "selector": "$[?count(@.*\n)==1]", - "document": [ - { - "a": 1 - }, - { - "b": 2 - }, - { - "a": 2, - "b": 1 - } - ], - "result": [ - { - "a": 1 - }, - { - "b": 2 - } - ], - "tags": [ - "count", - "function", - "whitespace" - ] - }, - { - "name": "whitespace, functions, tab between arg and parenthesis", - "selector": "$[?count(@.*\t)==1]", - "document": [ - { - "a": 1 - }, - { - "b": 2 - }, - { - "a": 2, - "b": 1 - } - ], - "result": [ - { - "a": 1 - }, - { - "b": 2 - } - ], - "tags": [ - "count", - "function", - "whitespace" - ] - }, - { - "name": "whitespace, functions, return between arg and parenthesis", - "selector": "$[?count(@.*\r)==1]", - "document": [ - { - "a": 1 - }, - { - "b": 2 - }, - { - "a": 2, - "b": 1 - } - ], - "result": [ - { - "a": 1 - }, - { - "b": 2 - } - ], - "tags": [ - "count", - "function", - "whitespace" - ] - }, - { - "name": "whitespace, functions, spaces in a relative singular selector", - "selector": "$[?length(@ .a .b) == 3]", - "document": [ - { - "a": { - "b": "foo" - } - }, - {} - ], - "result": [ - { - "a": { - "b": "foo" - } - } - ], - "tags": [ - "function", - "length", - "whitespace" - ] - }, - { - "name": "whitespace, functions, newlines in a relative singular selector", - "selector": "$[?length(@\n.a\n.b) == 3]", - "document": [ - { - "a": { - "b": "foo" - } - }, - {} - ], - "result": [ - { - "a": { - "b": "foo" - } - } - ], - "tags": [ - "function", - "length", - "whitespace" - ] - }, - { - "name": "whitespace, functions, tabs in a relative singular selector", - "selector": "$[?length(@\t.a\t.b) == 3]", - "document": [ - { - "a": { - "b": "foo" - } - }, - {} - ], - "result": [ - { - "a": { - "b": "foo" - } - } - ], - "tags": [ - "function", - "length", - "whitespace" - ] - }, - { - "name": "whitespace, functions, returns in a relative singular selector", - "selector": "$[?length(@\r.a\r.b) == 3]", - "document": [ - { - "a": { - "b": "foo" - } - }, - {} - ], - "result": [ - { - "a": { - "b": "foo" - } - } - ], - "tags": [ - "function", - "length", - "whitespace" - ] - }, - { - "name": "whitespace, functions, spaces in an absolute singular selector", - "selector": "$..[?length(@)==length($ [0] .a)]", - "document": [ - { - "a": "foo" - }, - {} - ], - "result": [ - "foo" - ], - "tags": [ - "function", - "length", - "whitespace" - ] - }, - { - "name": "whitespace, functions, newlines in an absolute singular selector", - "selector": "$..[?length(@)==length($\n[0]\n.a)]", - "document": [ - { - "a": "foo" - }, - {} - ], - "result": [ - "foo" - ], - "tags": [ - "function", - "length", - "whitespace" - ] - }, - { - "name": "whitespace, functions, tabs in an absolute singular selector", - "selector": "$..[?length(@)==length($\t[0]\t.a)]", - "document": [ - { - "a": "foo" - }, - {} - ], - "result": [ - "foo" - ], - "tags": [ - "function", - "length", - "whitespace" - ] - }, - { - "name": "whitespace, functions, returns in an absolute singular selector", - "selector": "$..[?length(@)==length($\r[0]\r.a)]", - "document": [ - { - "a": "foo" - }, - {} - ], - "result": [ - "foo" - ], - "tags": [ - "function", - "length", - "whitespace" - ] - }, - { - "name": "whitespace, operators, space before ||", - "selector": "$[?@.a ||@.b]", - "document": [ - { - "a": 1 - }, - { - "b": 2 - }, - { - "c": 3 - } - ], - "result": [ - { - "a": 1 - }, - { - "b": 2 - } - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, operators, newline before ||", - "selector": "$[?@.a\n||@.b]", - "document": [ - { - "a": 1 - }, - { - "b": 2 - }, - { - "c": 3 - } - ], - "result": [ - { - "a": 1 - }, - { - "b": 2 - } - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, operators, tab before ||", - "selector": "$[?@.a\t||@.b]", - "document": [ - { - "a": 1 - }, - { - "b": 2 - }, - { - "c": 3 - } - ], - "result": [ - { - "a": 1 - }, - { - "b": 2 - } - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, operators, return before ||", - "selector": "$[?@.a\r||@.b]", - "document": [ - { - "a": 1 - }, - { - "b": 2 - }, - { - "c": 3 - } - ], - "result": [ - { - "a": 1 - }, - { - "b": 2 - } - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, operators, space after ||", - "selector": "$[?@.a|| @.b]", - "document": [ - { - "a": 1 - }, - { - "b": 2 - }, - { - "c": 3 - } - ], - "result": [ - { - "a": 1 - }, - { - "b": 2 - } - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, operators, newline after ||", - "selector": "$[?@.a||\n@.b]", - "document": [ - { - "a": 1 - }, - { - "b": 2 - }, - { - "c": 3 - } - ], - "result": [ - { - "a": 1 - }, - { - "b": 2 - } - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, operators, tab after ||", - "selector": "$[?@.a||\t@.b]", - "document": [ - { - "a": 1 - }, - { - "b": 2 - }, - { - "c": 3 - } - ], - "result": [ - { - "a": 1 - }, - { - "b": 2 - } - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, operators, return after ||", - "selector": "$[?@.a||\r@.b]", - "document": [ - { - "a": 1 - }, - { - "b": 2 - }, - { - "c": 3 - } - ], - "result": [ - { - "a": 1 - }, - { - "b": 2 - } - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, operators, space before &&", - "selector": "$[?@.a &&@.b]", - "document": [ - { - "a": 1 - }, - { - "b": 2 - }, - { - "a": 1, - "b": 2 - } - ], - "result": [ - { - "a": 1, - "b": 2 - } - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, operators, newline before &&", - "selector": "$[?@.a\n&&@.b]", - "document": [ - { - "a": 1 - }, - { - "b": 2 - }, - { - "a": 1, - "b": 2 - } - ], - "result": [ - { - "a": 1, - "b": 2 - } - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, operators, tab before &&", - "selector": "$[?@.a\t&&@.b]", - "document": [ - { - "a": 1 - }, - { - "b": 2 - }, - { - "a": 1, - "b": 2 - } - ], - "result": [ - { - "a": 1, - "b": 2 - } - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, operators, return before &&", - "selector": "$[?@.a\r&&@.b]", - "document": [ - { - "a": 1 - }, - { - "b": 2 - }, - { - "a": 1, - "b": 2 - } - ], - "result": [ - { - "a": 1, - "b": 2 - } - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, operators, space after &&", - "selector": "$[?@.a&& @.b]", - "document": [ - { - "a": 1 - }, - { - "b": 2 - }, - { - "a": 1, - "b": 2 - } - ], - "result": [ - { - "a": 1, - "b": 2 - } - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, operators, newline after &&", - "selector": "$[?@.a&& @.b]", - "document": [ - { - "a": 1 - }, - { - "b": 2 - }, - { - "a": 1, - "b": 2 - } - ], - "result": [ - { - "a": 1, - "b": 2 - } - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, operators, tab after &&", - "selector": "$[?@.a&& @.b]", - "document": [ - { - "a": 1 - }, - { - "b": 2 - }, - { - "a": 1, - "b": 2 - } - ], - "result": [ - { - "a": 1, - "b": 2 - } - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, operators, return after &&", - "selector": "$[?@.a&& @.b]", - "document": [ - { - "a": 1 - }, - { - "b": 2 - }, - { - "a": 1, - "b": 2 - } - ], - "result": [ - { - "a": 1, - "b": 2 - } - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, operators, space before ==", - "selector": "$[?@.a ==@.b]", - "document": [ - { - "a": 1, - "b": 1 - }, - { - "a": 1, - "b": 2 - } - ], - "result": [ - { - "a": 1, - "b": 1 - } - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, operators, newline before ==", - "selector": "$[?@.a\n==@.b]", - "document": [ - { - "a": 1, - "b": 1 - }, - { - "a": 1, - "b": 2 - } - ], - "result": [ - { - "a": 1, - "b": 1 - } - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, operators, tab before ==", - "selector": "$[?@.a\t==@.b]", - "document": [ - { - "a": 1, - "b": 1 - }, - { - "a": 1, - "b": 2 - } - ], - "result": [ - { - "a": 1, - "b": 1 - } - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, operators, return before ==", - "selector": "$[?@.a\r==@.b]", - "document": [ - { - "a": 1, - "b": 1 - }, - { - "a": 1, - "b": 2 - } - ], - "result": [ - { - "a": 1, - "b": 1 - } - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, operators, space after ==", - "selector": "$[?@.a== @.b]", - "document": [ - { - "a": 1, - "b": 1 - }, - { - "a": 1, - "b": 2 - } - ], - "result": [ - { - "a": 1, - "b": 1 - } - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, operators, newline after ==", - "selector": "$[?@.a==\n@.b]", - "document": [ - { - "a": 1, - "b": 1 - }, - { - "a": 1, - "b": 2 - } - ], - "result": [ - { - "a": 1, - "b": 1 - } - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, operators, tab after ==", - "selector": "$[?@.a==\t@.b]", - "document": [ - { - "a": 1, - "b": 1 - }, - { - "a": 1, - "b": 2 - } - ], - "result": [ - { - "a": 1, - "b": 1 - } - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, operators, return after ==", - "selector": "$[?@.a==\r@.b]", - "document": [ - { - "a": 1, - "b": 1 - }, - { - "a": 1, - "b": 2 - } - ], - "result": [ - { - "a": 1, - "b": 1 - } - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, operators, space before !=", - "selector": "$[?@.a !=@.b]", - "document": [ - { - "a": 1, - "b": 1 - }, - { - "a": 1, - "b": 2 - } - ], - "result": [ - { - "a": 1, - "b": 2 - } - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, operators, newline before !=", - "selector": "$[?@.a\n!=@.b]", - "document": [ - { - "a": 1, - "b": 1 - }, - { - "a": 1, - "b": 2 - } - ], - "result": [ - { - "a": 1, - "b": 2 - } - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, operators, tab before !=", - "selector": "$[?@.a\t!=@.b]", - "document": [ - { - "a": 1, - "b": 1 - }, - { - "a": 1, - "b": 2 - } - ], - "result": [ - { - "a": 1, - "b": 2 - } - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, operators, return before !=", - "selector": "$[?@.a\r!=@.b]", - "document": [ - { - "a": 1, - "b": 1 - }, - { - "a": 1, - "b": 2 - } - ], - "result": [ - { - "a": 1, - "b": 2 - } - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, operators, space after !=", - "selector": "$[?@.a!= @.b]", - "document": [ - { - "a": 1, - "b": 1 - }, - { - "a": 1, - "b": 2 - } - ], - "result": [ - { - "a": 1, - "b": 2 - } - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, operators, newline after !=", - "selector": "$[?@.a!=\n@.b]", - "document": [ - { - "a": 1, - "b": 1 - }, - { - "a": 1, - "b": 2 - } - ], - "result": [ - { - "a": 1, - "b": 2 - } - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, operators, tab after !=", - "selector": "$[?@.a!=\t@.b]", - "document": [ - { - "a": 1, - "b": 1 - }, - { - "a": 1, - "b": 2 - } - ], - "result": [ - { - "a": 1, - "b": 2 - } - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, operators, return after !=", - "selector": "$[?@.a!=\r@.b]", - "document": [ - { - "a": 1, - "b": 1 - }, - { - "a": 1, - "b": 2 - } - ], - "result": [ - { - "a": 1, - "b": 2 - } - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, operators, space before <", - "selector": "$[?@.a <@.b]", - "document": [ - { - "a": 1, - "b": 1 - }, - { - "a": 1, - "b": 2 - } - ], - "result": [ - { - "a": 1, - "b": 2 - } - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, operators, newline before <", - "selector": "$[?@.a\n<@.b]", - "document": [ - { - "a": 1, - "b": 1 - }, - { - "a": 1, - "b": 2 - } - ], - "result": [ - { - "a": 1, - "b": 2 - } - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, operators, tab before <", - "selector": "$[?@.a\t<@.b]", - "document": [ - { - "a": 1, - "b": 1 - }, - { - "a": 1, - "b": 2 - } - ], - "result": [ - { - "a": 1, - "b": 2 - } - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, operators, return before <", - "selector": "$[?@.a\r<@.b]", - "document": [ - { - "a": 1, - "b": 1 - }, - { - "a": 1, - "b": 2 - } - ], - "result": [ - { - "a": 1, - "b": 2 - } - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, operators, space after <", - "selector": "$[?@.a< @.b]", - "document": [ - { - "a": 1, - "b": 1 - }, - { - "a": 1, - "b": 2 - } - ], - "result": [ - { - "a": 1, - "b": 2 - } - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, operators, newline after <", - "selector": "$[?@.a<\n@.b]", - "document": [ - { - "a": 1, - "b": 1 - }, - { - "a": 1, - "b": 2 - } - ], - "result": [ - { - "a": 1, - "b": 2 - } - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, operators, tab after <", - "selector": "$[?@.a<\t@.b]", - "document": [ - { - "a": 1, - "b": 1 - }, - { - "a": 1, - "b": 2 - } - ], - "result": [ - { - "a": 1, - "b": 2 - } - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, operators, return after <", - "selector": "$[?@.a<\r@.b]", - "document": [ - { - "a": 1, - "b": 1 - }, - { - "a": 1, - "b": 2 - } - ], - "result": [ - { - "a": 1, - "b": 2 - } - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, operators, space before >", - "selector": "$[?@.b >@.a]", - "document": [ - { - "a": 1, - "b": 1 - }, - { - "a": 1, - "b": 2 - } - ], - "result": [ - { - "a": 1, - "b": 2 - } - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, operators, newline before >", - "selector": "$[?@.b\n>@.a]", - "document": [ - { - "a": 1, - "b": 1 - }, - { - "a": 1, - "b": 2 - } - ], - "result": [ - { - "a": 1, - "b": 2 - } - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, operators, tab before >", - "selector": "$[?@.b\t>@.a]", - "document": [ - { - "a": 1, - "b": 1 - }, - { - "a": 1, - "b": 2 - } - ], - "result": [ - { - "a": 1, - "b": 2 - } - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, operators, return before >", - "selector": "$[?@.b\r>@.a]", - "document": [ - { - "a": 1, - "b": 1 - }, - { - "a": 1, - "b": 2 - } - ], - "result": [ - { - "a": 1, - "b": 2 - } - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, operators, space after >", - "selector": "$[?@.b> @.a]", - "document": [ - { - "a": 1, - "b": 1 - }, - { - "a": 1, - "b": 2 - } - ], - "result": [ - { - "a": 1, - "b": 2 - } - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, operators, newline after >", - "selector": "$[?@.b>\n@.a]", - "document": [ - { - "a": 1, - "b": 1 - }, - { - "a": 1, - "b": 2 - } - ], - "result": [ - { - "a": 1, - "b": 2 - } - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, operators, tab after >", - "selector": "$[?@.b>\t@.a]", - "document": [ - { - "a": 1, - "b": 1 - }, - { - "a": 1, - "b": 2 - } - ], - "result": [ - { - "a": 1, - "b": 2 - } - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, operators, return after >", - "selector": "$[?@.b>\r@.a]", - "document": [ - { - "a": 1, - "b": 1 - }, - { - "a": 1, - "b": 2 - } - ], - "result": [ - { - "a": 1, - "b": 2 - } - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, operators, space before <=", - "selector": "$[?@.a <=@.b]", - "document": [ - { - "a": 1, - "b": 1 - }, - { - "a": 1, - "b": 2 - }, - { - "a": 2, - "b": 1 - } - ], - "result": [ - { - "a": 1, - "b": 1 - }, - { - "a": 1, - "b": 2 - } - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, operators, newline before <=", - "selector": "$[?@.a\n<=@.b]", - "document": [ - { - "a": 1, - "b": 1 - }, - { - "a": 1, - "b": 2 - }, - { - "a": 2, - "b": 1 - } - ], - "result": [ - { - "a": 1, - "b": 1 - }, - { - "a": 1, - "b": 2 - } - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, operators, tab before <=", - "selector": "$[?@.a\t<=@.b]", - "document": [ - { - "a": 1, - "b": 1 - }, - { - "a": 1, - "b": 2 - }, - { - "a": 2, - "b": 1 - } - ], - "result": [ - { - "a": 1, - "b": 1 - }, - { - "a": 1, - "b": 2 - } - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, operators, return before <=", - "selector": "$[?@.a\r<=@.b]", - "document": [ - { - "a": 1, - "b": 1 - }, - { - "a": 1, - "b": 2 - }, - { - "a": 2, - "b": 1 - } - ], - "result": [ - { - "a": 1, - "b": 1 - }, - { - "a": 1, - "b": 2 - } - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, operators, space after <=", - "selector": "$[?@.a<= @.b]", - "document": [ - { - "a": 1, - "b": 1 - }, - { - "a": 1, - "b": 2 - }, - { - "a": 2, - "b": 1 - } - ], - "result": [ - { - "a": 1, - "b": 1 - }, - { - "a": 1, - "b": 2 - } - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, operators, newline after <=", - "selector": "$[?@.a<=\n@.b]", - "document": [ - { - "a": 1, - "b": 1 - }, - { - "a": 1, - "b": 2 - }, - { - "a": 2, - "b": 1 - } - ], - "result": [ - { - "a": 1, - "b": 1 - }, - { - "a": 1, - "b": 2 - } - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, operators, tab after <=", - "selector": "$[?@.a<=\t@.b]", - "document": [ - { - "a": 1, - "b": 1 - }, - { - "a": 1, - "b": 2 - }, - { - "a": 2, - "b": 1 - } - ], - "result": [ - { - "a": 1, - "b": 1 - }, - { - "a": 1, - "b": 2 - } - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, operators, return after <=", - "selector": "$[?@.a<=\r@.b]", - "document": [ - { - "a": 1, - "b": 1 - }, - { - "a": 1, - "b": 2 - }, - { - "a": 2, - "b": 1 - } - ], - "result": [ - { - "a": 1, - "b": 1 - }, - { - "a": 1, - "b": 2 - } - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, operators, space before >=", - "selector": "$[?@.b >=@.a]", - "document": [ - { - "a": 1, - "b": 1 - }, - { - "a": 1, - "b": 2 - }, - { - "a": 2, - "b": 1 - } - ], - "result": [ - { - "a": 1, - "b": 1 - }, - { - "a": 1, - "b": 2 - } - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, operators, newline before >=", - "selector": "$[?@.b\n>=@.a]", - "document": [ - { - "a": 1, - "b": 1 - }, - { - "a": 1, - "b": 2 - }, - { - "a": 2, - "b": 1 - } - ], - "result": [ - { - "a": 1, - "b": 1 - }, - { - "a": 1, - "b": 2 - } - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, operators, tab before >=", - "selector": "$[?@.b\t>=@.a]", - "document": [ - { - "a": 1, - "b": 1 - }, - { - "a": 1, - "b": 2 - }, - { - "a": 2, - "b": 1 - } - ], - "result": [ - { - "a": 1, - "b": 1 - }, - { - "a": 1, - "b": 2 - } - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, operators, return before >=", - "selector": "$[?@.b\r>=@.a]", - "document": [ - { - "a": 1, - "b": 1 - }, - { - "a": 1, - "b": 2 - }, - { - "a": 2, - "b": 1 - } - ], - "result": [ - { - "a": 1, - "b": 1 - }, - { - "a": 1, - "b": 2 - } - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, operators, space after >=", - "selector": "$[?@.b>= @.a]", - "document": [ - { - "a": 1, - "b": 1 - }, - { - "a": 1, - "b": 2 - }, - { - "a": 2, - "b": 1 - } - ], - "result": [ - { - "a": 1, - "b": 1 - }, - { - "a": 1, - "b": 2 - } - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, operators, newline after >=", - "selector": "$[?@.b>=\n@.a]", - "document": [ - { - "a": 1, - "b": 1 - }, - { - "a": 1, - "b": 2 - }, - { - "a": 2, - "b": 1 - } - ], - "result": [ - { - "a": 1, - "b": 1 - }, - { - "a": 1, - "b": 2 - } - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, operators, tab after >=", - "selector": "$[?@.b>=\t@.a]", - "document": [ - { - "a": 1, - "b": 1 - }, - { - "a": 1, - "b": 2 - }, - { - "a": 2, - "b": 1 - } - ], - "result": [ - { - "a": 1, - "b": 1 - }, - { - "a": 1, - "b": 2 - } - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, operators, return after >=", - "selector": "$[?@.b>=\r@.a]", - "document": [ - { - "a": 1, - "b": 1 - }, - { - "a": 1, - "b": 2 - }, - { - "a": 2, - "b": 1 - } - ], - "result": [ - { - "a": 1, - "b": 1 - }, - { - "a": 1, - "b": 2 - } - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, operators, space between logical not and test expression", - "selector": "$[?! @.a]", - "document": [ - { - "a": "a", - "d": "e" - }, - { - "d": "f" - }, - { - "a": "d", - "d": "f" - } - ], - "result": [ - { - "d": "f" - } - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, operators, newline between logical not and test expression", - "selector": "$[?!\n@.a]", - "document": [ - { - "a": "a", - "d": "e" - }, - { - "d": "f" - }, - { - "a": "d", - "d": "f" - } - ], - "result": [ - { - "d": "f" - } - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, operators, tab between logical not and test expression", - "selector": "$[?!\t@.a]", - "document": [ - { - "a": "a", - "d": "e" - }, - { - "d": "f" - }, - { - "a": "d", - "d": "f" - } - ], - "result": [ - { - "d": "f" - } - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, operators, return between logical not and test expression", - "selector": "$[?!\r@.a]", - "document": [ - { - "a": "a", - "d": "e" - }, - { - "d": "f" - }, - { - "a": "d", - "d": "f" - } - ], - "result": [ - { - "d": "f" - } - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, operators, space between logical not and parenthesized expression", - "selector": "$[?! (@.a=='b')]", - "document": [ - { - "a": "a", - "d": "e" - }, - { - "a": "b", - "d": "f" - }, - { - "a": "d", - "d": "f" - } - ], - "result": [ - { - "a": "a", - "d": "e" - }, - { - "a": "d", - "d": "f" - } - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, operators, newline between logical not and parenthesized expression", - "selector": "$[?!\n(@.a=='b')]", - "document": [ - { - "a": "a", - "d": "e" - }, - { - "a": "b", - "d": "f" - }, - { - "a": "d", - "d": "f" - } - ], - "result": [ - { - "a": "a", - "d": "e" - }, - { - "a": "d", - "d": "f" - } - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, operators, tab between logical not and parenthesized expression", - "selector": "$[?!\t(@.a=='b')]", - "document": [ - { - "a": "a", - "d": "e" - }, - { - "a": "b", - "d": "f" - }, - { - "a": "d", - "d": "f" - } - ], - "result": [ - { - "a": "a", - "d": "e" - }, - { - "a": "d", - "d": "f" - } - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, operators, return between logical not and parenthesized expression", - "selector": "$[?!\r(@.a=='b')]", - "document": [ - { - "a": "a", - "d": "e" - }, - { - "a": "b", - "d": "f" - }, - { - "a": "d", - "d": "f" - } - ], - "result": [ - { - "a": "a", - "d": "e" - }, - { - "a": "d", - "d": "f" - } - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, selectors, space between root and bracket", - "selector": "$ ['a']", - "document": { - "a": "ab" - }, - "result": [ - "ab" - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, selectors, newline between root and bracket", - "selector": "$\n['a']", - "document": { - "a": "ab" - }, - "result": [ - "ab" - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, selectors, tab between root and bracket", - "selector": "$\t['a']", - "document": { - "a": "ab" - }, - "result": [ - "ab" - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, selectors, return between root and bracket", - "selector": "$\r['a']", - "document": { - "a": "ab" - }, - "result": [ - "ab" - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, selectors, space between bracket and bracket", - "selector": "$['a'] ['b']", - "document": { - "a": { - "b": "ab" - } - }, - "result": [ - "ab" - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, selectors, newline between bracket and bracket", - "selector": "$['a'] \n['b']", - "document": { - "a": { - "b": "ab" - } - }, - "result": [ - "ab" - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, selectors, tab between bracket and bracket", - "selector": "$['a'] \t['b']", - "document": { - "a": { - "b": "ab" - } - }, - "result": [ - "ab" - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, selectors, return between bracket and bracket", - "selector": "$['a'] \r['b']", - "document": { - "a": { - "b": "ab" - } - }, - "result": [ - "ab" - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, selectors, space between root and dot", - "selector": "$ .a", - "document": { - "a": "ab" - }, - "result": [ - "ab" - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, selectors, newline between root and dot", - "selector": "$\n.a", - "document": { - "a": "ab" - }, - "result": [ - "ab" - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, selectors, tab between root and dot", - "selector": "$\t.a", - "document": { - "a": "ab" - }, - "result": [ - "ab" - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, selectors, return between root and dot", - "selector": "$\r.a", - "document": { - "a": "ab" - }, - "result": [ - "ab" - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, selectors, space between dot and name", - "selector": "$. a", - "invalid_selector": true, - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, selectors, newline between dot and name", - "selector": "$.\na", - "invalid_selector": true, - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, selectors, tab between dot and name", - "selector": "$.\ta", - "invalid_selector": true, - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, selectors, return between dot and name", - "selector": "$.\ra", - "invalid_selector": true, - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, selectors, space between recursive descent and name", - "selector": "$.. a", - "invalid_selector": true, - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, selectors, newline between recursive descent and name", - "selector": "$..\na", - "invalid_selector": true, - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, selectors, tab between recursive descent and name", - "selector": "$..\ta", - "invalid_selector": true, - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, selectors, return between recursive descent and name", - "selector": "$..\ra", - "invalid_selector": true, - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, selectors, space between bracket and selector", - "selector": "$[ 'a']", - "document": { - "a": "ab" - }, - "result": [ - "ab" - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, selectors, newline between bracket and selector", - "selector": "$[\n'a']", - "document": { - "a": "ab" - }, - "result": [ - "ab" - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, selectors, tab between bracket and selector", - "selector": "$[\t'a']", - "document": { - "a": "ab" - }, - "result": [ - "ab" - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, selectors, return between bracket and selector", - "selector": "$[\r'a']", - "document": { - "a": "ab" - }, - "result": [ - "ab" - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, selectors, space between selector and bracket", - "selector": "$['a' ]", - "document": { - "a": "ab" - }, - "result": [ - "ab" - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, selectors, newline between selector and bracket", - "selector": "$['a'\n]", - "document": { - "a": "ab" - }, - "result": [ - "ab" - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, selectors, tab between selector and bracket", - "selector": "$['a'\t]", - "document": { - "a": "ab" - }, - "result": [ - "ab" - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, selectors, return between selector and bracket", - "selector": "$['a'\r]", - "document": { - "a": "ab" - }, - "result": [ - "ab" - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, selectors, space between selector and comma", - "selector": "$['a' ,'b']", - "document": { - "a": "ab", - "b": "bc" - }, - "result": [ - "ab", - "bc" - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, selectors, newline between selector and comma", - "selector": "$['a'\n,'b']", - "document": { - "a": "ab", - "b": "bc" - }, - "result": [ - "ab", - "bc" - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, selectors, tab between selector and comma", - "selector": "$['a'\t,'b']", - "document": { - "a": "ab", - "b": "bc" - }, - "result": [ - "ab", - "bc" - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, selectors, return between selector and comma", - "selector": "$['a'\r,'b']", - "document": { - "a": "ab", - "b": "bc" - }, - "result": [ - "ab", - "bc" - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, selectors, space between comma and selector", - "selector": "$['a', 'b']", - "document": { - "a": "ab", - "b": "bc" - }, - "result": [ - "ab", - "bc" - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, selectors, newline between comma and selector", - "selector": "$['a',\n'b']", - "document": { - "a": "ab", - "b": "bc" - }, - "result": [ - "ab", - "bc" - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, selectors, tab between comma and selector", - "selector": "$['a',\t'b']", - "document": { - "a": "ab", - "b": "bc" - }, - "result": [ - "ab", - "bc" - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, selectors, return between comma and selector", - "selector": "$['a',\r'b']", - "document": { - "a": "ab", - "b": "bc" - }, - "result": [ - "ab", - "bc" - ], - "tags": [ - "whitespace" - ] - }, - { - "name": "whitespace, slice, space between start and colon", - "selector": "$[1 :5:2]", - "document": [ - 1, - 2, - 3, - 4, - 5, - 6 - ], - "result": [ - 2, - 4 - ], - "tags": [ - "index", - "whitespace" - ] - }, - { - "name": "whitespace, slice, newline between start and colon", - "selector": "$[1\n:5:2]", - "document": [ - 1, - 2, - 3, - 4, - 5, - 6 - ], - "result": [ - 2, - 4 - ], - "tags": [ - "index", - "whitespace" - ] - }, - { - "name": "whitespace, slice, tab between start and colon", - "selector": "$[1\t:5:2]", - "document": [ - 1, - 2, - 3, - 4, - 5, - 6 - ], - "result": [ - 2, - 4 - ], - "tags": [ - "index", - "whitespace" - ] - }, - { - "name": "whitespace, slice, return between start and colon", - "selector": "$[1\r:5:2]", - "document": [ - 1, - 2, - 3, - 4, - 5, - 6 - ], - "result": [ - 2, - 4 - ], - "tags": [ - "index", - "whitespace" - ] - }, - { - "name": "whitespace, slice, space between colon and end", - "selector": "$[1: 5:2]", - "document": [ - 1, - 2, - 3, - 4, - 5, - 6 - ], - "result": [ - 2, - 4 - ], - "tags": [ - "index", - "whitespace" - ] - }, - { - "name": "whitespace, slice, newline between colon and end", - "selector": "$[1:\n5:2]", - "document": [ - 1, - 2, - 3, - 4, - 5, - 6 - ], - "result": [ - 2, - 4 - ], - "tags": [ - "index", - "whitespace" - ] - }, - { - "name": "whitespace, slice, tab between colon and end", - "selector": "$[1:\t5:2]", - "document": [ - 1, - 2, - 3, - 4, - 5, - 6 - ], - "result": [ - 2, - 4 - ], - "tags": [ - "index", - "whitespace" - ] - }, - { - "name": "whitespace, slice, return between colon and end", - "selector": "$[1:\r5:2]", - "document": [ - 1, - 2, - 3, - 4, - 5, - 6 - ], - "result": [ - 2, - 4 - ], - "tags": [ - "index", - "whitespace" - ] - }, - { - "name": "whitespace, slice, space between end and colon", - "selector": "$[1:5 :2]", - "document": [ - 1, - 2, - 3, - 4, - 5, - 6 - ], - "result": [ - 2, - 4 - ], - "tags": [ - "index", - "whitespace" - ] - }, - { - "name": "whitespace, slice, newline between end and colon", - "selector": "$[1:5\n:2]", - "document": [ - 1, - 2, - 3, - 4, - 5, - 6 - ], - "result": [ - 2, - 4 - ], - "tags": [ - "index", - "whitespace" - ] - }, - { - "name": "whitespace, slice, tab between end and colon", - "selector": "$[1:5\t:2]", - "document": [ - 1, - 2, - 3, - 4, - 5, - 6 - ], - "result": [ - 2, - 4 - ], - "tags": [ - "index", - "whitespace" - ] - }, - { - "name": "whitespace, slice, return between end and colon", - "selector": "$[1:5\r:2]", - "document": [ - 1, - 2, - 3, - 4, - 5, - 6 - ], - "result": [ - 2, - 4 - ], - "tags": [ - "index", - "whitespace" - ] - }, - { - "name": "whitespace, slice, space between colon and step", - "selector": "$[1:5: 2]", - "document": [ - 1, - 2, - 3, - 4, - 5, - 6 - ], - "result": [ - 2, - 4 - ], - "tags": [ - "index", - "whitespace" - ] - }, - { - "name": "whitespace, slice, newline between colon and step", - "selector": "$[1:5:\n2]", - "document": [ - 1, - 2, - 3, - 4, - 5, - 6 - ], - "result": [ - 2, - 4 - ], - "tags": [ - "index", - "whitespace" - ] - }, - { - "name": "whitespace, slice, tab between colon and step", - "selector": "$[1:5:\t2]", - "document": [ - 1, - 2, - 3, - 4, - 5, - 6 - ], - "result": [ - 2, - 4 - ], - "tags": [ - "index", - "whitespace" - ] - }, - { - "name": "whitespace, slice, return between colon and step", - "selector": "$[1:5:\r2]", - "document": [ - 1, - 2, - 3, - 4, - 5, - 6 - ], - "result": [ - 2, - 4 - ], - "tags": [ - "index", - "whitespace" - ] - } -] diff --git a/src/jsonpath.rs b/src/jsonpath.rs deleted file mode 100644 index 39be3c4..0000000 --- a/src/jsonpath.rs +++ /dev/null @@ -1,936 +0,0 @@ -use crate::path::json_path_instance; -use crate::path::JsonLike; -use crate::JsonPathValue; -use crate::JsonPtr; -use crate::{JsonPath, JsonPathStr}; - -impl JsonPath -where - T: JsonLike, -{ - /// finds a slice of data in the set json. - /// The result is a vector of references to the incoming structure. - /// - /// In case, if there is no match [`Self::find_slice`] will return vec!<[`JsonPathValue::NoValue`]>. - /// - /// ## Example - /// ```rust - /// use jsonpath_rust::{JsonPath, JsonPathValue}; - /// use serde_json::json; - /// # use std::str::FromStr; - /// - /// let data = json!({"first":{"second":[{"active":1},{"passive":1}]}}); - /// let path = JsonPath::try_from("$.first.second[?(@.active)]").unwrap(); - /// let slice_of_data = path.find_slice(&data); - /// - /// let expected_value = json!({"active":1}); - /// let expected_path = "$.['first'].['second'][0]".to_string(); - /// - /// assert_eq!( - /// slice_of_data, - /// vec![JsonPathValue::Slice(&expected_value, expected_path)] - /// ); - /// ``` - pub fn find_slice<'a>(&'a self, json: &'a T) -> Vec> { - use crate::path::Path; - let instance = json_path_instance(self, json); - let res = instance.find(JsonPathValue::from_root(json)); - let has_v: Vec> = res.into_iter().filter(|v| v.has_value()).collect(); - - if has_v.is_empty() { - vec![JsonPathValue::NoValue] - } else { - has_v - } - } - - /// like [`Self::find_slice`] but returns a vector of [`JsonPtr`], which has no [`JsonPathValue::NoValue`]. - /// if there is no match, it will return an empty vector - pub fn find_slice_ptr<'a>(&'a self, json: &'a T) -> Vec> { - use crate::path::Path; - json_path_instance(self, json) - .find(JsonPathValue::from_root(json)) - .into_iter() - .filter(|v| v.has_value()) - .map(|v| match v { - JsonPathValue::Slice(v, _) => JsonPtr::Slice(v), - JsonPathValue::NewValue(v) => JsonPtr::NewValue(v), - JsonPathValue::NoValue => unreachable!("has_value was already checked"), - }) - .collect() - } - - /// finds a slice of data and wrap it with Value::Array by cloning the data. - /// Returns either an array of elements or Json::Null if the match is incorrect. - /// - /// In case, if there is no match `find` will return `json!(null)`. - /// - /// ## Example - /// ```rust - /// use jsonpath_rust::{JsonPath, JsonPathValue}; - /// use serde_json::{Value, json}; - /// # use std::str::FromStr; - /// - /// let data = json!({"first":{"second":[{"active":1},{"passive":1}]}}); - /// let path = JsonPath::try_from("$.first.second[?(@.active)]").unwrap(); - /// let cloned_data = path.find(&data); - /// - /// assert_eq!(cloned_data, Value::Array(vec![json!({"active":1})])); - /// ``` - pub fn find(&self, json: &T) -> T { - let slice = self.find_slice(json); - if !slice.is_empty() { - if JsonPathValue::only_no_value(&slice) { - T::null() - } else { - T::array( - slice - .into_iter() - .filter(|v| v.has_value()) - .map(|v| v.to_data()) - .collect(), - ) - } - } else { - T::array(vec![]) - } - } - - /// finds a path describing the value, instead of the value itself. - /// If the values has been obtained by moving the data out of the initial json the path is absent. - /// - /// ** If the value has been modified during the search, there is no way to find a path of a new value. - /// It can happen if we try to find a length() of array, for in stance.** - /// - /// ## Example - /// ```rust - /// use jsonpath_rust::{JsonPathStr, JsonPath, JsonPathValue}; - /// use serde_json::{Value, json}; - /// # use std::str::FromStr; - /// - /// let data = json!({"first":{"second":[{"active":1},{"passive":1}]}}); - /// let path = JsonPath::try_from("$.first.second[?(@.active)]").unwrap(); - /// let slice_of_data: Vec = path.find_as_path(&data); - /// - /// let expected_path = "$.['first'].['second'][0]".to_string(); - /// assert_eq!(slice_of_data, vec![expected_path]); - /// ``` - pub fn find_as_path(&self, json: &T) -> Vec { - self.find_slice(json) - .into_iter() - .flat_map(|v| v.to_path()) - .collect() - } -} - -#[cfg(test)] -mod tests { - use crate::path::JsonLike; - use crate::JsonPathQuery; - use crate::JsonPathValue::{NoValue, Slice}; - use crate::{jp_v, JsonPath, JsonPathParserError, JsonPathValue}; - use serde_json::{json, Value}; - use std::ops::Deref; - - fn test(json: &str, path: &str, expected: Vec>) { - let json: Value = match serde_json::from_str(json) { - Ok(json) => json, - Err(e) => panic!("error while parsing json: {}", e), - }; - let path = match JsonPath::try_from(path) { - Ok(path) => path, - Err(e) => panic!("error while parsing jsonpath: {}", e), - }; - - assert_eq!(path.find_slice(&json), expected) - } - - fn template_json<'a>() -> &'a str { - r#" {"store": { "book": [ - { - "category": "reference", - "author": "Nigel Rees", - "title": "Sayings of the Century", - "price": 8.95 - }, - { - "category": "fiction", - "author": "Evelyn Waugh", - "title": "Sword of Honour", - "price": 12.99 - }, - { - "category": "fiction", - "author": "Herman Melville", - "title": "Moby Dick", - "isbn": "0-553-21311-3", - "price": 8.99 - }, - { - "category": "fiction", - "author": "J. R. R. Tolkien", - "title": "The Lord of the Rings", - "isbn": "0-395-19395-8", - "price": 22.99 - } - ], - "bicycle": { - "color": "red", - "price": 19.95 - } - }, - "array":[0,1,2,3,4,5,6,7,8,9], - "orders":[ - { - "ref":[1,2,3], - "id":1, - "filled": true - }, - { - "ref":[4,5,6], - "id":2, - "filled": false - }, - { - "ref":[7,8,9], - "id":3, - "filled": null - } - ], - "expensive": 10 }"# - } - - #[test] - fn simple_test() { - let j1 = json!(2); - test("[1,2,3]", "$[1]", jp_v![&j1;"$[1]",]); - } - - #[test] - fn root_test() { - let js = serde_json::from_str(template_json()).unwrap(); - test(template_json(), "$", jp_v![&js;"$",]); - } - - #[test] - fn descent_test() { - let v1 = json!("reference"); - let v2 = json!("fiction"); - test( - template_json(), - "$..category", - jp_v![ - &v1;"$.['store'].['book'][0].['category']", - &v2;"$.['store'].['book'][1].['category']", - &v2;"$.['store'].['book'][2].['category']", - &v2;"$.['store'].['book'][3].['category']",], - ); - let js1 = json!(19.95); - let js2 = json!(8.95); - let js3 = json!(12.99); - let js4 = json!(8.99); - let js5 = json!(22.99); - test( - template_json(), - "$.store..price", - jp_v![ - &js1;"$.['store'].['bicycle'].['price']", - &js2;"$.['store'].['book'][0].['price']", - &js3;"$.['store'].['book'][1].['price']", - &js4;"$.['store'].['book'][2].['price']", - &js5;"$.['store'].['book'][3].['price']", - ], - ); - let js1 = json!("Nigel Rees"); - let js2 = json!("Evelyn Waugh"); - let js3 = json!("Herman Melville"); - let js4 = json!("J. R. R. Tolkien"); - test( - template_json(), - "$..author", - jp_v![ - &js1;"$.['store'].['book'][0].['author']", - &js2;"$.['store'].['book'][1].['author']", - &js3;"$.['store'].['book'][2].['author']", - &js4;"$.['store'].['book'][3].['author']",], - ); - } - - #[test] - fn wildcard_test() { - let js1 = json!("reference"); - let js2 = json!("fiction"); - test( - template_json(), - "$..book.[*].category", - jp_v![ - &js1;"$.['store'].['book'][0].['category']", - &js2;"$.['store'].['book'][1].['category']", - &js2;"$.['store'].['book'][2].['category']", - &js2;"$.['store'].['book'][3].['category']",], - ); - let js1 = json!("Nigel Rees"); - let js2 = json!("Evelyn Waugh"); - let js3 = json!("Herman Melville"); - let js4 = json!("J. R. R. Tolkien"); - test( - template_json(), - "$.store.book[*].author", - jp_v![ - &js1;"$.['store'].['book'][0].['author']", - &js2;"$.['store'].['book'][1].['author']", - &js3;"$.['store'].['book'][2].['author']", - &js4;"$.['store'].['book'][3].['author']",], - ); - } - - #[test] - fn descendent_wildcard_test() { - let js1 = json!("Moby Dick"); - let js2 = json!("The Lord of the Rings"); - test( - template_json(), - "$..*.[?(@.isbn)].title", - jp_v![ - &js1;"$.['store'].['book'][2].['title']", - &js2;"$.['store'].['book'][3].['title']", - &js1;"$.['store'].['book'][2].['title']", - &js2;"$.['store'].['book'][3].['title']"], - ); - } - - #[test] - fn field_test() { - let value = json!({"active":1}); - test( - r#"{"field":{"field":[{"active":1},{"passive":1}]}}"#, - "$.field.field[?(@.active)]", - jp_v![&value;"$.['field'].['field'][0]",], - ); - } - - #[test] - fn index_index_test() { - let value = json!("0-553-21311-3"); - test( - template_json(), - "$..book[2].isbn", - jp_v![&value;"$.['store'].['book'][2].['isbn']",], - ); - } - - #[test] - fn index_unit_index_test() { - let value = json!("0-553-21311-3"); - test( - template_json(), - "$..book[2,4].isbn", - jp_v![&value;"$.['store'].['book'][2].['isbn']",], - ); - let value1 = json!("0-395-19395-8"); - test( - template_json(), - "$..book[2,3].isbn", - jp_v![&value;"$.['store'].['book'][2].['isbn']", &value1;"$.['store'].['book'][3].['isbn']",], - ); - } - - #[test] - fn index_unit_keys_test() { - let js1 = json!("Moby Dick"); - let js2 = json!(8.99); - let js3 = json!("The Lord of the Rings"); - let js4 = json!(22.99); - test( - template_json(), - "$..book[2,3]['title','price']", - jp_v![ - &js1;"$.['store'].['book'][2].['title']", - &js2;"$.['store'].['book'][2].['price']", - &js3;"$.['store'].['book'][3].['title']", - &js4;"$.['store'].['book'][3].['price']",], - ); - } - - #[test] - fn index_slice_test() { - let i0 = "$.['array'][0]"; - let i1 = "$.['array'][1]"; - let i2 = "$.['array'][2]"; - let i3 = "$.['array'][3]"; - let i4 = "$.['array'][4]"; - let i5 = "$.['array'][5]"; - let i6 = "$.['array'][6]"; - let i7 = "$.['array'][7]"; - let i8 = "$.['array'][8]"; - let i9 = "$.['array'][9]"; - - let j0 = json!(0); - let j1 = json!(1); - let j2 = json!(2); - let j3 = json!(3); - let j4 = json!(4); - let j5 = json!(5); - let j6 = json!(6); - let j7 = json!(7); - let j8 = json!(8); - let j9 = json!(9); - test( - template_json(), - "$.array[:]", - jp_v![ - &j0;&i0, - &j1;&i1, - &j2;&i2, - &j3;&i3, - &j4;&i4, - &j5;&i5, - &j6;&i6, - &j7;&i7, - &j8;&i8, - &j9;&i9,], - ); - test(template_json(), "$.array[1:4:2]", jp_v![&j1;&i1, &j3;&i3,]); - test( - template_json(), - "$.array[::3]", - jp_v![&j0;&i0, &j3;&i3, &j6;&i6, &j9;&i9,], - ); - test(template_json(), "$.array[-1:]", jp_v![&j9;&i9,]); - test(template_json(), "$.array[-2:-1]", jp_v![&j8;&i8,]); - } - - #[test] - fn index_filter_test() { - let moby = json!("Moby Dick"); - let rings = json!("The Lord of the Rings"); - test( - template_json(), - "$..book[?(@.isbn)].title", - jp_v![ - &moby;"$.['store'].['book'][2].['title']", - &rings;"$.['store'].['book'][3].['title']",], - ); - let sword = json!("Sword of Honour"); - test( - template_json(), - "$..book[?(@.price != 8.95)].title", - jp_v![ - &sword;"$.['store'].['book'][1].['title']", - &moby;"$.['store'].['book'][2].['title']", - &rings;"$.['store'].['book'][3].['title']",], - ); - let sayings = json!("Sayings of the Century"); - test( - template_json(), - "$..book[?(@.price == 8.95)].title", - jp_v![&sayings;"$.['store'].['book'][0].['title']",], - ); - let js895 = json!(8.95); - test( - template_json(), - "$..book[?(@.author ~= '.*Rees')].price", - jp_v![&js895;"$.['store'].['book'][0].['price']",], - ); - let js12 = json!(12.99); - let js899 = json!(8.99); - let js2299 = json!(22.99); - test( - template_json(), - "$..book[?(@.price >= 8.99)].price", - jp_v![ - &js12;"$.['store'].['book'][1].['price']", - &js899;"$.['store'].['book'][2].['price']", - &js2299;"$.['store'].['book'][3].['price']", - ], - ); - test( - template_json(), - "$..book[?(@.price > 8.99)].price", - jp_v![ - &js12;"$.['store'].['book'][1].['price']", - &js2299;"$.['store'].['book'][3].['price']",], - ); - test( - template_json(), - "$..book[?(@.price < 8.99)].price", - jp_v![&js895;"$.['store'].['book'][0].['price']",], - ); - test( - template_json(), - "$..book[?(@.price <= 8.99)].price", - jp_v![ - &js895;"$.['store'].['book'][0].['price']", - &js899;"$.['store'].['book'][2].['price']", - ], - ); - test( - template_json(), - "$..book[?(@.price <= $.expensive)].price", - jp_v![ - &js895;"$.['store'].['book'][0].['price']", - &js899;"$.['store'].['book'][2].['price']", - ], - ); - test( - template_json(), - "$..book[?(@.price >= $.expensive)].price", - jp_v![ - &js12;"$.['store'].['book'][1].['price']", - &js2299;"$.['store'].['book'][3].['price']", - ], - ); - test( - template_json(), - "$..book[?(@.title in ['Moby Dick','Shmoby Dick','Big Dick','Dicks'])].price", - jp_v![&js899;"$.['store'].['book'][2].['price']",], - ); - test( - template_json(), - "$..book[?(@.title nin ['Moby Dick','Shmoby Dick','Big Dick','Dicks'])].title", - jp_v![ - &sayings;"$.['store'].['book'][0].['title']", - &sword;"$.['store'].['book'][1].['title']", - &rings;"$.['store'].['book'][3].['title']",], - ); - test( - template_json(), - "$..book[?(@.author size 10)].title", - jp_v![&sayings;"$.['store'].['book'][0].['title']",], - ); - let filled_true = json!(1); - test( - template_json(), - "$.orders[?(@.filled == true)].id", - jp_v![&filled_true;"$.['orders'][0].['id']",], - ); - let filled_null = json!(3); - test( - template_json(), - "$.orders[?(@.filled == null)].id", - jp_v![&filled_null;"$.['orders'][2].['id']",], - ); - } - - #[test] - fn index_filter_sets_test() { - let j1 = json!(1); - test( - template_json(), - "$.orders[?(@.ref subsetOf [1,2,3,4])].id", - jp_v![&j1;"$.['orders'][0].['id']",], - ); - let j2 = json!(2); - test( - template_json(), - "$.orders[?(@.ref anyOf [1,4])].id", - jp_v![&j1;"$.['orders'][0].['id']", &j2;"$.['orders'][1].['id']",], - ); - let j3 = json!(3); - test( - template_json(), - "$.orders[?(@.ref noneOf [3,6])].id", - jp_v![&j3;"$.['orders'][2].['id']",], - ); - } - - #[test] - fn query_test() { - let json: Box = serde_json::from_str(template_json()).expect("to get json"); - let v = json - .path("$..book[?(@.author size 10)].title") - .expect("the path is correct"); - assert_eq!(v, json!(["Sayings of the Century"])); - - let json: Value = serde_json::from_str(template_json()).expect("to get json"); - let path = &json - .path("$..book[?(@.author size 10)].title") - .expect("the path is correct"); - - assert_eq!(path, &json!(["Sayings of the Century"])); - } - - #[test] - fn find_slice_test() { - let json: Box = serde_json::from_str(template_json()).expect("to get json"); - let path: Box> = Box::from( - JsonPath::try_from("$..book[?(@.author size 10)].title").expect("the path is correct"), - ); - let v = path.find_slice(&json); - let js = json!("Sayings of the Century"); - assert_eq!(v, jp_v![&js;"$.['store'].['book'][0].['title']",]); - } - - #[test] - fn find_in_array_test() { - let json: Box = Box::new(json!([{"verb": "TEST"}, {"verb": "RUN"}])); - let path: Box> = - Box::from(JsonPath::try_from("$.[?(@.verb == 'TEST')]").expect("the path is correct")); - let v = path.find_slice(&json); - let js = json!({"verb":"TEST"}); - assert_eq!(v, jp_v![&js;"$[0]",]); - } - - #[test] - fn length_test() { - let json: Box = - Box::new(json!([{"verb": "TEST"},{"verb": "TEST"}, {"verb": "RUN"}])); - let path: Box> = Box::from( - JsonPath::try_from("$.[?(@.verb == 'TEST')].length()").expect("the path is correct"), - ); - let v = path.find(&json); - let js = json!([2]); - assert_eq!(v, js); - - let json: Box = - Box::new(json!([{"verb": "TEST"},{"verb": "TEST"}, {"verb": "RUN"}])); - let path: Box> = - Box::from(JsonPath::try_from("$.length()").expect("the path is correct")); - assert_eq!(path.find(&json), json!([3])); - - // length of search following the wildcard returns correct result - let json: Box = - Box::new(json!([{"verb": "TEST"},{"verb": "TEST","x":3}, {"verb": "RUN"}])); - let path: Box> = Box::from( - JsonPath::try_from("$.[?(@.verb == 'TEST')].[*].length()") - .expect("the path is correct"), - ); - assert_eq!(path.find(&json), json!([3])); - - // length of object returns 0 - let json: Box = Box::new(json!({"verb": "TEST"})); - let path: Box> = - Box::from(JsonPath::try_from("$.length()").expect("the path is correct")); - assert_eq!(path.find(&json), Value::Null); - - // length of integer returns null - let json: Box = Box::new(json!(1)); - let path: Box> = - Box::from(JsonPath::try_from("$.length()").expect("the path is correct")); - assert_eq!(path.find(&json), Value::Null); - - // length of array returns correct result - let json: Box = Box::new(json!([[1], [2], [3]])); - let path: Box> = - Box::from(JsonPath::try_from("$.length()").expect("the path is correct")); - assert_eq!(path.find(&json), json!([3])); - - // path does not exist returns length null - let json: Box = - Box::new(json!([{"verb": "TEST"},{"verb": "TEST"}, {"verb": "RUN"}])); - let path: Box> = - Box::from(JsonPath::try_from("$.not.exist.length()").expect("the path is correct")); - assert_eq!(path.find(&json), Value::Null); - - // seraching one value returns correct length - let json: Box = - Box::new(json!([{"verb": "TEST"},{"verb": "TEST"}, {"verb": "RUN"}])); - let path: Box> = Box::from( - JsonPath::try_from("$.[?(@.verb == 'RUN')].length()").expect("the path is correct"), - ); - - let v = path.find(&json); - let js = json!([1]); - assert_eq!(v, js); - - // searching correct path following unexisting key returns length 0 - let json: Box = - Box::new(json!([{"verb": "TEST"},{"verb": "TEST"}, {"verb": "RUN"}])); - let path: Box> = Box::from( - JsonPath::try_from("$.[?(@.verb == 'RUN')].key123.length()") - .expect("the path is correct"), - ); - - let v = path.find(&json); - let js = json!(null); - assert_eq!(v, js); - - // fetching first object returns length null - let json: Box = - Box::new(json!([{"verb": "TEST"},{"verb": "TEST"}, {"verb": "RUN"}])); - let path: Box> = - Box::from(JsonPath::try_from("$.[0].length()").expect("the path is correct")); - - let v = path.find(&json); - let js = Value::Null; - assert_eq!(v, js); - - // length on fetching the index after search gives length of the object (array) - let json: Box = Box::new(json!([{"prop": [["a", "b", "c"], "d"]}])); - let path: Box> = Box::from( - JsonPath::try_from("$.[?(@.prop)].prop.[0].length()").expect("the path is correct"), - ); - - let v = path.find(&json); - let js = json!([3]); - assert_eq!(v, js); - - // length on fetching the index after search gives length of the object (string) - let json: Box = Box::new(json!([{"prop": [["a", "b", "c"], "d"]}])); - let path: Box> = Box::from( - JsonPath::try_from("$.[?(@.prop)].prop.[1].length()").expect("the path is correct"), - ); - - let v = path.find(&json); - let js = Value::Null; - assert_eq!(v, js); - } - - #[test] - fn no_value_index_from_not_arr_filter_test() { - let json: Box = Box::new(json!({ - "field":"field", - })); - - let path: Box> = - Box::from(JsonPath::try_from("$.field[1]").expect("the path is correct")); - let v = path.find_slice(&json); - assert_eq!(v, vec![NoValue]); - - let json: Box = Box::new(json!({ - "field":[0], - })); - - let path: Box> = - Box::from(JsonPath::try_from("$.field[1]").expect("the path is correct")); - let v = path.find_slice(&json); - assert_eq!(v, vec![NoValue]); - } - - #[test] - fn no_value_filter_from_not_arr_filter_test() { - let json: Box = Box::new(json!({ - "field":"field", - })); - - let path: Box> = - Box::from(JsonPath::try_from("$.field[?(@ == 0)]").expect("the path is correct")); - let v = path.find_slice(&json); - assert_eq!(v, vec![NoValue]); - } - - #[test] - fn no_value_index_filter_test() { - let json: Box = Box::new(json!({ - "field":[{"f":1},{"f":0}], - })); - - let path: Box> = - Box::from(JsonPath::try_from("$.field[?(@.f_ == 0)]").expect("the path is correct")); - let v = path.find_slice(&json); - assert_eq!(v, vec![NoValue]); - } - - #[test] - fn no_value_decent_test() { - let json: Box = Box::new(json!({ - "field":[{"f":1},{"f":{"f_":1}}], - })); - - let path: Box> = - Box::from(JsonPath::try_from("$..f_").expect("the path is correct")); - let v = path.find_slice(&json); - assert_eq!( - v, - vec![Slice(&json!(1), "$.['field'][1].['f'].['f_']".to_string())] - ); - } - - #[test] - fn no_value_chain_test() { - let json: Box = Box::new(json!({ - "field":{"field":[1]}, - })); - - let path: Box> = - Box::from(JsonPath::try_from("$.field_.field").expect("the path is correct")); - let v = path.find_slice(&json); - assert_eq!(v, vec![NoValue]); - - let path: Box> = Box::from( - JsonPath::try_from("$.field_.field[?(@ == 1)]").expect("the path is correct"), - ); - let v = path.find_slice(&json); - assert_eq!(v, vec![NoValue]); - } - - #[test] - fn no_value_filter_test() { - // searching unexisting value returns length 0 - let json: Box = - Box::new(json!([{"verb": "TEST"},{"verb": "TEST"}, {"verb": "RUN"}])); - let path: Box> = Box::from( - JsonPath::try_from("$.[?(@.verb == \"RUN1\")]").expect("the path is correct"), - ); - let v = path.find(&json); - let js = json!(null); - assert_eq!(v, js); - } - - #[test] - fn no_value_len_test() { - let json: Box = Box::new(json!({ - "field":{"field":1}, - })); - - let path: Box> = - Box::from(JsonPath::try_from("$.field.field.length()").expect("the path is correct")); - let v = path.find_slice(&json); - assert_eq!(v, vec![NoValue]); - - let json: Box = Box::new(json!({ - "field":[{"a":1},{"a":1}], - })); - let path: Box> = Box::from( - JsonPath::try_from("$.field[?(@.a == 0)].f.length()").expect("the path is correct"), - ); - let v = path.find_slice(&json); - assert_eq!(v, vec![NoValue]); - } - - #[test] - fn no_clone_api_test() { - fn test_coercion(value: &Value) -> Value { - value.clone() - } - - let json: Value = serde_json::from_str(template_json()).expect("to get json"); - let query = - JsonPath::try_from("$..book[?(@.author size 10)].title").expect("the path is correct"); - - let results = query.find_slice_ptr(&json); - let v = results.first().expect("to get value"); - - // V can be implicitly converted to &Value - test_coercion(v); - - // To explicitly convert to &Value, use deref() - assert_eq!(v.deref(), &json!("Sayings of the Century")); - } - - #[test] - fn logical_exp_test() { - let json: Box = Box::new(json!({"first":{"second":[{"active":1},{"passive":1}]}})); - - let path: Box> = Box::from( - JsonPath::try_from("$.first[?(@.does_not_exist && @.does_not_exist >= 1.0)]") - .expect("the path is correct"), - ); - let v = path.find_slice(&json); - assert_eq!(v, vec![NoValue]); - - let path: Box> = Box::from( - JsonPath::try_from("$.first[?(@.does_not_exist >= 1.0)]").expect("the path is correct"), - ); - let v = path.find_slice(&json); - assert_eq!(v, vec![NoValue]); - } - - #[test] - fn regex_filter_test() { - let json: Box = Box::new(json!({ - "author":"abcd(Rees)", - })); - - let path: Box> = Box::from( - JsonPath::try_from("$.[?(@.author ~= '(?i)d\\(Rees\\)')]") - .expect("the path is correct"), - ); - assert_eq!( - path.find_slice(&json.clone()), - vec![Slice(&json!({"author":"abcd(Rees)"}), "$".to_string())] - ); - } - - #[test] - fn logical_not_exp_test() { - let json: Box = Box::new(json!({"first":{"second":{"active":1}}})); - let path: Box> = Box::from( - JsonPath::try_from("$.first[?(!@.does_not_exist >= 1.0)]") - .expect("the path is correct"), - ); - let v = path.find_slice(&json); - assert_eq!( - v, - vec![Slice( - &json!({"second":{"active": 1}}), - "$.['first']".to_string() - )] - ); - - let path: Box> = Box::from( - JsonPath::try_from("$.first[?(!(@.does_not_exist >= 1.0))]") - .expect("the path is correct"), - ); - let v = path.find_slice(&json); - assert_eq!( - v, - vec![Slice( - &json!({"second":{"active": 1}}), - "$.['first']".to_string() - )] - ); - - let path: Box> = Box::from( - JsonPath::try_from("$.first[?(!(@.second.active == 1) || @.second.active == 1)]") - .expect("the path is correct"), - ); - let v = path.find_slice(&json); - assert_eq!( - v, - vec![Slice( - &json!({"second":{"active": 1}}), - "$.['first']".to_string() - )] - ); - - let path: Box> = Box::from( - JsonPath::try_from("$.first[?(!@.second.active == 1 && !@.second.active == 1 || !@.second.active == 2)]") - .expect("the path is correct"), - ); - let v = path.find_slice(&json); - assert_eq!( - v, - vec![Slice( - &json!({"second":{"active": 1}}), - "$.['first']".to_string() - )] - ); - } - - #[test] - fn update_by_path_test() -> Result<(), JsonPathParserError> { - let mut json = json!([ - {"verb": "RUN","distance":[1]}, - {"verb": "TEST"}, - {"verb": "DO NOT RUN"} - ]); - - let path: Box = Box::from(JsonPath::try_from("$.[?(@.verb == 'RUN')]")?); - let elem = path - .find_as_path(&json) - .first() - .cloned() - .ok_or(JsonPathParserError::InvalidJsonPath("".to_string()))?; - - if let Some(v) = json - .reference_mut(elem)? - .and_then(|v| v.as_object_mut()) - .and_then(|v| v.get_mut("distance")) - .and_then(|v| v.as_array_mut()) - { - v.push(json!(2)) - } - - assert_eq!( - json, - json!([ - {"verb": "RUN","distance":[1,2]}, - {"verb": "TEST"}, - {"verb": "DO NOT RUN"} - ]) - ); - - Ok(()) - } -} diff --git a/src/lib.rs b/src/lib.rs index 948721b..ba96322 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -79,410 +79,40 @@ //! - `[::]`slice operator to get a list of element operating with their indexes. By default step = 1, start = 0, end = array len. The elements can be omitted ```[:]``` //! - `[?()]`the logical expression to filter elements in the list.It is used with arrays preliminary. //! -//! # Examples -//!```rust -//! use std::str::FromStr; -//! use serde_json::{json, Value}; -//! use jsonpath_rust::{jp_v, JsonPathValue, JsonPath}; -//! -//! fn test() -> Result<(), Box> { -//! let json = serde_json::from_str(r#"{"first":{"second":[{"active":1},{"passive":1}]}}"#)?; -//! let path = JsonPath::try_from("$.first.second[?(@.active)]")?; -//! let slice_of_data:Vec> = path.find_slice(&json); -//! let js = json!({"active":1}); -//! assert_eq!(slice_of_data, jp_v![&js;"$.first.second[0]",]); -//! # Ok(()) -//! } -//! ``` -//! //! //! [`there`]: https://goessner.net/articles/JsonPath/ +#![allow(warnings)] -pub use parser::model::JsonPath; -pub use parser::JsonPathParserError; -use serde_json::Value; -use std::fmt::Debug; -use std::ops::Deref; -use JsonPathValue::{NewValue, NoValue, Slice}; +pub mod query; -mod jsonpath; +#[allow(clippy::module_inception)] pub mod parser; -pub mod path; #[macro_use] extern crate pest_derive; extern crate core; extern crate pest; -/// the trait allows to query a path on any value by just passing the &str of as JsonPath. -/// -/// It is equal to -/// ```rust -/// # use serde_json::json; -/// # use std::str::FromStr; -/// use jsonpath_rust::JsonPath; -/// -/// let query = "$.hello"; -/// let json_path = JsonPath::from_str(query).unwrap(); -/// json_path.find(&json!({"hello": "world"})); -/// ``` -/// -/// It is default implemented for [Value]. -/// -/// #Note: -/// the result is going to be cloned and therefore it can be significant for the huge queries. -/// if the same &str is used multiple times, it's more efficient to reuse a single JsonPath. -/// -/// # Examples: -/// ``` -/// use std::str::FromStr; -/// use serde_json::{json, Value}; -/// use jsonpath_rust::jp_v; -/// use jsonpath_rust::{JsonPathQuery, JsonPath, JsonPathValue}; -/// -/// fn test() -> Result<(), Box> { -/// let json: Value = serde_json::from_str("{}")?; -/// let v = json.path("$..book[?(@.author size 10)].title")?; -/// assert_eq!(v, json!([])); -/// -/// let json: Value = serde_json::from_str("{}")?; -/// let path = json.path("$..book[?(@.author size 10)].title")?; -/// -/// assert_eq!(path, json!(["Sayings of the Century"])); -/// -/// let json: Value = serde_json::from_str("{}")?; -/// let path = JsonPath::try_from("$..book[?(@.author size 10)].title")?; -/// -/// let v = path.find_slice(&json); -/// let js = json!("Sayings of the Century"); -/// assert_eq!(v, jp_v![&js;"",]); -/// # Ok(()) -/// } -/// -/// ``` -pub trait JsonPathQuery { - fn path(self, query: &str) -> Result; -} - -/// Json paths may return either pointers to the original json or new data. This custom pointer type allows us to handle both cases. -/// Unlike JsonPathValue, this type does not represent NoValue to allow the implementation of Deref. -#[derive(Debug, PartialEq, Clone)] -pub enum JsonPtr<'a, Data> { - /// The slice of the initial json data - Slice(&'a Data), - /// The new data that was generated from the input data (like length operator) - NewValue(Data), -} - -/// Allow deref from json pointer to value. -impl Deref for JsonPtr<'_, Value> { - type Target = Value; - - fn deref(&self) -> &Self::Target { - match self { - JsonPtr::Slice(v) => v, - JsonPtr::NewValue(v) => v, - } - } -} - -impl JsonPathQuery for Value { - fn path(self, query: &str) -> Result { - let p = JsonPath::try_from(query)?; - Ok(p.find(&self)) - } -} - -/* -impl JsonPathQuery for T - where T: Deref { - fn path(self, query: &str) -> Result { - let p = JsonPath::from_str(query)?; - Ok(p.find(self.deref())) - } -} - */ - -/// just to create a json path value of data -/// Example: -/// - `jp_v(&json) = JsonPathValue::Slice(&json)` -/// - `jp_v(&json;"foo") = JsonPathValue::Slice(&json, "foo".to_string())` -/// - `jp_v(&json,) = vec![JsonPathValue::Slice(&json)]` -/// - `jp_v[&json1,&json1] = vec![JsonPathValue::Slice(&json1),JsonPathValue::Slice(&json2)]` -/// - `jp_v(json) = JsonPathValue::NewValue(json)` -/// ``` -/// use std::str::FromStr; -/// use serde_json::{json, Value}; -/// use jsonpath_rust::{jp_v, JsonPathQuery, JsonPath, JsonPathValue}; -/// -/// fn test() -> Result<(), Box> { -/// let json: Value = serde_json::from_str("{}")?; -/// let path = JsonPath::try_from("$..book[?(@.author size 10)].title")?; -/// let v = path.find_slice(&json); -/// -/// let js = json!("Sayings of the Century"); -/// assert_eq!(v, jp_v![&js;"",]); -/// # Ok(()) -/// } -/// ``` -#[macro_export] -macro_rules! jp_v { - (&$v:expr) =>{ - JsonPathValue::Slice(&$v, String::new()) - }; - - (&$v:expr ; $s:expr) =>{ - JsonPathValue::Slice(&$v, $s.to_string()) - }; - - ($(&$v:expr;$s:expr),+ $(,)?) =>{ - { - vec![ - $( - jp_v!(&$v ; $s), - )+ - ] - } - }; - - ($(&$v:expr),+ $(,)?) => { - { - vec![ - $( - jp_v!(&$v), - )+ - ] - } - }; - - ($v:expr) =>{ - JsonPathValue::NewValue($v) - }; - -} - -/// Represents the path of the found json data -pub type JsonPathStr = String; - -pub fn jsp_idx(prefix: &str, idx: usize) -> String { - format!("{}[{}]", prefix, idx) -} -pub fn jsp_obj(prefix: &str, key: &str) -> String { - format!("{}.['{}']", prefix, key) -} - -/// A result of json path -/// Can be either a slice of initial data or a new generated value(like length of array) -#[derive(Debug, PartialEq, Clone)] -pub enum JsonPathValue<'a, Data> { - /// The slice of the initial json data - Slice(&'a Data, JsonPathStr), - /// The new data that was generated from the input data (like length operator) - NewValue(Data), - /// The absent value that indicates the input data is not matched to the given json path (like the absent fields) - NoValue, -} - -impl<'a, Data: Clone + Debug + Default> JsonPathValue<'a, Data> { - /// Transforms given value into data either by moving value out or by cloning - pub fn to_data(self) -> Data { - match self { - Slice(r, _) => r.clone(), - NewValue(val) => val, - NoValue => Data::default(), - } - } - - /// Transforms given value into path - pub fn to_path(self) -> Option { - match self { - Slice(_, path) => Some(path), - _ => None, - } - } - - pub fn from_root(data: &'a Data) -> Self { - Slice(data, String::from("$")) - } - pub fn new_slice(data: &'a Data, path: String) -> Self { - Slice(data, path.to_string()) - } -} - -impl<'a, Data> JsonPathValue<'a, Data> { - pub fn only_no_value(input: &[JsonPathValue<'a, Data>]) -> bool { - !input.is_empty() && input.iter().filter(|v| v.has_value()).count() == 0 - } - - pub fn map_vec(data: Vec<(&'a Data, JsonPathStr)>) -> Vec> { - data.into_iter() - .map(|(data, pref)| Slice(data, pref)) - .collect() - } - - pub fn map_slice(self, mapper: F) -> Vec> - where - F: FnOnce(&'a Data, JsonPathStr) -> Vec<(&'a Data, JsonPathStr)>, - { - match self { - Slice(r, pref) => mapper(r, pref) - .into_iter() - .map(|(d, s)| Slice(d, s)) - .collect(), - - NewValue(_) => vec![], - no_v => vec![no_v], - } - } - - pub fn flat_map_slice(self, mapper: F) -> Vec> - where - F: FnOnce(&'a Data, JsonPathStr) -> Vec>, - { - match self { - Slice(r, pref) => mapper(r, pref), - _ => vec![NoValue], - } - } +use crate::query::queryable::Queryable; +use crate::query::{Queried, QueryPath, QueryRef}; +use serde_json::Value; - pub fn has_value(&self) -> bool { - !matches!(self, NoValue) +/// A trait for types that can be queried with JSONPath. +pub trait JsonPath: Queryable { + /// Queries the value with a JSONPath expression and returns a vector of `QueryResult`. + fn query_with_path(&self, path: &str) -> Queried>> { + query::js_path(path, self) } - pub fn vec_as_data(input: Vec>) -> Vec<&'a Data> { - input - .into_iter() - .filter_map(|v| match v { - Slice(el, _) => Some(el), - _ => None, - }) - .collect() - } - pub fn vec_as_pair(input: Vec>) -> Vec<(&'a Data, JsonPathStr)> { - input - .into_iter() - .filter_map(|v| match v { - Slice(el, v) => Some((el, v)), - _ => None, - }) - .collect() + /// Queries the value with a JSONPath expression and returns a vector of values. + fn query_only_path(&self, path: &str) -> Queried> { + query::js_path_path(path, self) } - /// moves a pointer (from slice) out or provides a default value when the value was generated - pub fn slice_or(self, default: &'a Data) -> &'a Data { - match self { - Slice(r, _) => r, - NewValue(_) | NoValue => default, - } + /// Queries the value with a JSONPath expression and returns a vector of values, omitting the path. + fn query(&self, path: &str) -> Queried> { + query::js_path_vals(path, self) } } -#[cfg(test)] -mod tests { - use colored::Colorize; - use serde_json::Value; - - use crate::JsonPath; - use std::str::FromStr; - - #[derive(serde::Deserialize)] - struct RFC9535Test { - name: String, - selector: String, - document: Option, - result: Option, - results: Option>, - #[serde(default)] - invalid_selector: bool, - } - - struct RFC9535Failure { - name: String, - message: String, - } - - #[test] - fn to_string_test() { - let path: Box> = Box::from( - JsonPath::from_str( - "$.['a'].a..book[1:3][*][1]['a','b'][?(@)][?(@.verb == 'TEST')].a.length()", - ) - .unwrap(), - ); - - assert_eq!( - path.to_string(), - "$.'a'.'a'..book[1:3:1][*][1]['a','b'][?(@ exists )][?(@.'verb' == \"TEST\")].'a'.length()" - ); - } - - #[test] - fn rfc9535_compliance_test() { - let file = std::fs::File::open("./src/fixtures/rfc9535-cts.json") - .expect("src/fixtures/rfc9535-cts.json must exist"); - - let reader = std::io::BufReader::new(file); - - let suite: Vec = serde_json::from_reader(reader) - .expect("compliance test suite file has valid structure"); - - let failures = suite - .iter() - .filter_map(|test| match rfc9535_compliance_test_case(test) { - Ok(_) => None, - Err(message) => Some(RFC9535Failure { - name: test.name.clone(), - message, - }), - }) - .inspect(|failure| { - println!( - "{}: {}", - failure.name.red().bold(), - failure.message.replace("\n", "\\n") - ); - }) - .count(); - - println!( - "\n{}:\n{}\n{}", - "RFC9535 Compliance tests".underline().bold(), - format!("Passed: {}", suite.len() - failures).green().bold(), - format!("Failed: {}", failures).red().bold() - ); - - assert!(failures != 0, "compliance test should be failing for now"); - } - - fn rfc9535_compliance_test_case(test: &RFC9535Test) -> Result<(), String> { - let path = JsonPath::::try_from(test.selector.as_str()); - if test.invalid_selector && path.is_ok() { - return Err(format!( - "path should have been considered invalid: {}", - test.selector - )); - } - - let path = path.map_err(|e| e.to_string())?; - let doc = test.document.as_ref().ok_or("document is required")?; - let actual = std::panic::catch_unwind(|| path.find(doc)); - if let Err(e) = &actual { - return Err(format!("panicked: {:?}", e)); - } - let actual = actual.unwrap(); - - if let Some(expected) = &test.result { - if actual != *expected { - return Err("actual did not match expected".to_string()); - } - } else if let Some(expected) = &test.results { - if !expected.iter().any(|exp| actual == *exp) { - return Err("actual result did not match any expected alternatives".to_string()); - } - } else { - return Err("no assertions were specified".to_string()); - } - - Ok(()) - } -} +impl JsonPath for Value {} diff --git a/src/parser.rs b/src/parser.rs new file mode 100644 index 0000000..add5276 --- /dev/null +++ b/src/parser.rs @@ -0,0 +1,424 @@ +#![allow(clippy::empty_docs)] +pub mod errors; +mod macros; +pub mod model; +mod tests; + +use crate::parser::errors::JsonPathError; +use crate::parser::model::{ + Comparable, Comparison, Filter, FilterAtom, FnArg, JpQuery, Literal, Segment, Selector, + SingularQuery, SingularQuerySegment, Test, TestFunction, +}; + +use pest::iterators::Pair; +use pest::Parser; + +#[derive(Parser)] +#[grammar = "parser/grammar/json_path_9535.pest"] +pub(super) struct JSPathParser; +const MAX_VAL: i64 = 9007199254740991; // Maximum safe integer value in JavaScript +const MIN_VAL: i64 = -9007199254740991; // Minimum safe integer value in JavaScript + +pub(super) type Parsed = Result; + +/// Parses a string into a [JsonPath]. +/// +/// # Errors +/// +/// Returns a variant of [crate::JsonPathParserError] if the parsing operation failed. +pub fn parse_json_path(jp_str: &str) -> Parsed { + if jp_str != jp_str.trim() { + Err(JsonPathError::InvalidJsonPath( + "Leading or trailing whitespaces".to_string(), + )) + } else { + JSPathParser::parse(Rule::main, jp_str) + .map_err(Box::new)? + .next() + .ok_or(JsonPathError::UnexpectedPestOutput) + .and_then(next_down) + .and_then(jp_query) + } +} + +pub fn jp_query(rule: Pair) -> Parsed { + Ok(JpQuery::new(segments(next_down(rule)?)?)) +} +pub fn rel_query(rule: Pair) -> Parsed> { + segments(next_down(rule)?) +} + +pub fn segments(rule: Pair) -> Parsed> { + let mut segments = vec![]; + for r in rule.into_inner() { + segments.push(segment(next_down(r)?)?); + } + Ok(segments) +} + +pub fn child_segment(rule: Pair) -> Parsed { + match rule.as_rule() { + Rule::wildcard_selector => Ok(Segment::Selector(Selector::Wildcard)), + Rule::member_name_shorthand => Ok(Segment::name(rule.as_str().trim())), + Rule::bracketed_selection => { + let mut selectors = vec![]; + for r in rule.into_inner() { + selectors.push(selector(r)?); + } + if selectors.len() == 1 { + Ok(Segment::Selector( + selectors + .into_iter() + .next() + .ok_or(JsonPathError::empty("selector"))?, + )) + } else { + Ok(Segment::Selectors(selectors)) + } + } + _ => Err(rule.into()), + } +} + +pub fn segment(child: Pair) -> Parsed { + match child.as_rule() { + Rule::child_segment => { + let val = child.as_str().strip_prefix(".").unwrap_or_default(); + if val != val.trim_start() { + Err(JsonPathError::InvalidJsonPath(format!( + "Invalid child segment `{}`", + child.as_str() + ))) + } else { + child_segment(next_down(child)?) + } + } + Rule::descendant_segment => { + if child + .as_str() + .chars() + .nth(2) + .ok_or(JsonPathError::empty(child.as_str()))? + .is_whitespace() + { + Err(JsonPathError::InvalidJsonPath(format!( + "Invalid descendant segment `{}`", + child.as_str() + ))) + } else { + Ok(Segment::Descendant(Box::new(child_segment(next_down( + child, + )?)?))) + } + } + _ => Err(child.into()), + } +} + +pub fn selector(rule: Pair) -> Parsed { + let child = next_down(rule)?; + match child.as_rule() { + Rule::name_selector => Ok(Selector::Name( + validate_js_str(child.as_str().trim())?.to_string(), + )), + Rule::wildcard_selector => Ok(Selector::Wildcard), + Rule::index_selector => Ok(Selector::Index(validate_range( + child + .as_str() + .trim() + .parse::() + .map_err(|e| (e, "wrong integer"))?, + )?)), + Rule::slice_selector => { + let (start, end, step) = slice_selector(child)?; + Ok(Selector::Slice(start, end, step)) + } + Rule::filter_selector => Ok(Selector::Filter(logical_expr(next_down(child)?)?)), + _ => Err(child.into()), + } +} + +pub fn function_expr(rule: Pair) -> Parsed { + 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"))?; + + // Check if the function name is valid namely nothing between the name and the opening parenthesis + if fn_str + .chars() + .nth(name.len()) + .map(|c| c != '(') + .unwrap_or_default() + { + Err(JsonPathError::InvalidJsonPath(format!( + "Invalid function expression `{}`", + fn_str + ))) + } else { + let mut args = vec![]; + for arg in elems { + let next = next_down(arg)?; + match next.as_rule() { + Rule::literal => args.push(FnArg::Literal(literal(next)?)), + Rule::test => args.push(FnArg::Test(Box::new(test(next)?))), + Rule::logical_expr => args.push(FnArg::Filter(logical_expr(next)?)), + + _ => return Err(next.into()), + } + } + + TestFunction::try_new(name, args) + } +} + +pub fn test(rule: Pair) -> Parsed { + let child = next_down(rule)?; + 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)?))), + _ => Err(child.into()), + } +} + +pub fn logical_expr(rule: Pair) -> Parsed { + let mut ors = vec![]; + for r in rule.into_inner() { + ors.push(logical_expr_and(r)?); + } + if ors.len() == 1 { + Ok(ors + .into_iter() + .next() + .ok_or(JsonPathError::empty("logical expression"))?) + } else { + Ok(Filter::Or(ors)) + } +} + +pub fn logical_expr_and(rule: Pair) -> Parsed { + let mut ands = vec![]; + for r in rule.into_inner() { + ands.push(Filter::Atom(filter_atom(r)?)); + } + if ands.len() == 1 { + Ok(ands + .into_iter() + .next() + .ok_or(JsonPathError::empty("logical expression"))?) + } else { + Ok(Filter::And(ands)) + } +} + +pub fn singular_query_segments(rule: Pair) -> Parsed> { + let mut segments = vec![]; + for r in rule.into_inner() { + match r.as_rule() { + Rule::name_segment => { + segments.push(SingularQuerySegment::Name( + next_down(r)?.as_str().trim().to_string(), + )); + } + Rule::index_segment => { + segments.push(SingularQuerySegment::Index( + next_down(r)? + .as_str() + .trim() + .parse::() + .map_err(|e| (e, "int"))?, + )); + } + _ => return Err(r.into()), + } + } + Ok(segments) +} +fn validate_range(val: i64) -> Result { + if val > MAX_VAL || val < MIN_VAL { + Err(JsonPathError::InvalidJsonPath(format!( + "Value {} is out of range", + val + ))) + } else { + Ok(val) + } +} +pub fn slice_selector(rule: Pair) -> Parsed<(Option, Option, Option)> { + let mut start = None; + let mut end = None; + let mut step = None; + let get_int = |r: Pair| r.as_str().trim().parse::().map_err(|e| (e, "int")); + + for r in rule.into_inner() { + match r.as_rule() { + Rule::start => start = Some(validate_range(get_int(r)?)?), + Rule::end => end = Some(validate_range(get_int(r)?)?), + Rule::step => { + step = { + if let Some(int) = r.into_inner().next() { + Some(validate_range(get_int(int)?)?) + } else { + None + } + } + } + + _ => return Err(r.into()), + } + } + Ok((start, end, step)) +} + +pub fn singular_query(rule: Pair) -> Parsed { + let query = next_down(rule)?; + let segments = singular_query_segments(next_down(query.clone())?)?; + match query.as_rule() { + Rule::rel_singular_query => Ok(SingularQuery::Current(segments)), + Rule::abs_singular_query => Ok(SingularQuery::Root(segments)), + _ => Err(query.into()), + } +} + +pub fn comp_expr(rule: Pair) -> Parsed { + let mut children = rule.into_inner(); + + let lhs = comparable(children.next().ok_or(JsonPathError::empty("comparison"))?)?; + let op = children + .next() + .ok_or(JsonPathError::empty("comparison"))? + .as_str(); + let rhs = comparable(children.next().ok_or(JsonPathError::empty("comparison"))?)?; + + Comparison::try_new(op, lhs, rhs) +} + +/// Validates a JSONPath string literal according to RFC 9535 +/// Control characters (U+0000 through U+001F and U+007F) are not allowed unescaped +/// in string literals, whether single-quoted or double-quoted +fn validate_js_str(s: &str) -> Parsed<&str> { + for (i, c) in s.chars().enumerate() { + if c <= '\u{001F}' { + return Err(JsonPathError::InvalidJsonPath(format!( + "Invalid control character U+{:04X} at position {} in string literal", + c as u32, i + ))); + } + } + + Ok(s) +} + +pub fn literal(rule: Pair) -> Parsed { + fn parse_number(num: &str) -> Parsed { + let num = num.trim(); + + if num.contains('.') || num.contains('e') || num.contains('E') { + Ok(Literal::Float(num.parse::().map_err(|e| (e, num))?)) + } else { + let num = num.trim().parse::().map_err(|e| (e, num))?; + if num > MAX_VAL || num < MIN_VAL { + Err(JsonPathError::InvalidNumber(format!( + "number out of bounds: {}", + num + ))) + } else { + Ok(Literal::Int(num)) + } + } + } + + fn parse_string(string: &str) -> Parsed { + let string = validate_js_str(string.trim())?; + if string.starts_with('\'') && string.ends_with('\'') { + Ok(Literal::String(string[1..string.len() - 1].to_string())) + } else if string.starts_with('"') && string.ends_with('"') { + Ok(Literal::String(string[1..string.len() - 1].to_string())) + } else { + Err(JsonPathError::InvalidJsonPath(format!( + "Invalid string literal `{}`", + string + ))) + } + } + + let first = next_down(rule)?; + + match first.as_rule() { + Rule::string => parse_string(first.as_str()), + Rule::number => parse_number(first.as_str()), + Rule::bool => Ok(Literal::Bool(first.as_str().parse::()?)), + Rule::null => Ok(Literal::Null), + + _ => Err(first.into()), + } +} + +pub fn filter_atom(pair: Pair) -> Parsed { + let rule = next_down(pair)?; + + match rule.as_rule() { + Rule::paren_expr => { + let mut not = false; + let mut logic_expr = None; + for r in rule.into_inner() { + match r.as_rule() { + Rule::not_op => not = true, + Rule::logical_expr => logic_expr = Some(logical_expr(r)?), + _ => (), + } + } + + logic_expr + .map(|expr| FilterAtom::filter(expr, not)) + .ok_or("Logical expression is absent".into()) + } + Rule::comp_expr => Ok(FilterAtom::cmp(Box::new(comp_expr(rule)?))), + Rule::test_expr => { + let mut not = false; + let mut test_expr = None; + for r in rule.into_inner() { + match r.as_rule() { + Rule::not_op => not = true, + Rule::test => test_expr = Some(test(r)?), + _ => (), + } + } + + test_expr + .map(|expr| FilterAtom::test(expr, not)) + .ok_or("Logical expression is absent".into()) + } + _ => Err(rule.into()), + } +} + +pub fn comparable(rule: Pair) -> Parsed { + let rule = next_down(rule)?; + match rule.as_rule() { + Rule::literal => Ok(Comparable::Literal(literal(rule)?)), + Rule::singular_query => Ok(Comparable::SingularQuery(singular_query(rule)?)), + Rule::function_expr => { + let tf = function_expr(rule)?; + if tf.is_comparable() { + Ok(Comparable::Function(tf)) + } else { + Err(JsonPathError::InvalidJsonPath(format!( + "Function {} is not comparable", + tf.to_string() + ))) + } + } + _ => Err(rule.into()), + } +} + +fn next_down(rule: Pair) -> Parsed> { + let rule_as_str = rule.as_str().to_string(); + rule.into_inner() + .next() + .ok_or(JsonPathError::InvalidJsonPath(rule_as_str)) +} diff --git a/src/parser/errors.rs b/src/parser/errors.rs index 7e89ec9..43f4b7c 100644 --- a/src/parser/errors.rs +++ b/src/parser/errors.rs @@ -1,13 +1,17 @@ +use crate::parser::Rule; +use crate::query::queryable::Queryable; +use pest::iterators::Pair; +use std::num::{ParseFloatError, ParseIntError}; +use std::str::ParseBoolError; use thiserror::Error; -use super::parser::Rule; - -#[derive(Error, Debug)] -pub enum JsonPathParserError { +/// This error type is used to represent errors that can occur during the parsing of JSONPath expressions. +#[derive(Error, Debug, PartialEq)] +pub enum JsonPathError { #[error("Failed to parse rule: {0}")] PestError(#[from] Box>), - #[error("Unexpected rule {0:?} when trying to parse logic atom: {1} within {2}")] - UnexpectedRuleLogicError(Rule, String, String), + #[error("Unexpected rule `{0:?}` when trying to parse `{1}`")] + UnexpectedRuleLogicError(Rule, String), #[error("Unexpected `none` when trying to parse logic atom: {0} within {1}")] UnexpectedNoneLogicError(String, String), #[error("Pest returned successful parsing but did not produce any output, that should be unreachable due to .pest definition file: SOI ~ chain ~ EOI")] @@ -27,3 +31,49 @@ pub enum JsonPathParserError { #[error("Invalid json path: {0}")] InvalidJsonPath(String), } + +impl JsonPathError { + pub fn empty(v: &str) -> Self { + JsonPathError::EmptyInner(v.to_string()) + } +} + +impl From for JsonPathError { + fn from(val: T) -> Self { + JsonPathError::InvalidJsonPath(format!("Result '{:?}' is not a reference", val)) + } +} + +impl From<&str> for JsonPathError { + fn from(val: &str) -> Self { + JsonPathError::EmptyInner(val.to_string()) + } +} + +impl From<(ParseIntError, &str)> for JsonPathError { + fn from((err, val): (ParseIntError, &str)) -> Self { + JsonPathError::InvalidNumber(format!("{:?} for `{}`", err, val)) + } +} + +impl From<(JsonPathError, &str)> for JsonPathError { + fn from((err, val): (JsonPathError, &str)) -> Self { + JsonPathError::InvalidJsonPath(format!("{:?} for `{}`", err, val)) + } +} + +impl From<(ParseFloatError, &str)> for JsonPathError { + fn from((err, val): (ParseFloatError, &str)) -> Self { + JsonPathError::InvalidNumber(format!("{:?} for `{}`", err, val)) + } +} +impl From for JsonPathError { + fn from(err: ParseBoolError) -> Self { + JsonPathError::InvalidJsonPath(format!("{:?} ", err)) + } +} +impl From> for JsonPathError { + fn from(rule: Pair) -> Self { + JsonPathError::UnexpectedRuleLogicError(rule.as_rule(), rule.as_str().to_string()) + } +} diff --git a/src/parser/grammar/json_path.pest b/src/parser/grammar/json_path.pest deleted file mode 100644 index 5d2041e..0000000 --- a/src/parser/grammar/json_path.pest +++ /dev/null @@ -1,55 +0,0 @@ -WHITESPACE = _{ " " | "\t" | "\r\n" | "\n"} - -boolean = {"true" | "false"} -null = {"null"} - -min = _{"-"} -col = _{":"} -dot = _{ "." } -word = _{ ('a'..'z' | 'A'..'Z')+ } -specs = _{ "_" | "-" | "/" | "\\" | "#" } -number = @{"-"? ~ ("0" | ASCII_NONZERO_DIGIT ~ ASCII_DIGIT*) ~ ("." ~ ASCII_DIGIT+)? ~ (^"e" ~ ("+" | "-")? ~ ASCII_DIGIT+)?} - -string_qt = ${ ("\'" ~ inner ~ "\'") | ("\"" ~ inner ~ "\"") } -inner = @{ char* } -char = _{ - !("\"" | "\\" | "\'") ~ ANY - | "\\" ~ ("\"" | "\'" | "\\" | "/" | "b" | "f" | "n" | "r" | "t" | "(" | ")") - | "\\" ~ ("u" ~ ASCII_HEX_DIGIT{4}) -} -root = {"$"} -sign = { "==" | "!=" | "~=" | ">=" | ">" | "<=" | "<" | "in" | "nin" | "size" | "noneOf" | "anyOf" | "subsetOf"} -not = {"!"} -key_lim = {!"length()" ~ (word | ASCII_DIGIT | specs)+} -key_unlim = {"[" ~ string_qt ~ "]"} -key = ${key_lim | key_unlim} - -descent = {dot ~ dot ~ key} -descent_w = {dot ~ dot ~ "*"} // refactor afterwards -wildcard = {dot? ~ "[" ~"*"~"]" | dot ~ "*"} -current = {"@" ~ chain?} -field = ${dot? ~ key_unlim | dot ~ key_lim } -function = { dot ~ "length" ~ "(" ~ ")"} -unsigned = {("0" | ASCII_NONZERO_DIGIT ~ ASCII_DIGIT*)} -signed = {min? ~ unsigned} -start_slice = {signed} -end_slice = {signed} -step_slice = {col ~ unsigned} -slice = {start_slice? ~ col ~ end_slice? ~ step_slice? } - -unit_keys = { string_qt ~ ("," ~ string_qt)+ } -unit_indexes = { number ~ ("," ~ number)+ } -filter = {"?"~ "(" ~ logic_or ~ ")"} - -logic_or = {logic_and ~ ("||" ~ logic_and)*} -logic_and = {logic_not ~ ("&&" ~ logic_not)*} -logic_not = {not? ~ logic_atom} -logic_atom = {atom ~ (sign ~ atom)? | "(" ~ logic_or ~ ")"} - -atom = {chain | string_qt | number | boolean | null} - -index = {dot? ~ "["~ (unit_keys | unit_indexes | slice | unsigned |filter) ~ "]" } - -chain = {(root | descent | descent_w | wildcard | current | field | index | function)+} - -path = {SOI ~ chain ~ EOI } \ No newline at end of file diff --git a/src/parser/grammar/json_path_9535.pest b/src/parser/grammar/json_path_9535.pest new file mode 100644 index 0000000..18c42b6 --- /dev/null +++ b/src/parser/grammar/json_path_9535.pest @@ -0,0 +1,84 @@ +main = { SOI ~ jp_query ~ EOI } +jp_query = {root ~ segments} +segments = {(S ~ segment)*} +segment = { child_segment | descendant_segment } +child_segment = { bracketed_selection | ("." ~ (wildcard_selector | member_name_shorthand)) } +bracketed_selection = { "[" ~ S ~ selector ~ (S ~ "," ~ S ~ selector)* ~ S ~ "]" } +descendant_segment = { ".." ~ (bracketed_selection | wildcard_selector | member_name_shorthand)} +selector = {name_selector | wildcard_selector | slice_selector| index_selector | filter_selector} + +root = _{"$"} +name_selector = {string} +wildcard_selector = {"*"} +index_selector = {int} +int = { "0" | ("-"? ~ DIGIT1 ~ DIGIT*) } +step = {":" ~ S ~ int?} +start = {int} +end = {int} +slice_selector = { start? ~ S ~ ":" ~ S ~ end? ~ S ~ step? } +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 ~ "(" ~ S ~ (function_argument ~ (S ~ "," ~ S ~ function_argument)*)? ~ S ~ ")" } +function_name = { function_name_first ~ function_name_char* } +function_name_first = { LCALPHA } +function_name_char = { function_name_first | "_" | DIGIT } +function_argument = { literal | test | logical_expr } +comparable = { literal | singular_query | function_expr } +literal = { number | string | bool | null } +bool = {"true" | "false"} +null = {"null"} +singular_query = { rel_singular_query | abs_singular_query } +rel_singular_query = { curr ~ singular_query_segments } +abs_singular_query = { root ~ singular_query_segments } +singular_query_segments = { (S ~ (name_segment | index_segment))* } +name_segment = { ("[" ~ name_selector ~ "]") | ("." ~ member_name_shorthand) } +index_segment = { "[" ~ index_selector ~ "]" } +comp_op = { "==" | "!=" | "<=" | ">=" | "<" | ">" | "in" | "nin" | "size" | "noneOf" | "anyOf" | "subsetOf"} + +LCALPHA = { 'a'..'z' } + + +string = { "\"" ~ double_quoted* ~ "\"" | "\'" ~ single_quoted* ~ "\'" } +double_quoted = _{ unescaped | "\'" | ESC ~ "\"" | ESC ~ escapable } +single_quoted = _{ unescaped | "\"" | ESC ~ "\'" | ESC ~ escapable } +escapable = _{ + "b" | "f" | "n" | "r" | "t" | "/" | "\\" | ("u" ~ hexchar) +} + +member_name_shorthand = { name_first ~ name_char* } +name_first = { ALPHA | "_" | '\u{0080}'..'\u{D7FF}' | '\u{E000}'..'\u{10FFFF}' } +name_char = { name_first | DIGIT } +not_op = {"!"} +curr = _{"@"} +ESC = _{ "\\" } +unescaped = _{ + '\u{0020}'..'\u{0021}' | + '\u{0023}'..'\u{0026}' | + '\u{0028}'..'\u{005B}' | + '\u{005D}'..'\u{D7FF}' | + '\u{E000}'..'\u{10FFFF}' +} + +S = _{ WHITESPACE* } +hexchar = _{ non_surrogate | (high_surrogate ~ "\\" ~ "u" ~ low_surrogate) } +number = { (int | "-0") ~ frac? ~ exp? } +frac = { "." ~ DIGIT+ } +exp = { ("e" | "E") ~ ("-" | "+")? ~ DIGIT+ } +non_surrogate = _{ (DIGIT | "A" | "B" | "C" | "E" | "F") ~ HEXDIG{3} | ("D" ~ ('0'..'7') ~ HEXDIG{2}) } + +high_surrogate = _{ "D" ~ ("8" | "9" | "A" | "B") ~ HEXDIG{2} } + +low_surrogate = _{ "D" ~ ("C" | "D" | "E" | "F") ~ HEXDIG{2} } + +HEXDIG = _{ DIGIT | "A" | "B" | "C" | "D" | "E" | "F" } +DIGIT = _{ ASCII_DIGIT } +DIGIT1 = _{ ASCII_NONZERO_DIGIT} +ALPHA = { ASCII_ALPHA } +WHITESPACE = _{ " " | "\t" | "\r\n" | "\n" | "\r"} \ No newline at end of file diff --git a/src/parser/macros.rs b/src/parser/macros.rs index 9bc732d..a10a8ee 100644 --- a/src/parser/macros.rs +++ b/src/parser/macros.rs @@ -1,110 +1,227 @@ +use crate::parser::model::{ + Comparable, Filter, FilterAtom, FnArg, Literal, Segment, Selector, SingularQuery, Test, +}; + +#[macro_export] +macro_rules! lit { + () => { + Literal::Null + }; + (b$b:expr ) => { + Literal::Bool($b) + }; + (s$s:expr) => { + Literal::String($s.to_string()) + }; + (i$n:expr) => { + Literal::Int($n) + }; + (f$n:expr) => { + Literal::Float($n) + }; +} + +#[macro_export] +macro_rules! q_segments { + ($segment:tt) => { + vec![q_segment!($segment)] + }; + // Recursive case: multiple segments + ($segment:tt $($rest:tt)*) => {{ + let mut segments = q_segments!($($rest)*); + segments.insert(0, q_segment!($segment)); + segments + }}; +} + #[macro_export] -macro_rules! filter { - () => {FilterExpression::Atom(op!,FilterSign::new(""),op!())}; - ( $left:expr, $s:literal, $right:expr) => { - FilterExpression::Atom($left,FilterSign::new($s),$right) - }; - ( $left:expr,||, $right:expr) => {FilterExpression::Or(Box::new($left),Box::new($right)) }; - ( $left:expr,&&, $right:expr) => {FilterExpression::And(Box::new($left),Box::new($right)) }; -} - -#[macro_export] -macro_rules! op { - ( ) => { - Operand::Dynamic(Box::new(JsonPath::Empty)) - }; - ( $s:literal) => { - Operand::Static(json!($s)) - }; - ( s $s:expr) => { - Operand::Static(json!($s)) - }; - ( $s:expr) => { - Operand::Dynamic(Box::new($s)) - }; -} - -#[macro_export] -macro_rules! idx { - ( $s:literal) => {JsonPathIndex::Single(json!($s))}; - ( idx $($ss:literal),+) => {{ - let ss_vec = vec![ - $( - json!($ss), - )+ - ]; - JsonPathIndex::UnionIndex(ss_vec) - }}; - ( $($ss:literal),+) => {{ - let ss_vec = vec![ - $( - $ss.to_string(), - )+ - ]; - JsonPathIndex::UnionKeys(ss_vec) - }}; - ( $s:literal) => {JsonPathIndex::Single(json!($s))}; - ( ? $s:expr) => {JsonPathIndex::Filter($s)}; - ( [$l:literal;$m:literal;$r:literal]) => {JsonPathIndex::Slice($l,$m,$r)}; - ( [$l:literal;$m:literal;]) => {JsonPathIndex::Slice($l,$m,1)}; - ( [$l:literal;;$m:literal]) => {JsonPathIndex::Slice($l,0,$m)}; - ( [;$l:literal;$m:literal]) => {JsonPathIndex::Slice(0,$l,$m)}; - ( [;;$m:literal]) => {JsonPathIndex::Slice(0,0,$m)}; - ( [;$m:literal;]) => {JsonPathIndex::Slice(0,$m,1)}; - ( [$m:literal;;]) => {JsonPathIndex::Slice($m,0,1)}; - ( [;;]) => {JsonPathIndex::Slice(0,0,1)}; -} - -#[macro_export] -macro_rules! chain { - ($($ss:expr),+) => {{ - let ss_vec = vec![ - $( - $ss, - )+ - ]; - JsonPath::Chain(ss_vec) - }}; -} - -/// Can be used to Parse a JsonPath with a more native way. -/// e.g. -/// ```rust -/// use jsonpath_rust::{path, JsonPath}; -/// use std::str::FromStr; -/// use serde_json::Value; -/// -/// let path:JsonPath = JsonPath::from_str(".abc.*").unwrap(); -/// let path2 = JsonPath::Chain(vec![path!("abc"), path!(*)]); -/// assert_eq!(path, path2); -/// ``` -#[macro_export] -macro_rules! path { - ( ) => {JsonPath::Empty}; - (*) => {JsonPath::Wildcard}; - ($) => {JsonPath::Root}; - (@) => {JsonPath::Current(Box::new(JsonPath::Empty))}; - (@$e:expr) => {JsonPath::Current(Box::new($e))}; - (@,$($ss:expr),+) => {{ - let ss_vec = vec![ - $( - $ss, - )+ - ]; - let chain = JsonPath::Chain(ss_vec); - JsonPath::Current(Box::new(chain)) - }}; - (..$e:literal) => {JsonPath::Descent($e.to_string())}; - (..*) => {JsonPath::DescentW}; - ($e:literal) => {JsonPath::Field($e.to_string())}; - ($e:expr) => {JsonPath::Index($e)}; -} - -#[cfg(test)] -pub(crate) use chain; -#[cfg(test)] -pub(crate) use filter; -#[cfg(test)] -pub(crate) use idx; -#[cfg(test)] -pub(crate) use op; +macro_rules! q_segment { + ($name:ident) => { + SingularQuerySegment::Name(stringify!($name).to_string()) + }; + ([$name:ident]) => { + SingularQuerySegment::Name(format!("\"{}\"", stringify!($name))) + }; + ([$index:expr]) => { + SingularQuerySegment::Index($index) + }; +} +#[macro_export] +macro_rules! singular_query { + (@ $($segment:tt)*) => { + SingularQuery::Current(q_segments!($($segment)*)) + }; + ($($segment:tt)*) => { + SingularQuery::Root(q_segments!($($segment)*)) + }; +} + +#[macro_export] +macro_rules! slice { + () => { + (None, None, None) + }; + ($start:expr) => { + (Some($start), None, None) + }; + ($start:expr, $end:expr) => { + (Some($start), Some($end), None) + }; + ($start:expr,, $step:expr) => { + (Some($start), None, Some($step)) + }; + (,, $step:expr) => { + (None, None, Some($step)) + }; + (, $end:expr) => { + (None, Some($end), None) + }; + (, $end:expr,$step:expr ) => { + (None, Some($end), Some($step)) + }; + ($start:expr, $end:expr, $step:expr) => { + (Some($start), Some($end), Some($step)) + }; +} + +#[macro_export] +macro_rules! test_fn { + ($name:ident $arg:expr) => { + TestFunction::try_new(stringify!($name), vec![$arg]).unwrap() + }; + + ($name:ident $arg1:expr, $arg2:expr ) => { + TestFunction::try_new(stringify!($name), vec![$arg1, $arg2]).unwrap() + }; +} + +#[macro_export] +macro_rules! arg { + ($arg:expr) => { + FnArg::Literal($arg) + }; + (t $arg:expr) => { + FnArg::Test(Box::new($arg)) + }; + (f $arg:expr) => { + FnArg::Filter($arg) + }; +} + +#[macro_export] +macro_rules! test { + (@ $($segments:expr)*) => { Test::RelQuery(vec![$($segments),*]) }; + (S $jq:expr) => { Test::AbsQuery($jq) }; + ($tf:expr) => { Test::Function(Box::new($tf)) }; + +} + +#[macro_export] +macro_rules! or { + ($($items:expr),*) => { + crate::parser::model::Filter::Or(vec![ $($items),* ]) + }; +} + +#[macro_export] +macro_rules! and { + ($($items:expr),*) => { + crate::parser::model::Filter::And(vec![ $($items),* ]) + }; +} + +#[macro_export] +macro_rules! filter_ { + ($item:expr) => { + crate::parser::model::Filter::Atom($item) + }; + + (or $($items:expr),*) => { + crate::parser::model::Filter::Or(vec![ $($items),* ]) + }; + + (and $($items:expr),*) => { + crate::parser::model::Filter::And(vec![ $($items),* ]) + }; +} + +#[macro_export] +macro_rules! atom { + (! $filter:expr) => { + FilterAtom::filter($filter, true) + }; + ($filter:expr) => { + FilterAtom::filter($filter, false) + }; + (t! $filter:expr) => { + FilterAtom::test($filter, true) + }; + (t $filter:expr) => { + FilterAtom::filter($filter, false) + }; + ($lhs:expr, $s:expr, $rhs:expr) => { + FilterAtom::Comparison(Box::new(cmp!($lhs, $s, $rhs))) + }; +} + +#[macro_export] +macro_rules! cmp { + ($lhs:expr, $op:expr , $rhs:expr) => { + Comparison::try_new($op, $lhs, $rhs).unwrap() + }; +} + +#[macro_export] +macro_rules! comparable { + ($lit:expr) => { + Comparable::Literal($lit) + }; + (f $func:expr) => { + Comparable::Function($func) + }; + (> $sq:expr) => { + Comparable::SingularQuery($sq) + }; +} + +#[macro_export] +macro_rules! selector { + (*) => { + Selector::Wildcard + }; + (?$filter:expr) => { + Selector::Filter($filter) + }; + (slice $slice:expr) => { + slice_from($slice) + }; + ($name:ident) => { + Selector::Name(stringify!($name).to_string()) + }; + ([$name:ident]) => { + Selector::Name(format!("\"{}\"", stringify!($name))) + }; + ([$index:expr]) => { + Selector::Index($index) + }; +} + +#[macro_export] +macro_rules! segment { + (..$segment:expr) => { + Segment::Descendant(Box::new($segment)) + }; + ($selector:expr) => { + Segment::Selector($selector) + }; + ($($selectors:expr),*) => { + Segment::Selectors(vec![$($selectors),*]) + }; +} + +#[macro_export] +macro_rules! jq { + ($($segment:expr),*) => { + JpQuery::new(vec![$($segment),*]) + }; +} diff --git a/src/parser/mod.rs b/src/parser/mod.rs deleted file mode 100644 index 5cf0786..0000000 --- a/src/parser/mod.rs +++ /dev/null @@ -1,15 +0,0 @@ -//! The parser for the jsonpath. -//! The module grammar denotes the structure of the parsing grammar - -mod errors; -pub(crate) mod macros; -pub(crate) mod model; -#[allow(clippy::module_inception)] -pub(crate) mod parser; - -pub use errors::JsonPathParserError; -pub use model::FilterExpression; -pub use model::JsonPath; -pub use model::JsonPathIndex; -pub use model::Operand; -pub use parser::{parse_json_path, Rule}; diff --git a/src/parser/model.rs b/src/parser/model.rs index 2f40e94..72ecd80 100644 --- a/src/parser/model.rs +++ b/src/parser/model.rs @@ -1,334 +1,483 @@ -use serde_json::Value; +use crate::parser::errors::JsonPathError; +use crate::parser::Parsed; +use std::fmt::{Display, Formatter}; -use crate::path::JsonLike; +/// Represents a JSONPath query with a list of segments. +#[derive(Debug, Clone, PartialEq)] +pub struct JpQuery { + pub segments: Vec, +} -use super::errors::JsonPathParserError; -use super::parse_json_path; -use std::fmt::{Display, Formatter}; -use std::{convert::TryFrom, str::FromStr}; - -/// The basic structures for parsing json paths. -/// The common logic of the structures pursues to correspond the internal parsing structure. -/// -/// usually it's created by using [`FromStr`] or [`TryFrom<&str>`] -#[derive(Debug, Clone)] -pub enum JsonPath { - /// The $ operator - Root, - /// Field represents key - Field(String), - /// The whole chain of the path. - Chain(Vec>), - /// The .. operator - Descent(String), - /// The ..* operator - DescentW, - /// The indexes for array - Index(JsonPathIndex), - /// The @ operator - Current(Box>), - /// The * operator - Wildcard, - /// The item uses to define the unresolved state - Empty, - /// Functions that can calculate some expressions - Fn(Function), +impl JpQuery { + pub fn new(segments: Vec) -> Self { + JpQuery { segments } + } } -impl Display for JsonPath { +impl Display for JpQuery { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - let str = match self { - JsonPath::Root => "$".to_string(), - JsonPath::Field(e) => format!(".'{}'", e), - JsonPath::Chain(elems) => elems.iter().map(ToString::to_string).collect::(), - JsonPath::Descent(e) => { - format!("..{}", e) - } - JsonPath::DescentW => "..*".to_string(), - JsonPath::Index(e) => e.to_string(), - JsonPath::Current(e) => format!("@{}", e), - JsonPath::Wildcard => "[*]".to_string(), - JsonPath::Empty => "".to_string(), - JsonPath::Fn(e) => format!(".{}", e), - }; - write!(f, "{}", str) + write!( + f, + "${}", + self.segments + .iter() + .map(|s| s.to_string()) + .collect::() + ) } } +/// Enum representing different types of segments in a JSONPath query. +#[derive(Debug, Clone, PartialEq)] +pub enum Segment { + /// Represents a descendant segment. + Descendant(Box), + /// Represents a selector segment. + Selector(Selector), + /// Represents multiple selectors. + Selectors(Vec), +} -impl TryFrom<&str> for JsonPath -where - T: JsonLike, -{ - type Error = JsonPathParserError; - - /// Parses a string into a [JsonPath]. - /// - /// # Errors - /// - /// Returns a variant of [JsonPathParserError] if the parsing operation failed. - fn try_from(value: &str) -> Result { - parse_json_path(value) +impl Segment { + pub fn name(name: &str) -> Self { + Segment::Selector(Selector::Name(name.to_string())) } } -impl FromStr for JsonPath -where - T: JsonLike, -{ - type Err = JsonPathParserError; - - /// Parses a string into a [JsonPath]. - /// - /// # Errors - /// - /// Returns a variant of [JsonPathParserError] if the parsing operation failed. - fn from_str(value: &str) -> Result { - parse_json_path(value) +impl Display for Segment { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Segment::Descendant(s) => write!(f, "..{}", s), + Segment::Selector(selector) => write!(f, "{}", selector), + Segment::Selectors(selectors) => write!( + f, + "{}", + selectors.iter().map(|s| s.to_string()).collect::() + ), + } } } +/// Enum representing different types of selectors in a JSONPath query. +#[derive(Debug, Clone, PartialEq)] +pub enum Selector { + /// Represents a name selector. + Name(String), + /// Represents a wildcard selector. + Wildcard, + /// Represents an index selector. + Index(i64), + /// Represents a slice selector. + Slice(Option, Option, Option), + /// Represents a filter selector. + Filter(Filter), +} -#[derive(Debug, PartialEq, Clone)] -pub enum Function { - /// length() - Length, +pub fn slice_from((start, end, step): (Option, Option, Option)) -> Selector { + Selector::Slice(start, end, step) } -impl Display for Function { +impl Display for Selector { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - let str = match self { - Function::Length => "length()".to_string(), - }; - write!(f, "{}", str) + match self { + Selector::Name(name) => write!(f, "{}", name), + Selector::Wildcard => write!(f, "*"), + Selector::Index(index) => write!(f, "{}", index), + Selector::Slice(start, end, step) => write!( + f, + "{}:{}:{}", + start.unwrap_or(0), + end.unwrap_or(0), + step.unwrap_or(1) + ), + Selector::Filter(filter) => write!(f, "[?{}]", filter), + } } } - -#[derive(Debug, Clone)] -pub enum JsonPathIndex { - /// A single element in array - Single(T), - /// Union represents a several indexes - UnionIndex(Vec), - /// Union represents a several keys - UnionKeys(Vec), - /// DEfault slice where the items are start/end/step respectively - Slice(i32, i32, usize), - /// Filter ?() - Filter(FilterExpression), +/// Enum representing different types of filters in a JSONPath query. +#[derive(Debug, Clone, PartialEq)] +pub enum Filter { + /// Represents a logical OR filter. + Or(Vec), + /// Represents a logical AND filter. + And(Vec), + /// Represents an atomic filter. + Atom(FilterAtom), } -impl Display for JsonPathIndex { +impl Display for Filter { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - let str = match self { - JsonPathIndex::Single(e) => format!("[{}]", e), - JsonPathIndex::UnionIndex(elems) => { - format!( - "[{}]", - elems - .iter() - .map(ToString::to_string) - .collect::>() - .join(",") - ) - } - JsonPathIndex::UnionKeys(elems) => { - format!( - "[{}]", - elems - .iter() - .map(|el| format!("'{}'", el)) - .collect::>() - .join(",") - ) - } - JsonPathIndex::Slice(s, e, st) => { - format!("[{}:{}:{}]", s, e, st) - } - JsonPathIndex::Filter(filter) => format!("[?({})]", filter), + let items_to_str = |items: &Vec, sep: &str| { + items + .iter() + .map(|f| f.to_string()) + .collect::>() + .join(sep) }; - write!(f, "{}", str) + + match self { + Filter::Or(filters) => write!(f, "{}", items_to_str(filters, " || ")), + Filter::And(filters) => write!(f, "{}", items_to_str(filters, " && ")), + Filter::Atom(atom) => write!(f, "{}", atom), + } } } +/// Enum representing different types of atomic filters in a JSONPath query. #[derive(Debug, Clone, PartialEq)] -pub enum FilterExpression { - /// a single expression like a > 2 - Atom(Operand, FilterSign, Operand), - /// and with && - And(Box>, Box>), - /// or with || - Or(Box>, Box>), - /// not with ! - Not(Box>), -} - -impl Display for FilterExpression { +pub enum FilterAtom { + /// Represents a nested filter with an optional NOT flag. + Filter { expr: Box, not: bool }, + /// Represents a test filter with an optional NOT flag. + Test { expr: Box, not: bool }, + /// Represents a comparison filter. + Comparison(Box), +} + +impl FilterAtom { + pub fn filter(expr: Filter, not: bool) -> Self { + FilterAtom::Filter { + expr: Box::new(expr), + not, + } + } + + pub fn test(expr: Test, not: bool) -> Self { + FilterAtom::Test { + expr: Box::new(expr), + not, + } + } + + pub fn cmp(cmp: Box) -> Self { + FilterAtom::Comparison(cmp) + } +} + +impl Display for FilterAtom { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - let str = match self { - FilterExpression::Atom(left, sign, right) => { - format!("{} {} {}", left, sign, right) + match self { + FilterAtom::Filter { expr, not } => { + if *not { + write!(f, "!{}", expr) + } else { + write!(f, "{}", expr) + } } - FilterExpression::And(left, right) => { - format!("{} && {}", left, right) + FilterAtom::Test { expr, not } => { + if *not { + write!(f, "!{}", expr) + } else { + write!(f, "{}", expr) + } } - FilterExpression::Or(left, right) => { - format!("{} || {}", left, right) - } - FilterExpression::Not(expr) => { - format!("!{}", expr) - } - }; - write!(f, "{}", str) + FilterAtom::Comparison(cmp) => write!(f, "{}", cmp), + } } } +/// Enum representing different types of comparisons in a JSONPath query. +#[derive(Debug, Clone, PartialEq)] +pub enum Comparison { + /// Represents an equality comparison. + Eq(Comparable, Comparable), + /// Represents a non-equality comparison. + Ne(Comparable, Comparable), + /// Represents a greater-than comparison. + Gt(Comparable, Comparable), + /// Represents a greater-than-or-equal-to comparison. + Gte(Comparable, Comparable), + /// Represents a less-than comparison. + Lt(Comparable, Comparable), + /// Represents a less-than-or-equal-to comparison. + Lte(Comparable, Comparable), +} -impl FilterExpression { - pub fn exists(op: Operand) -> Self { - FilterExpression::Atom( - op, - FilterSign::Exists, - Operand::Dynamic(Box::new(JsonPath::Empty)), - ) +impl Comparison { + pub fn try_new(op: &str, left: Comparable, right: Comparable) -> Parsed { + match op { + "==" => Ok(Comparison::Eq(left, right)), + "!=" => Ok(Comparison::Ne(left, right)), + ">" => Ok(Comparison::Gt(left, right)), + ">=" => Ok(Comparison::Gte(left, right)), + "<" => Ok(Comparison::Lt(left, right)), + "<=" => Ok(Comparison::Lte(left, right)), + _ => Err(JsonPathError::InvalidJsonPath(format!( + "Invalid comparison operator: {}", + op + ))), + } + } + + pub fn vals(&self) -> (&Comparable, &Comparable) { + match self { + Comparison::Eq(left, right) => (left, right), + Comparison::Ne(left, right) => (left, right), + Comparison::Gt(left, right) => (left, right), + Comparison::Gte(left, right) => (left, right), + Comparison::Lt(left, right) => (left, right), + Comparison::Lte(left, right) => (left, right), + } + } +} + +impl Display for Comparison { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Comparison::Eq(left, right) => write!(f, "{} == {}", left, right), + Comparison::Ne(left, right) => write!(f, "{} != {}", left, right), + Comparison::Gt(left, right) => write!(f, "{} > {}", left, right), + Comparison::Gte(left, right) => write!(f, "{} >= {}", left, right), + Comparison::Lt(left, right) => write!(f, "{} < {}", left, right), + Comparison::Lte(left, right) => write!(f, "{} <= {}", left, right), + } } } -/// Operand for filtering expressions -#[derive(Debug, Clone)] -pub enum Operand { - Static(T), - Dynamic(Box>), +/// Enum representing different types of comparable values in a JSONPath query. +#[derive(Debug, Clone, PartialEq)] +pub enum Comparable { + /// Represents a literal value. + Literal(Literal), + /// Represents a function. + Function(TestFunction), + /// Represents a singular query. + SingularQuery(SingularQuery), } -impl Display for Operand { +impl Display for Comparable { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - let str = match self { - Operand::Static(e) => e.to_string(), - Operand::Dynamic(e) => e.to_string(), - }; - write!(f, "{}", str) + match self { + Comparable::Literal(literal) => write!(f, "{}", literal), + Comparable::Function(func) => write!(f, "{}", func), + Comparable::SingularQuery(query) => write!(f, "{}", query), + } } } -#[allow(dead_code)] -impl Operand { - pub fn val(v: T) -> Self { - Operand::Static(v) +/// Enum representing different types of singular queries in a JSONPath query. +#[derive(Debug, Clone, PartialEq)] +pub enum SingularQuery { + /// Represents a current node query. + Current(Vec), + /// Represents a root node query. + Root(Vec), +} + +impl Display for SingularQuery { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + SingularQuery::Current(segments) => write!( + f, + "@.{}", + segments.iter().map(|s| s.to_string()).collect::() + ), + SingularQuery::Root(segments) => write!( + f, + "$.{}", + segments.iter().map(|s| s.to_string()).collect::() + ), + } } } -/// The operators for filtering functions +/// Enum representing different types of singular query segments in a JSONPath query. #[derive(Debug, Clone, PartialEq)] -pub enum FilterSign { - Equal, - Unequal, - Less, - Greater, - LeOrEq, - GrOrEq, - Regex, - In, - Nin, - Size, - NoneOf, - AnyOf, - SubSetOf, - Exists, -} - -impl Display for FilterSign { +pub enum SingularQuerySegment { + /// Represents an index segment. + Index(i64), + /// Represents a name segment. + Name(String), +} + +impl Display for SingularQuerySegment { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - let str = match self { - FilterSign::Equal => "==", - FilterSign::Unequal => "!=", - FilterSign::Less => "<", - FilterSign::Greater => ">", - FilterSign::LeOrEq => "<=", - FilterSign::GrOrEq => ">=", - FilterSign::Regex => "~=", - FilterSign::In => "in", - FilterSign::Nin => "nin", - FilterSign::Size => "size", - FilterSign::NoneOf => "noneOf", - FilterSign::AnyOf => "anyOf", - FilterSign::SubSetOf => "subsetOf", - FilterSign::Exists => "exists", - }; - write!(f, "{}", str) + match self { + SingularQuerySegment::Index(index) => write!(f, "{}", index), + SingularQuerySegment::Name(name) => write!(f, "{}", name), + } } } -impl FilterSign { - pub fn new(key: &str) -> Self { - match key { - "==" => FilterSign::Equal, - "!=" => FilterSign::Unequal, - "<" => FilterSign::Less, - ">" => FilterSign::Greater, - "<=" => FilterSign::LeOrEq, - ">=" => FilterSign::GrOrEq, - "~=" => FilterSign::Regex, - "in" => FilterSign::In, - "nin" => FilterSign::Nin, - "size" => FilterSign::Size, - "noneOf" => FilterSign::NoneOf, - "anyOf" => FilterSign::AnyOf, - "subsetOf" => FilterSign::SubSetOf, - _ => FilterSign::Exists, +/// Enum representing different types of tests in a JSONPath query. +#[derive(Debug, Clone, PartialEq)] +pub enum Test { + /// Represents a relative query. + RelQuery(Vec), + /// Represents an absolute query. + AbsQuery(JpQuery), + /// Represents a function test. + Function(Box), +} + +impl Test { + pub fn is_res_bool(&self) -> bool { + match self { + Test::RelQuery(_) => false, + Test::AbsQuery(_) => false, + Test::Function(func) => match **func { + TestFunction::Length(_) => false, + TestFunction::Value(_) => false, + TestFunction::Count(_) => false, + TestFunction::Custom(_, _) => true, + TestFunction::Search(_, _) => true, + TestFunction::Match(_, _) => true, + }, } } } -impl PartialEq for JsonPath -where - T: PartialEq, -{ - fn eq(&self, other: &Self) -> bool { - match (self, other) { - (JsonPath::Root, JsonPath::Root) => true, - (JsonPath::Descent(k1), JsonPath::Descent(k2)) => k1 == k2, - (JsonPath::DescentW, JsonPath::DescentW) => true, - (JsonPath::Field(k1), JsonPath::Field(k2)) => k1 == k2, - (JsonPath::Wildcard, JsonPath::Wildcard) => true, - (JsonPath::Empty, JsonPath::Empty) => true, - (JsonPath::Current(jp1), JsonPath::Current(jp2)) => jp1 == jp2, - (JsonPath::Chain(ch1), JsonPath::Chain(ch2)) => ch1 == ch2, - (JsonPath::Index(idx1), JsonPath::Index(idx2)) => idx1 == idx2, - (JsonPath::Fn(fn1), JsonPath::Fn(fn2)) => fn2 == fn1, - (_, _) => false, +impl Display for Test { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Test::RelQuery(segments) => write!( + f, + "{}", + segments.iter().map(|s| s.to_string()).collect::() + ), + Test::AbsQuery(query) => write!(f, "{}", query), + Test::Function(func) => write!(f, "{}", func), } } } -impl PartialEq for JsonPathIndex -where - T: PartialEq, -{ - fn eq(&self, other: &Self) -> bool { - match (self, other) { - (JsonPathIndex::Slice(s1, e1, st1), JsonPathIndex::Slice(s2, e2, st2)) => { - s1 == s2 && e1 == e2 && st1 == st2 - } - (JsonPathIndex::Single(el1), JsonPathIndex::Single(el2)) => el1 == el2, - (JsonPathIndex::UnionIndex(elems1), JsonPathIndex::UnionIndex(elems2)) => { - elems1 == elems2 +/// Enum representing different types of test functions in a JSONPath query. +#[derive(Debug, Clone, PartialEq)] +pub enum TestFunction { + /// Represents a custom function. + Custom(String, Vec), + /// Represents a length function. + Length(Box), + /// Represents a value function. + Value(FnArg), + /// Represents a count function. + Count(FnArg), + /// Represents a search function. + Search(FnArg, FnArg), + /// Represents a match function. + Match(FnArg, FnArg), +} + +impl TestFunction { + pub fn try_new(name: &str, args: Vec) -> Parsed { + fn with_node_type_validation<'a>( + a: &'a FnArg, + name: &str, + ) -> Result<&'a FnArg, JsonPathError> { + if a.is_lit() { + Err(JsonPathError::InvalidJsonPath(format!( + "Invalid argument for the function `{}`: expected a node, got a literal", + name + ))) + } else if a.is_filter() { + Err(JsonPathError::InvalidJsonPath(format!( + "Invalid argument for the function `{}`: expected a node, got a filter", + name + ))) + } else { + Ok(a) } - (JsonPathIndex::UnionKeys(elems1), JsonPathIndex::UnionKeys(elems2)) => { - elems1 == elems2 + } + + match (name, args.as_slice()) { + ("length", [a]) => Ok(TestFunction::Length(Box::new(a.clone()))), + ("value", [a]) => Ok(TestFunction::Value(a.clone())), + ("count", [a]) => Ok(TestFunction::Count( + with_node_type_validation(a, name)?.clone(), + )), + ("search", [a, b]) => Ok(TestFunction::Search(a.clone(), b.clone())), + ("match", [a, b]) => Ok(TestFunction::Match(a.clone(), b.clone())), + ("length" | "value" | "count" | "match" | "search", args) => { + Err(JsonPathError::InvalidJsonPath(format!( + "Invalid number of arguments for the function `{}`: got {}", + name, + args.len() + ))) } - (JsonPathIndex::Filter(left), JsonPathIndex::Filter(right)) => left.eq(right), - (_, _) => false, + (custom, _) => Ok(TestFunction::Custom(custom.to_string(), args)), + } + } + + pub fn is_comparable(&self) -> bool { + match self { + TestFunction::Length(_) => true, + TestFunction::Value(_) => true, + TestFunction::Count(_) => true, + TestFunction::Custom(_, _) => false, + TestFunction::Search(_, _) => false, + TestFunction::Match(_, _) => false, } } } -impl PartialEq for Operand -where - T: PartialEq, -{ - fn eq(&self, other: &Self) -> bool { - match (self, other) { - (Operand::Static(v1), Operand::Static(v2)) => v1 == v2, - (Operand::Dynamic(jp1), Operand::Dynamic(jp2)) => jp1 == jp2, - (_, _) => false, +impl Display for TestFunction { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + TestFunction::Custom(name, args) => write!( + f, + "{}({})", + name, + args.iter().map(|a| a.to_string()).collect::() + ), + TestFunction::Length(arg) => write!(f, "length({})", arg), + TestFunction::Value(arg) => write!(f, "value({})", arg), + TestFunction::Count(arg) => write!(f, "count({})", arg), + TestFunction::Search(arg1, arg2) => write!(f, "search({}, {})", arg1, arg2), + TestFunction::Match(arg1, arg2) => write!(f, "match({}, {})", arg1, arg2), + } + } +} + +/// Enum representing different types of function arguments in a JSONPath query. +#[derive(Debug, Clone, PartialEq)] +pub enum FnArg { + /// Represents a literal argument. + Literal(Literal), + /// Represents a test argument. + Test(Box), + /// Represents a filter argument. + Filter(Filter), +} + +impl FnArg { + pub fn is_lit(&self) -> bool { + matches!(self, FnArg::Literal(_)) + } + pub fn is_filter(&self) -> bool { + matches!(self, FnArg::Filter(_)) + } +} + +impl Display for FnArg { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + FnArg::Literal(literal) => write!(f, "{}", literal), + FnArg::Test(test) => write!(f, "{}", test), + FnArg::Filter(filter) => write!(f, "{}", filter), + } + } +} + +/// Enum representing different types of literal values in a JSONPath query. +#[derive(Debug, Clone, PartialEq)] +pub enum Literal { + /// Represents an integer literal. + Int(i64), + /// Represents a floating-point literal. + Float(f64), + /// Represents a string literal. + String(String), + /// Represents a boolean literal. + Bool(bool), + /// Represents a null literal. + Null, +} + +impl Display for Literal { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Literal::Int(val) => write!(f, "{}", val), + Literal::Float(val) => write!(f, "{}", val), + Literal::String(val) => write!(f, "\"{}\"", val), + Literal::Bool(val) => write!(f, "{}", val), + Literal::Null => write!(f, "null"), } } } diff --git a/src/parser/parser.rs b/src/parser/parser.rs deleted file mode 100644 index 355f619..0000000 --- a/src/parser/parser.rs +++ /dev/null @@ -1,624 +0,0 @@ -#![allow(clippy::empty_docs)] - -use crate::parser::errors::JsonPathParserError; -use crate::parser::model::FilterExpression::{And, Not, Or}; -use crate::parser::model::{ - FilterExpression, FilterSign, Function, JsonPath, JsonPathIndex, Operand, -}; -use crate::path::JsonLike; -use pest::iterators::{Pair, Pairs}; -use pest::Parser; - -#[derive(Parser)] -#[grammar = "parser/grammar/json_path.pest"] -struct JsonPathParser; - -/// Parses a string into a [JsonPath]. -/// -/// # Errors -/// -/// Returns a variant of [JsonPathParserError] if the parsing operation failed. -pub fn parse_json_path(jp_str: &str) -> Result, JsonPathParserError> -where - T: JsonLike, -{ - JsonPathParser::parse(Rule::path, jp_str) - .map_err(Box::new)? - .next() - .ok_or(JsonPathParserError::UnexpectedPestOutput) - .and_then(parse_internal) -} - -/// Internal function takes care of the logic by parsing the operators and unrolling the string into the final result. -/// -/// # Errors -/// -/// Returns a variant of [JsonPathParserError] if the parsing operation failed -fn parse_internal(rule: Pair<'_, Rule>) -> Result, JsonPathParserError> -where - T: JsonLike, -{ - match rule.as_rule() { - Rule::path => rule - .into_inner() - .next() - .ok_or(JsonPathParserError::NoRulePath) - .and_then(parse_internal), - Rule::current => rule - .into_inner() - .next() - .map(parse_internal) - .unwrap_or(Ok(JsonPath::Empty)) - .map(Box::new) - .map(JsonPath::Current), - Rule::chain => rule - .into_inner() - .map(parse_internal) - .collect::, _>>() - .map(JsonPath::Chain), - Rule::root => Ok(JsonPath::Root), - Rule::wildcard => Ok(JsonPath::Wildcard), - Rule::descent => parse_key(down(rule)?)? - .map(JsonPath::Descent) - .ok_or(JsonPathParserError::NoJsonPathDescent), - Rule::descent_w => Ok(JsonPath::DescentW), - Rule::function => Ok(JsonPath::Fn(Function::Length)), - Rule::field => parse_key(down(rule)?)? - .map(JsonPath::Field) - .ok_or(JsonPathParserError::NoJsonPathField), - Rule::index => parse_index(rule).map(JsonPath::Index), - rule => Err(JsonPathParserError::InvalidTopLevelRule(rule)), - } -} - -/// parsing the rule 'key' with the structures either .key or .\['key'\] -fn parse_key(rule: Pair) -> Result, JsonPathParserError> { - let parsed_key = match rule.as_rule() { - Rule::key | Rule::key_unlim | Rule::string_qt => parse_key(down(rule)?), - Rule::key_lim | Rule::inner => Ok(Some(String::from(rule.as_str()))), - _ => Ok(None), - }; - parsed_key -} - -fn parse_slice(pairs: Pairs) -> Result, JsonPathParserError> { - let mut start = 0; - let mut end = 0; - let mut step = 1; - for in_pair in pairs { - match in_pair.as_rule() { - Rule::start_slice => start = in_pair.as_str().parse::().unwrap_or(start), - Rule::end_slice => end = in_pair.as_str().parse::().unwrap_or(end), - Rule::step_slice => step = down(in_pair)?.as_str().parse::().unwrap_or(step), - _ => (), - } - } - Ok(JsonPathIndex::Slice(start, end, step)) -} - -fn parse_unit_keys(pairs: Pairs) -> Result, JsonPathParserError> { - let mut keys = vec![]; - - for pair in pairs { - keys.push(String::from(down(pair)?.as_str())); - } - Ok(JsonPathIndex::UnionKeys(keys)) -} - -fn number_to_value(number: &str) -> Result -where - T: From + From, -{ - match number - .parse::() - .ok() - .map(T::from) - .or_else(|| number.parse::().ok().map(T::from)) - { - Some(value) => Ok(value), - None => Err(JsonPathParserError::InvalidNumber(number.to_string())), - } -} - -fn bool_to_value(boolean: &str) -> T -where - T: From, -{ - boolean - .parse::() - .map(T::from) - .expect("unreachable: according to .pest this is either `true` or `false`") -} - -fn parse_unit_indexes(pairs: Pairs) -> Result, JsonPathParserError> -where - T: From + From, -{ - let mut keys = vec![]; - - for pair in pairs { - keys.push(number_to_value(pair.as_str())?); - } - Ok(JsonPathIndex::UnionIndex(keys)) -} - -fn parse_chain_in_operand(rule: Pair<'_, Rule>) -> Result, JsonPathParserError> -where - T: JsonLike, -{ - let parsed_chain = match parse_internal::(rule)? { - JsonPath::Chain(elems) => { - if elems.len() == 1 { - match elems.first() { - Some(JsonPath::Index(JsonPathIndex::UnionKeys(keys))) => { - Operand::val(T::from(keys.clone())) - } - Some(JsonPath::Index(JsonPathIndex::UnionIndex(keys))) => { - Operand::val(T::from(keys.clone())) - } - Some(JsonPath::Field(f)) => Operand::val(T::from(vec![f.to_string()])), - _ => Operand::Dynamic(Box::new(JsonPath::Chain(elems))), - } - } else { - Operand::Dynamic(Box::new(JsonPath::Chain(elems))) - } - } - jp => Operand::Dynamic(Box::new(jp)), - }; - Ok(parsed_chain) -} - -fn parse_filter_index(pair: Pair<'_, Rule>) -> Result, JsonPathParserError> -where - T: JsonLike, -{ - Ok(JsonPathIndex::Filter(parse_logic_or(pair.into_inner())?)) -} - -fn parse_logic_or(pairs: Pairs<'_, Rule>) -> Result, JsonPathParserError> -where - T: JsonLike, -{ - let mut expr: Option> = None; - // only possible for the loop not to produce any value (except Errors) - if pairs.len() == 0 { - return Err(JsonPathParserError::UnexpectedNoneLogicError( - pairs.get_input().to_string(), - pairs.as_str().to_string(), - )); - } - for pair in pairs.into_iter() { - let next_expr = parse_logic_and(pair.into_inner())?; - match expr { - None => expr = Some(next_expr), - Some(e) => expr = Some(Or(Box::new(e), Box::new(next_expr))), - } - } - Ok(expr.expect("unreachable: above len() == 0 check should have catched this")) -} - -fn parse_logic_and(pairs: Pairs<'_, Rule>) -> Result, JsonPathParserError> -where - T: JsonLike, -{ - let mut expr: Option> = None; - // only possible for the loop not to produce any value (except Errors) - if pairs.len() == 0 { - return Err(JsonPathParserError::UnexpectedNoneLogicError( - pairs.get_input().to_string(), - pairs.as_str().to_string(), - )); - } - for pair in pairs { - let next_expr = parse_logic_not(pair.into_inner())?; - match expr { - None => expr = Some(next_expr), - Some(e) => expr = Some(And(Box::new(e), Box::new(next_expr))), - } - } - Ok(expr.expect("unreachable: above len() == 0 check should have catched this")) -} - -fn parse_logic_not( - mut pairs: Pairs<'_, Rule>, -) -> Result, JsonPathParserError> -where - T: JsonLike, -{ - if let Some(rule) = pairs.peek().map(|x| x.as_rule()) { - match rule { - Rule::not => { - pairs.next().expect("unreachable in arithmetic: should have a value as pairs.peek() was Some(_)"); - parse_logic_not(pairs) - .map(|expr|Not(Box::new(expr))) - }, - Rule::logic_atom => parse_logic_atom(pairs.next().expect("unreachable in arithmetic: should have a value as pairs.peek() was Some(_)").into_inner()), - rule => Err(JsonPathParserError::UnexpectedRuleLogicError(rule, pairs.get_input().to_string(), pairs.as_str().to_string())), - } - } else { - Err(JsonPathParserError::UnexpectedNoneLogicError( - pairs.get_input().to_string(), - pairs.as_str().to_string(), - )) - } -} - -fn parse_logic_atom( - mut pairs: Pairs<'_, Rule>, -) -> Result, JsonPathParserError> -where - T: JsonLike, -{ - if let Some(rule) = pairs.peek().map(|x| x.as_rule()) { - match rule { - Rule::logic_or => parse_logic_or(pairs.next().expect("unreachable in arithmetic: should have a value as pairs.peek() was Some(_)").into_inner()), - Rule::atom => { - let left: Operand = parse_atom(pairs.next().unwrap())?; - if pairs.peek().is_none() { - Ok(FilterExpression::exists(left)) - } else { - let sign: FilterSign = FilterSign::new(pairs.next().expect("unreachable in arithmetic: should have a value as pairs.peek() was Some(_)").as_str()); - let right: Operand = - parse_atom(pairs.next().expect("unreachable in arithemetic: should have a right side operand"))?; - Ok(FilterExpression::Atom(left, sign, right)) - } - } - rule => Err(JsonPathParserError::UnexpectedRuleLogicError(rule, pairs.get_input().to_string(), pairs.as_str().to_string())), - } - } else { - Err(JsonPathParserError::UnexpectedNoneLogicError( - pairs.get_input().to_string(), - pairs.as_str().to_string(), - )) - } -} - -fn parse_atom(rule: Pair<'_, Rule>) -> Result, JsonPathParserError> -where - T: JsonLike, -{ - let atom = down(rule.clone())?; - let parsed_atom = match atom.as_rule() { - Rule::number => Operand::Static(number_to_value(rule.as_str())?), - Rule::string_qt => Operand::Static(T::from(down(atom)?.as_str())), - Rule::chain => parse_chain_in_operand(down(rule)?)?, - Rule::boolean => Operand::Static(bool_to_value(rule.as_str())), - _ => Operand::Static(T::null()), - }; - Ok(parsed_atom) -} - -fn parse_index(rule: Pair<'_, Rule>) -> Result, JsonPathParserError> -where - T: JsonLike, -{ - let next = down(rule)?; - let parsed_index = match next.as_rule() { - Rule::unsigned => JsonPathIndex::Single(number_to_value(next.as_str())?), - Rule::slice => parse_slice(next.into_inner())?, - Rule::unit_indexes => parse_unit_indexes(next.into_inner())?, - Rule::unit_keys => parse_unit_keys(next.into_inner())?, - Rule::filter => parse_filter_index(down(next)?)?, - _ => JsonPathIndex::Single(number_to_value(next.as_str())?), - }; - Ok(parsed_index) -} - -fn down(rule: Pair) -> Result, JsonPathParserError> { - let error_message = rule.to_string(); - match rule.into_inner().next() { - Some(rule) => Ok(rule), - None => Err(JsonPathParserError::EmptyInner(error_message)), - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::parser::macros::{chain, filter, idx, op}; - use crate::path; - use serde_json::{json, Value}; - use std::panic; - - fn test_failed(input: &str) { - match parse_json_path::(input) { - Ok(elem) => panic!("should be false but got {:?}", elem), - Err(e) => println!("{}", e), - } - } - - fn test(input: &str, expected: Vec>) - where - T: JsonLike, - { - match parse_json_path::(input) { - Ok(JsonPath::Chain(elems)) => assert_eq!(elems, expected), - Ok(e) => panic!("unexpected value {:?}", e), - Err(e) => { - panic!("parsing error {}", e); - } - } - } - - #[test] - fn path_test() { - test::("$.k.['k']['k']..k..['k'].*.[*][*][1][1,2]['k','k'][:][10:][:10][10:10:10][?(@)][?(@.abc >= 10)]", - vec![ - path!($), - path!("k"), - path!("k"), - path!("k"), - path!(.."k"), - path!(.."k"), - path!(*), - path!(*), - path!(*), - path!(idx!(1)), - path!(idx!(idx 1,2)), - path!(idx!("k","k")), - path!(idx!([; ;])), - path!(idx!([10; ;])), - path!(idx!([;10;])), - path!(idx!([10;10;10])), - path!(idx!(?filter!(op!(chain!(path!(@path!()))), "exists", op!(path!())))), - path!(idx!(?filter!(op!(chain!(path!(@,path!("abc")))), ">=", op!(10)))), - ]); - test::( - "$..*[?(@.isbn)].title", - vec![ - // Root, DescentW, Index(Filter(Atom(Dynamic(Chain([Current(Chain([Field("isbn")]))])), Exists, Dynamic(Empty)))), Field("title") - path!($), - path!(..*), - path!(idx!(?filter!(op!(chain!(path!(@,path!("isbn")))), "exists", op!(path!())))), - path!("title"), - ], - ) - } - - #[test] - fn descent_test() { - test::("..abc", vec![path!(.."abc")]); - test::("..['abc']", vec![path!(.."abc")]); - test_failed("...['abc']"); - test_failed("...abc"); - } - - #[test] - fn field_test() { - test::(".abc", vec![path!("abc")]); - test::(".['abc']", vec![path!("abc")]); - test::("['abc']", vec![path!("abc")]); - test::(".['abc\\\"abc']", vec![path!("abc\\\"abc")]); - test_failed(".abc()abc"); - test_failed("..[abc]"); - test_failed(".'abc'"); - } - - #[test] - fn wildcard_test() { - test::(".*", vec![path!(*)]); - test::(".[*]", vec![path!(*)]); - test::(".abc.*", vec![path!("abc"), path!(*)]); - test::(".abc.[*]", vec![path!("abc"), path!(*)]); - test::(".abc[*]", vec![path!("abc"), path!(*)]); - test::("..*", vec![path!(..*)]); - test_failed("abc*"); - } - - #[test] - fn index_single_test() { - test::("[1]", vec![path!(idx!(1))]); - test_failed("[-1]"); - test_failed("[1a]"); - } - - #[test] - fn index_slice_test() { - test::("[1:1000:10]", vec![path!(idx!([1; 1000; 10]))]); - test::("[:1000:10]", vec![path!(idx!([0; 1000; 10]))]); - test::("[:1000]", vec![path!(idx!([;1000;]))]); - test::("[:]", vec![path!(idx!([;;]))]); - test::("[::10]", vec![path!(idx!([;;10]))]); - test_failed("[::-1]"); - test_failed("[:::0]"); - } - - #[test] - fn index_union_test() { - test::("[1,2,3]", vec![path!(idx!(idx 1,2,3))]); - test::("['abc','bcd']", vec![path!(idx!("abc", "bcd"))]); - test_failed("[]"); - test::("[-1,-2]", vec![path!(idx!(idx - 1, -2))]); - test_failed("[abc,bcd]"); - test::("[\"abc\",\"bcd\"]", vec![path!(idx!("abc", "bcd"))]); - } - - #[test] - fn array_start_test() { - test::( - "$.[?(@.verb== \"TEST\")]", - vec![ - path!($), - path!(idx!(?filter!(op!(chain!(path!(@,path!("verb")))),"==",op!("TEST")))), - ], - ); - } - - #[test] - fn logical_filter_test() { - test::( - "$.[?(@.verb == 'T' || @.size > 0 && @.size < 10)]", - vec![ - path!($), - path!(idx!(? - filter!( - filter!(op!(chain!(path!(@,path!("verb")))), "==", op!("T")), - ||, - filter!( - filter!(op!(chain!(path!(@,path!("size")))), ">", op!(0)), - &&, - filter!(op!(chain!(path!(@,path!("size")))), "<", op!(10)) - ) - ))), - ], - ); - test::( - "$.[?((@.verb == 'T' || @.size > 0) && @.size < 10)]", - vec![ - path!($), - path!(idx!(? - filter!( - filter!( - filter!(op!(chain!(path!(@,path!("verb")))), "==", op!("T")), - ||, - filter!(op!(chain!(path!(@,path!("size")))), ">", op!(0)) - ), - &&, - filter!(op!(chain!(path!(@,path!("size")))), "<", op!(10)) - ))), - ], - ); - test::( - "$.[?(@.verb == 'T' || @.size > 0 && @.size < 10 && @.elem == 0)]", - vec![ - path!($), - path!(idx!(?filter!( - filter!(op!(chain!(path!(@,path!("verb")))), "==", op!("T")), - ||, - filter!( - filter!( - filter!(op!(chain!(path!(@,path!("size")))), ">", op!(0)), - &&, - filter!(op!(chain!(path!(@,path!("size")))), "<", op!(10)) - ), - &&, - filter!(op!(chain!(path!(@,path!("elem")))), "==", op!(0)) - ) - - ))), - ], - ); - } - - #[test] - fn index_filter_test() { - test::( - "[?('abc' == 'abc')]", - vec![path!(idx!(?filter!(op!("abc"),"==",op!("abc") )))], - ); - test::( - "[?('abc' == 1)]", - vec![path!(idx!(?filter!( op!("abc"),"==",op!(1))))], - ); - test::( - "[?('abc' == true)]", - vec![path!(idx!(?filter!( op!("abc"),"==",op!(true))))], - ); - test::( - "[?('abc' == null)]", - vec![path!( - idx!(?filter!( op!("abc"),"==",Operand::Static(Value::Null))) - )], - ); - - test::( - "[?(@.abc in ['abc','bcd'])]", - vec![path!( - idx!(?filter!(op!(chain!(path!(@,path!("abc")))),"in",Operand::val(json!(["abc","bcd"])))) - )], - ); - - test::( - "[?(@.abc.[*] in ['abc','bcd'])]", - vec![path!(idx!(?filter!( - op!(chain!(path!(@,path!("abc"), path!(*)))), - "in", - op!(s json!(["abc","bcd"])) - )))], - ); - test::( - "[?(@.[*]..next in ['abc','bcd'])]", - vec![path!(idx!(?filter!( - op!(chain!(path!(@,path!(*), path!(.."next")))), - "in", - op!(s json!(["abc","bcd"])) - )))], - ); - - test::( - "[?(@[1] in ['abc','bcd'])]", - vec![path!(idx!(?filter!( - op!(chain!(path!(@,path!(idx!(1))))), - "in", - op!(s json!(["abc","bcd"])) - )))], - ); - test::( - "[?(@ == 'abc')]", - vec![path!(idx!(?filter!( - op!(chain!(path!(@path!()))),"==",op!("abc") - )))], - ); - test::( - "[?(@ subsetOf ['abc'])]", - vec![path!(idx!(?filter!( - op!(chain!(path!(@path!()))),"subsetOf",op!(s json!(["abc"])) - )))], - ); - test::( - "[?(@[1] subsetOf ['abc','abc'])]", - vec![path!(idx!(?filter!( - op!(chain!(path!(@,path!(idx!(1))))), - "subsetOf", - op!(s json!(["abc","abc"])) - )))], - ); - test::( - "[?(@ subsetOf [1,2,3])]", - vec![path!(idx!(?filter!( - op!(chain!(path!(@path!()))),"subsetOf",op!(s json!([1,2,3])) - )))], - ); - - test_failed("[?(@[1] subsetof ['abc','abc'])]"); - test_failed("[?(@ >< ['abc','abc'])]"); - test_failed("[?(@ in {\"abc\":1})]"); - } - - #[test] - fn fn_size_test() { - test::( - "$.k.length()", - vec![path!($), path!("k"), JsonPath::Fn(Function::Length)], - ); - - test::( - "$.k.length.field", - vec![path!($), path!("k"), path!("length"), path!("field")], - ) - } - - #[test] - fn parser_error_test_invalid_rule() { - let result = parse_json_path::("notapath"); - - assert!(result.is_err()); - assert!(result - .err() - .unwrap() - .to_string() - .starts_with("Failed to parse rule")); - } - - #[test] - fn parser_error_test_empty_rule() { - let result = parse_json_path::(""); - - assert!(result.is_err()); - assert!(result - .err() - .unwrap() - .to_string() - .starts_with("Failed to parse rule")); - } -} diff --git a/src/parser/tests.rs b/src/parser/tests.rs new file mode 100644 index 0000000..95f16df --- /dev/null +++ b/src/parser/tests.rs @@ -0,0 +1,264 @@ +use crate::parser::model::slice_from; +use crate::parser::model::Comparison; +use crate::parser::model::FilterAtom; +use crate::parser::model::FnArg; +use crate::parser::model::JpQuery; +use crate::parser::model::Literal; +use crate::parser::model::Segment; +use crate::parser::model::Selector; +use crate::parser::model::SingularQuery; +use crate::parser::model::SingularQuerySegment; +use crate::parser::model::TestFunction; +use crate::parser::model::{Comparable, Filter}; +use crate::parser::Test; +use crate::parser::{ + comp_expr, comparable, filter_atom, function_expr, jp_query, literal, parse_json_path, segment, + selector, singular_query, singular_query_segments, slice_selector, test, JSPathParser, Parsed, + Rule, +}; +use crate::{ + arg, atom, cmp, comparable, jq, lit, or, q_segment, q_segments, segment, selector, + singular_query, slice, test, test_fn, +}; +use pest::error::Error; +use pest::iterators::Pair; +use pest::Parser; +use std::fmt::Debug; +use std::{panic, vec}; + +struct TestPair { + rule: Rule, + parse_fn: fn(Pair) -> Parsed, +} + +impl TestPair { + fn new(rule: Rule, parse_fn: fn(Pair) -> Parsed) -> Self { + Self { rule, parse_fn } + } + fn assert(self, input: &str, expected: T) -> Self { + match parse(input, self.rule) { + Ok(e) => { + assert((self.parse_fn)(e), expected); + } + Err(e) => { + panic!("parsing error `{}`", e); + } + } + self + } + fn assert_fail(self, input: &str) -> Self { + match parse(input, self.rule) { + Ok(e) => { + if let Ok(r) = (self.parse_fn)(e) { + panic!("expected error, got {:?}", r); + } + } + Err(e) => { + println!("parsing error `{}`", e); + } + } + self + } +} + +fn parse(input: &str, rule: Rule) -> Result, Error> { + match JSPathParser::parse(rule, input) { + Ok(e) => Ok(e.into_iter().next().expect("no pairs found")), + Err(e) => Err(e), + } +} + +fn assert(result: Parsed, expected: T) +where + T: PartialEq + Debug, +{ + match result { + Ok(e) => assert_eq!(e, expected), + Err(e) => { + panic!("parsing error `{}`", e); + } + } +} + +#[test] +fn singular_query_segment_test() { + TestPair::new(Rule::singular_query_segments, singular_query_segments) + .assert("[\"b\"][\"b\"]", q_segments!([b][b])) + .assert("[2][1]", q_segments!([2][1])) + .assert("[2][\"a\"]", q_segments!([2][a])) + .assert(".a.b", q_segments!(a b)) + .assert(".a.b[\"c\"][1]", q_segments!(a b [c][1])); +} +#[test] +fn singular_query_test() { + TestPair::new(Rule::singular_query, singular_query) + .assert("@.a.b", singular_query!(@ a b)) + .assert("@", SingularQuery::Current(vec![])) + .assert("$", SingularQuery::Root(vec![])) + .assert("$.a.b.c", singular_query!(a b c)) + .assert("$[\"a\"].b[3]", singular_query!([a] b [3])); +} + +#[test] +fn slice_selector_test() { + TestPair::new(Rule::slice_selector, slice_selector) + .assert(":", slice!()) + .assert("::", slice!()) + .assert("1:", slice!(1)) + .assert("1:1", slice!(1, 1)) + .assert("1:1:1", slice!(1, 1, 1)) + .assert(":1:1", slice!(,1,1)) + .assert("::1", slice!(,,1)) + .assert("1::1", slice!(1,,1)) + .assert_fail("-0:") + .assert_fail("9007199254740995"); +} + +#[test] +fn function_expr_test() { + TestPair::new(Rule::function_expr, function_expr) + .assert("length(1)", test_fn!(length arg!(lit!(i 1)))) + .assert("length(true)", test_fn!(length arg!(lit!(b true)))) + .assert( + "search(@, \"abc\")", + test_fn!(search arg!(t test!(@ ) ), arg!(lit!(s "abc"))), + ) + .assert( + "count(@.a)", + test_fn!(count arg!(t test!(@ segment!(selector!(a))))), + ) + .assert_fail("count\t(@.*)"); +} + +#[test] +fn jq_test() { + let atom = Filter::Atom(atom!( + comparable!(> singular_query!(@ a b)), + ">", + comparable!(lit!(i 1)) + )); + TestPair::new(Rule::jp_query, jp_query).assert( + "$.a.b[?@.a.b > 1]", + jq!( + segment!(selector!(a)), + segment!(selector!(b)), + segment!(selector!(?atom)) + ), + ); +} + +#[test] +fn comp_expr_test() { + TestPair::new(Rule::comp_expr, comp_expr).assert( + "@.a.b.c == 1", + cmp!( + comparable!(> singular_query!(@ a b c)), + "==", + comparable!(lit!(i 1)) + ), + ); +} + +#[test] +fn literal_test() { + TestPair::new(Rule::literal, literal) + .assert("'☺'", lit!(s "☺")) + .assert_fail("\"\n\"") + .assert("' '", lit!(s " ")) + .assert("'\"'", lit!(s "\"")) + .assert("null", lit!()) + .assert("false", lit!(b false)) + .assert("true", lit!(b true)) + .assert("\"hello\"", lit!(s "hello")) + .assert("\'hello\'", lit!(s "hello")) + .assert("\'hel\\'lo\'", lit!(s "hel\\'lo")) + .assert("\'hel\"lo\'", lit!(s "hel\"lo")) + .assert("\'hel\\nlo\'", lit!(s "hel\\nlo")) + .assert("1", lit!(i 1)) + .assert("0", lit!(i 0)) + .assert("-0", lit!(i 0)) + .assert("1.2", lit!(f 1.2)) + .assert("9007199254740990", lit!(i 9007199254740990)) + .assert_fail("hel\\\"lo") + .assert_fail("9007199254740995"); +} + +#[test] +fn filter_atom_test() { + TestPair::new(Rule::atom_expr, filter_atom) + .assert( + "1 > 2", + atom!(comparable!(lit!(i 1)), ">", comparable!(lit!(i 2))), + ) + .assert( + "!(@.a ==1 || @.b == 2)", + atom!(!or!( + Filter::Atom(atom!( + comparable!(> singular_query!(@ a)), + "==", + comparable!(lit!(i 1)) + )), + Filter::Atom(atom!( + comparable!(> singular_query!(@ b)), + "==", + comparable!(lit!(i 2)) + )) + )), + ); +} +#[test] +fn comparable_test() { + TestPair::new(Rule::comparable, comparable) + .assert("1", comparable!(lit!(i 1))) + .assert("\"a\"", comparable!(lit!(s "a"))) + .assert("@.a.b.c", comparable!(> singular_query!(@ a b c))) + .assert("$.a.b.c", comparable!(> singular_query!(a b c))) + .assert("$[1]", comparable!(> singular_query!([1]))) + .assert("length(1)", comparable!(f test_fn!(length arg!(lit!(i 1))))); +} + +#[test] +fn parse_path() { + let result = parse_json_path("$"); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), JpQuery::new(vec![])); +} + +#[test] +fn parse_i64() { + TestPair::new(Rule::literal, literal).assert("1e2", lit!(f 100.0)); +} +#[test] +fn parse_selector() { + TestPair::new(Rule::selector, selector).assert("1:1", Selector::Slice(Some(1), Some(1), None)); +} +#[test] +fn parse_global() { + let sel_a = segment!(selector!(a)); + TestPair::new(Rule::jp_query, jp_query) + // .assert("$", JpQuery::new(vec![])) + // .assert("$.a", JpQuery::new(vec![sel_a.clone()])) + // .assert("$..a", JpQuery::new(vec![segment!(..sel_a)])) + // .assert( + // "$..*", + // JpQuery::new(vec![segment!(..segment!(selector!(*)))]), + // ) + // .assert( + // "$[1 :5:2]", + // JpQuery::new(vec![segment!(selector!(slice slice!(1, 5, 2)))]), + // ) + // .assert( + // "$['a']['b']", + // JpQuery::new(vec![segment!(Selector::Name("'a'".to_string())), segment!(Selector::Name("'b'".to_string()))]), + // ) + // + // .assert( + // "$[1, 1:1]", + // JpQuery::new(vec![Segment::Selectors(vec![ + // Selector::Index(1), + // Selector::Slice(Some(1), Some(1), None), + // ])]), + // ) + // .assert_fail("$..\ra") + ; +} diff --git a/src/path/index.rs b/src/path/index.rs deleted file mode 100644 index bb33ce8..0000000 --- a/src/path/index.rs +++ /dev/null @@ -1,907 +0,0 @@ -use std::fmt::Debug; - -use crate::jsp_idx; -use crate::parser::model::{FilterExpression, FilterSign, JsonPath}; - -use crate::path::top::ObjectField; -use crate::path::{json_path_instance, process_operand, JsonPathValue, Path, PathInstance}; -use crate::JsonPathValue::{NoValue, Slice}; - -use super::{JsonLike, TopPaths}; - -/// process the slice like [start:end:step] -#[derive(Debug)] -pub struct ArraySlice { - start_index: i32, - end_index: i32, - step: usize, - _t: std::marker::PhantomData, -} - -impl ArraySlice { - pub(crate) fn new(start_index: i32, end_index: i32, step: usize) -> Self { - ArraySlice { - start_index, - end_index, - step, - _t: std::marker::PhantomData, - } - } - - fn end(&self, len: i32) -> Option { - if self.end_index >= 0 { - if self.end_index > len { - None - } else { - Some(self.end_index as usize) - } - } else if self.end_index < -len { - None - } else { - Some((len - (-self.end_index)) as usize) - } - } - - fn start(&self, len: i32) -> Option { - if self.start_index >= 0 { - if self.start_index > len { - None - } else { - Some(self.start_index as usize) - } - } else if self.start_index < -len { - None - } else { - Some((len - -self.start_index) as usize) - } - } - - fn process<'a, F>(&self, elements: &'a [F]) -> Vec<(&'a F, usize)> { - let len = elements.len() as i32; - let mut filtered_elems: Vec<(&'a F, usize)> = vec![]; - match (self.start(len), self.end(len)) { - (Some(start_idx), Some(end_idx)) => { - let end_idx = if end_idx == 0 { - elements.len() - } else { - end_idx - }; - for idx in (start_idx..end_idx).step_by(self.step) { - if let Some(v) = elements.get(idx) { - filtered_elems.push((v, idx)) - } - } - filtered_elems - } - _ => filtered_elems, - } - } -} - -impl<'a, T> Path<'a> for ArraySlice -where - T: JsonLike, -{ - type Data = T; - - fn find(&self, input: JsonPathValue<'a, Self::Data>) -> Vec> { - input.flat_map_slice(|data, pref| { - data.as_array() - .map(|elems| self.process(elems)) - .and_then(|v| { - if v.is_empty() { - None - } else { - let v = v.into_iter().map(|(e, i)| (e, jsp_idx(&pref, i))).collect(); - Some(JsonPathValue::map_vec(v)) - } - }) - .unwrap_or_else(|| vec![NoValue]) - }) - } -} - -/// process the simple index like [index] -pub struct ArrayIndex { - index: usize, - _t: std::marker::PhantomData, -} - -impl ArrayIndex { - pub(crate) fn new(index: usize) -> Self { - ArrayIndex { - index, - _t: std::marker::PhantomData, - } - } -} - -impl<'a, T> Path<'a> for ArrayIndex -where - T: JsonLike, -{ - type Data = T; - - fn find(&self, input: JsonPathValue<'a, Self::Data>) -> Vec> { - input.flat_map_slice(|data, pref| { - data.as_array() - .and_then(|elems| elems.get(self.index)) - .map(|e| vec![JsonPathValue::new_slice(e, jsp_idx(&pref, self.index))]) - .unwrap_or_else(|| vec![NoValue]) - }) - } -} - -/// process @ element -pub struct Current<'a, T> { - tail: Option>, - _t: std::marker::PhantomData, -} - -impl<'a, T> Current<'a, T> -where - T: JsonLike, -{ - pub(crate) fn from(jp: &'a JsonPath, root: &'a T) -> Self { - match jp { - JsonPath::Empty => Current::none(), - tail => Current::new(Box::new(json_path_instance(tail, root))), - } - } - pub(crate) fn new(tail: PathInstance<'a, T>) -> Self { - Current { - tail: Some(tail), - _t: std::marker::PhantomData, - } - } - pub(crate) fn none() -> Self { - Current { - tail: None, - _t: std::marker::PhantomData, - } - } -} - -impl<'a, T> Path<'a> for Current<'a, T> -where - T: JsonLike, -{ - type Data = T; - - fn find(&self, input: JsonPathValue<'a, Self::Data>) -> Vec> { - self.tail - .as_ref() - .map(|p| p.find(input.clone())) - .unwrap_or_else(|| vec![input]) - } -} - -/// the list of indexes like [1,2,3] -pub struct UnionIndex<'a, T> { - indexes: Vec>, -} - -impl<'a, T> UnionIndex<'a, T> -where - T: JsonLike, -{ - pub fn from_indexes(elems: &'a [T]) -> Self { - let mut indexes: Vec> = vec![]; - - for idx in elems.iter() { - indexes.push(TopPaths::ArrayIndex(ArrayIndex::new( - idx.as_u64().unwrap() as usize - ))) - } - - UnionIndex::new(indexes) - } - pub fn from_keys(elems: &'a [String]) -> Self { - let mut indexes: Vec> = vec![]; - - for key in elems.iter() { - indexes.push(TopPaths::ObjectField(ObjectField::new(key))) - } - - UnionIndex::new(indexes) - } - - pub fn new(indexes: Vec>) -> Self { - UnionIndex { indexes } - } -} - -impl<'a, T> Path<'a> for UnionIndex<'a, T> -where - T: JsonLike, -{ - type Data = T; - - fn find(&self, input: JsonPathValue<'a, Self::Data>) -> Vec> { - self.indexes - .iter() - .flat_map(|e| e.find(input.clone())) - .collect() - } -} - -/// process filter element like [?(op sign op)] -pub enum FilterPath<'a, T> { - Filter { - left: PathInstance<'a, T>, - right: PathInstance<'a, T>, - op: &'a FilterSign, - }, - Or { - left: PathInstance<'a, T>, - right: PathInstance<'a, T>, - }, - And { - left: PathInstance<'a, T>, - right: PathInstance<'a, T>, - }, - Not { - exp: PathInstance<'a, T>, - }, -} - -impl<'a, T> FilterPath<'a, T> -where - T: JsonLike, -{ - pub(crate) fn new(expr: &'a FilterExpression, root: &'a T) -> Self { - match expr { - FilterExpression::Atom(left, op, right) => FilterPath::Filter { - left: process_operand(left, root), - right: process_operand(right, root), - op, - }, - FilterExpression::And(l, r) => FilterPath::And { - left: Box::new(FilterPath::new(l, root)), - right: Box::new(FilterPath::new(r, root)), - }, - FilterExpression::Or(l, r) => FilterPath::Or { - left: Box::new(FilterPath::new(l, root)), - right: Box::new(FilterPath::new(r, root)), - }, - FilterExpression::Not(exp) => FilterPath::Not { - exp: Box::new(FilterPath::new(exp, root)), - }, - } - } - fn compound( - one: &'a FilterSign, - two: &'a FilterSign, - left: Vec>, - right: Vec>, - ) -> bool { - FilterPath::process_atom(one, left.clone(), right.clone()) - || FilterPath::process_atom(two, left, right) - } - fn process_atom( - op: &'a FilterSign, - left: Vec>, - right: Vec>, - ) -> bool { - match op { - FilterSign::Equal => ::eq( - JsonPathValue::vec_as_data(left), - JsonPathValue::vec_as_data(right), - ), - FilterSign::Unequal => !FilterPath::process_atom(&FilterSign::Equal, left, right), - FilterSign::Less => ::less( - JsonPathValue::vec_as_data(left), - JsonPathValue::vec_as_data(right), - ), - FilterSign::LeOrEq => { - FilterPath::compound(&FilterSign::Less, &FilterSign::Equal, left, right) - } - FilterSign::Greater => ::less( - JsonPathValue::vec_as_data(right), - JsonPathValue::vec_as_data(left), - ), - FilterSign::GrOrEq => { - FilterPath::compound(&FilterSign::Greater, &FilterSign::Equal, left, right) - } - FilterSign::Regex => ::regex( - JsonPathValue::vec_as_data(left), - JsonPathValue::vec_as_data(right), - ), - FilterSign::In => ::inside( - JsonPathValue::vec_as_data(left), - JsonPathValue::vec_as_data(right), - ), - FilterSign::Nin => !FilterPath::process_atom(&FilterSign::In, left, right), - FilterSign::NoneOf => !FilterPath::process_atom(&FilterSign::AnyOf, left, right), - FilterSign::AnyOf => ::any_of( - JsonPathValue::vec_as_data(left), - JsonPathValue::vec_as_data(right), - ), - FilterSign::SubSetOf => ::sub_set_of( - JsonPathValue::vec_as_data(left), - JsonPathValue::vec_as_data(right), - ), - FilterSign::Exists => !JsonPathValue::vec_as_data(left).is_empty(), - FilterSign::Size => ::size( - JsonPathValue::vec_as_data(left), - JsonPathValue::vec_as_data(right), - ), - } - } - - fn process(&self, curr_el: &'a T) -> bool { - let pref = String::new(); - match self { - FilterPath::Filter { left, right, op } => FilterPath::process_atom( - op, - left.find(Slice(curr_el, pref.clone())), - right.find(Slice(curr_el, pref)), - ), - FilterPath::Or { left, right } => { - if !JsonPathValue::vec_as_data(left.find(Slice(curr_el, pref.clone()))).is_empty() { - true - } else { - !JsonPathValue::vec_as_data(right.find(Slice(curr_el, pref))).is_empty() - } - } - FilterPath::And { left, right } => { - if JsonPathValue::vec_as_data(left.find(Slice(curr_el, pref.clone()))).is_empty() { - false - } else { - !JsonPathValue::vec_as_data(right.find(Slice(curr_el, pref))).is_empty() - } - } - FilterPath::Not { exp } => { - JsonPathValue::vec_as_data(exp.find(Slice(curr_el, pref))).is_empty() - } - } - } -} - -impl<'a, T> Path<'a> for FilterPath<'a, T> -where - T: JsonLike, -{ - type Data = T; - - fn find(&self, input: JsonPathValue<'a, Self::Data>) -> Vec> { - input.flat_map_slice(|data, pref| { - let mut res = vec![]; - if data.is_array() { - let elems = data.as_array().unwrap(); - for (i, el) in elems.iter().enumerate() { - if self.process(el) { - res.push(Slice(el, jsp_idx(&pref, i))) - } - } - } else if self.process(data) { - res.push(Slice(data, pref)) - } - - // match data { - // Array(elems) => { - // for (i, el) in elems.iter().enumerate() { - // if self.process(el) { - // res.push(Slice(el, jsp_idx(&pref, i))) - // } - // } - // } - // el => { - // if self.process(el) { - // res.push(Slice(el, pref)) - // } - // } - // } - if res.is_empty() { - vec![NoValue] - } else { - res - } - }) - } -} - -#[cfg(test)] -mod tests { - use crate::jp_v; - use crate::parser::macros::{chain, filter, idx, op}; - use crate::parser::model::{FilterExpression, FilterSign, JsonPath, JsonPathIndex, Operand}; - use crate::path::index::{ArrayIndex, ArraySlice}; - use crate::path::JsonPathValue; - use crate::path::{json_path_instance, Path}; - use crate::JsonPathValue::NoValue; - - use crate::path; - use serde_json::{json, Value}; - - #[test] - fn array_slice_end_start_test() { - let array = [0, 1, 2, 3, 4, 5]; - let len = array.len() as i32; - let mut slice: ArraySlice = ArraySlice::new(0, 0, 0); - - assert_eq!(slice.start(len).unwrap(), 0); - slice.start_index = 1; - - assert_eq!(slice.start(len).unwrap(), 1); - - slice.start_index = 2; - assert_eq!(slice.start(len).unwrap(), 2); - - slice.start_index = 5; - assert_eq!(slice.start(len).unwrap(), 5); - - slice.start_index = 7; - assert_eq!(slice.start(len), None); - - slice.start_index = -1; - assert_eq!(slice.start(len).unwrap(), 5); - - slice.start_index = -5; - assert_eq!(slice.start(len).unwrap(), 1); - - slice.end_index = 0; - assert_eq!(slice.end(len).unwrap(), 0); - - slice.end_index = 5; - assert_eq!(slice.end(len).unwrap(), 5); - - slice.end_index = -1; - assert_eq!(slice.end(len).unwrap(), 5); - - slice.end_index = -5; - assert_eq!(slice.end(len).unwrap(), 1); - } - - #[test] - fn slice_test() { - let array = json!([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); - - let mut slice = ArraySlice::new(0, 6, 2); - let j1 = json!(0); - let j2 = json!(2); - let j4 = json!(4); - assert_eq!( - slice.find(JsonPathValue::new_slice(&array, "a".to_string())), - jp_v![&j1;"a[0]", &j2;"a[2]", &j4;"a[4]"] - ); - - slice.step = 3; - let j0 = json!(0); - let j3 = json!(3); - assert_eq!(slice.find(jp_v!(&array)), jp_v![&j0;"[0]", &j3;"[3]"]); - - slice.start_index = -1; - slice.end_index = 1; - - assert_eq!( - slice.find(JsonPathValue::new_slice(&array, "a".to_string())), - vec![NoValue] - ); - - slice.start_index = -10; - slice.end_index = 10; - - let j1 = json!(1); - let j4 = json!(4); - let j7 = json!(7); - - assert_eq!( - slice.find(JsonPathValue::new_slice(&array, "a".to_string())), - jp_v![&j1;"a[1]", &j4;"a[4]", &j7;"a[7]"] - ); - } - - #[test] - fn index_test() { - let array = json!([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); - - let mut index = ArrayIndex::new(0); - let j0 = json!(0); - let j10 = json!(10); - assert_eq!( - index.find(JsonPathValue::new_slice(&array, "a".to_string())), - jp_v![&j0;"a[0]",] - ); - index.index = 10; - assert_eq!( - index.find(JsonPathValue::new_slice(&array, "a".to_string())), - jp_v![&j10;"a[10]",] - ); - index.index = 100; - assert_eq!( - index.find(JsonPathValue::new_slice(&array, "a".to_string())), - vec![NoValue] - ); - } - - #[test] - fn current_test() { - let json = json!( - { - "object":{ - "field_1":[1,2,3], - "field_2":42, - "field_3":{"a":"b"} - - } - }); - - let chain = chain!(path!($), path!("object"), path!(@)); - - let path_inst = json_path_instance(&chain, &json); - let res = json!({ - "field_1":[1,2,3], - "field_2":42, - "field_3":{"a":"b"} - }); - - let expected_res = jp_v!(&res;"$.['object']",); - assert_eq!(path_inst.find(jp_v!(&json)), expected_res); - - let cur = path!(@,path!("field_3"),path!("a")); - let chain = chain!(path!($), path!("object"), cur); - - let path_inst = json_path_instance(&chain, &json); - let res1 = json!("b"); - - let expected_res = vec![JsonPathValue::new_slice( - &res1, - "$.['object'].['field_3'].['a']".to_string(), - )]; - assert_eq!(path_inst.find(jp_v!(&json)), expected_res); - } - - #[test] - fn filter_exist_test() { - let json = json!({ - "threshold" : 3, - "key":[{"field":[1,2,3,4,5],"field1":[7]},{"field":42}], - }); - - let index = path!(idx!(?filter!(op!(path!(@, path!("field"))), "exists", op!()))); - let chain = chain!(path!($), path!("key"), index, path!("field")); - - let path_inst = json_path_instance(&chain, &json); - - let exp1 = json!([1, 2, 3, 4, 5]); - let exp2 = json!(42); - let expected_res = jp_v!(&exp1;"$.['key'][0].['field']",&exp2;"$.['key'][1].['field']"); - assert_eq!(path_inst.find(jp_v!(&json)), expected_res) - } - - #[test] - fn filter_gr_test() { - let json = json!({ - "threshold" : 4, - "key":[ - {"field":1}, - {"field":10}, - {"field":4}, - {"field":5}, - {"field":1}, - ] - }); - let _exp1 = json!( {"field":10}); - let _exp2 = json!( {"field":5}); - let exp3 = json!( {"field":4}); - let exp4 = json!( {"field":1}); - - let index = path!( - idx!(?filter!(op!(path!(@, path!("field"))), ">", op!(chain!(path!($), path!("threshold"))))) - ); - - let chain = chain!(path!($), path!("key"), index); - - let path_inst = json_path_instance(&chain, &json); - - let exp1 = json!( {"field":10}); - let exp2 = json!( {"field":5}); - let expected_res = jp_v![&exp1;"$.['key'][1]", &exp2;"$.['key'][3]"]; - assert_eq!( - path_inst.find(JsonPathValue::from_root(&json)), - expected_res - ); - let expected_res = jp_v![&exp1;"$.['key'][1]", &exp2;"$.['key'][3]"]; - assert_eq!( - path_inst.find(JsonPathValue::from_root(&json)), - expected_res - ); - - let index = path!( - idx!(?filter!(op!(path!(@, path!("field"))), ">=", op!(chain!(path!($), path!("threshold"))))) - ); - let chain = chain!(path!($), path!("key"), index); - let path_inst = json_path_instance(&chain, &json); - let expected_res = jp_v![ - &exp1;"$.['key'][1]", &exp3;"$.['key'][2]", &exp2;"$.['key'][3]"]; - assert_eq!( - path_inst.find(JsonPathValue::from_root(&json)), - expected_res - ); - - let index = path!( - idx!(?filter!(op!(path!(@, path!("field"))), "<", op!(chain!(path!($), path!("threshold"))))) - ); - let chain = chain!(path!($), path!("key"), index); - let path_inst = json_path_instance(&chain, &json); - let expected_res = jp_v![&exp4;"$.['key'][0]", &exp4;"$.['key'][4]"]; - assert_eq!( - path_inst.find(JsonPathValue::from_root(&json)), - expected_res - ); - - let index = path!( - idx!(?filter!(op!(path!(@, path!("field"))), "<=", op!(chain!(path!($), path!("threshold"))))) - ); - let chain = chain!(path!($), path!("key"), index); - let path_inst = json_path_instance(&chain, &json); - let expected_res = jp_v![ - &exp4;"$.['key'][0]", - &exp3;"$.['key'][2]", - &exp4;"$.['key'][4]"]; - assert_eq!( - path_inst.find(JsonPathValue::from_root(&json)), - expected_res - ); - } - - #[test] - fn filter_regex_test() { - let json = json!({ - "key":[ - {"field":"a11#"}, - {"field":"a1#1"}, - {"field":"a#11"}, - {"field":"#a11"}, - ] - }); - - let index = idx!(?filter!(op!(path!(@,path!("field"))),"~=", op!("[a-zA-Z]+[0-9]#[0-9]+"))); - let chain = chain!(path!($), path!("key"), path!(index)); - - let path_inst = json_path_instance(&chain, &json); - - let exp2 = json!( {"field":"a1#1"}); - let expected_res = jp_v![&exp2;"$.['key'][1]",]; - assert_eq!( - path_inst.find(JsonPathValue::from_root(&json)), - expected_res - ) - } - - #[test] - fn filter_any_of_test() { - let json = json!({ - "key":[ - {"field":"a11#"}, - {"field":"a1#1"}, - {"field":"a#11"}, - {"field":"#a11"}, - ] - }); - - let index = idx!(?filter!( - op!(path!(@,path!("field"))), - "anyOf", - op!(s ["a11#","aaa","111"]) - )); - - let chain = chain!(path!($), JsonPath::Field(String::from("key")), path!(index)); - - let path_inst = json_path_instance(&chain, &json); - - let exp2 = json!( {"field":"a11#"}); - let expected_res = jp_v![&exp2;"$.['key'][0]",]; - assert_eq!( - path_inst.find(JsonPathValue::from_root(&json)), - expected_res - ) - } - - #[test] - fn size_test() { - let json = json!({ - "key":[ - {"field":"aaaa"}, - {"field":"bbb"}, - {"field":"cc"}, - {"field":"dddd"}, - {"field":[1,1,1,1]}, - ] - }); - - let index = idx!(?filter!(op!(path!(@, path!("field"))),"size",op!(4))); - let chain = chain!(path!($), path!("key"), path!(index)); - let path_inst = json_path_instance(&chain, &json); - - let f1 = json!( {"field":"aaaa"}); - let f2 = json!( {"field":"dddd"}); - let f3 = json!( {"field":[1,1,1,1]}); - - let expected_res = jp_v![&f1;"$.['key'][0]", &f2;"$.['key'][3]", &f3;"$.['key'][4]"]; - assert_eq!( - path_inst.find(JsonPathValue::from_root(&json)), - expected_res - ) - } - - #[test] - fn nested_filter_test() { - let json = json!({ - "obj":{ - "id":1, - "not_id": 2, - "more_then_id" :3 - } - }); - let index = idx!(?filter!( - op!(path!(@,path!("not_id"))), "==",op!(2) - )); - let chain = chain!(path!($), path!("obj"), path!(index)); - let path_inst = json_path_instance(&chain, &json); - let js = json!({ - "id":1, - "not_id": 2, - "more_then_id" :3 - }); - assert_eq!( - path_inst.find(JsonPathValue::from_root(&json)), - jp_v![&js;"$.['obj']",] - ) - } - - #[test] - fn or_arr_test() { - let json = json!({ - "key":[ - {"city":"London","capital":true, "size": "big"}, - {"city":"Berlin","capital":true,"size": "big"}, - {"city":"Tokyo","capital":true,"size": "big"}, - {"city":"Moscow","capital":true,"size": "big"}, - {"city":"Athlon","capital":false,"size": "small"}, - {"city":"Dortmund","capital":false,"size": "big"}, - {"city":"Dublin","capital":true,"size": "small"}, - ] - }); - let index = idx!(?filter!( - filter!(op!(path!(@,path!("capital"))), "==", op!(false)), - ||, - filter!(op!(path!(@,path!("size"))), "==", op!("small")) - ) - ); - let chain = chain!(path!($), path!("key"), path!(index), path!("city")); - let path_inst = json_path_instance(&chain, &json); - let a = json!("Athlon"); - let d = json!("Dortmund"); - let dd = json!("Dublin"); - assert_eq!( - path_inst.find(JsonPathValue::from_root(&json)), - jp_v![ - &a;"$.['key'][4].['city']", - &d;"$.['key'][5].['city']", - ⅆ"$.['key'][6].['city']"] - ) - } - - #[test] - fn or_obj_test() { - let json = json!({ - "key":{ - "id":1, - "name":"a", - "another":"b" - } - }); - let index = idx!(?filter!( - filter!(op!(path!(@,path!("name"))), "==", op!("a")), - ||, - filter!(op!(path!(@,path!("another"))), "==", op!("b")) - ) - ); - let chain = chain!(path!($), path!("key"), path!(index), path!("id")); - let path_inst = json_path_instance(&chain, &json); - let j1 = json!(1); - assert_eq!( - path_inst.find(JsonPathValue::from_root(&json)), - jp_v![&j1;"$.['key'].['id']",] - ) - } - - #[test] - fn or_obj_2_test() { - let json = json!({ - "key":{ - "id":1, - "name":"a", - "another":"d" - } - }); - let index = idx!(?filter!( - filter!(op!(path!(@,path!("name"))), "==", op!("c")), - ||, - filter!(op!(path!(@,path!("another"))), "==", op!("d")) - ) - ); - let chain = chain!(path!($), path!("key"), path!(index), path!("id")); - let path_inst = json_path_instance(&chain, &json); - let j1 = json!(1); - assert_eq!( - path_inst.find(JsonPathValue::from_root(&json)), - jp_v![&j1;"$.['key'].['id']",] - ) - } - - #[test] - fn and_arr_test() { - let json = json!({ - "key":[ - {"city":"London","capital":true, "size": "big"}, - {"city":"Berlin","capital":true,"size": "big"}, - {"city":"Tokyo","capital":true,"size": "big"}, - {"city":"Moscow","capital":true,"size": "big"}, - {"city":"Athlon","capital":false,"size": "small"}, - {"city":"Dortmund","capital":false,"size": "big"}, - {"city":"Dublin","capital":true,"size": "small"}, - ] - }); - let index = idx!(?filter!( - filter!(op!(path!(@,path!("capital"))), "==", op!(false)), - &&, - filter!(op!(path!(@,path!("size"))), "==", op!("small")) - ) - ); - let chain = chain!(path!($), path!("key"), path!(index), path!("city")); - let path_inst = json_path_instance(&chain, &json); - let a = json!("Athlon"); - let value = jp_v!( &a;"$.['key'][4].['city']",); - assert_eq!(path_inst.find(JsonPathValue::from_root(&json)), value) - } - - #[test] - fn and_obj_test() { - let json = json!({ - "key":{ - "id":1, - "name":"a", - "another":"b" - } - }); - let index = idx!(?filter!( - filter!(op!(path!(@,path!("name"))), "==", op!("a")), - &&, - filter!(op!(path!(@,path!("another"))), "==", op!("b")) - ) - ); - let chain = chain!(path!($), path!("key"), path!(index), path!("id")); - let path_inst = json_path_instance(&chain, &json); - let j1 = json!(1); - assert_eq!( - path_inst.find(JsonPathValue::from_root(&json)), - jp_v![&j1; "$.['key'].['id']",] - ) - } - - #[test] - fn and_obj_2_test() { - let json = json!({ - "key":{ - "id":1, - "name":"a", - "another":"d" - } - }); - let index = idx!(?filter!( - filter!(op!(path!(@,path!("name"))), "==", op!("c")), - &&, - filter!(op!(path!(@,path!("another"))), "==", op!("d")) - ) - ); - let chain = chain!(path!($), path!("key"), path!(index), path!("id")); - let path_inst = json_path_instance(&chain, &json); - assert_eq!( - path_inst.find(JsonPathValue::from_root(&json)), - vec![NoValue] - ) - } -} diff --git a/src/path/mod.rs b/src/path/mod.rs deleted file mode 100644 index 9b67555..0000000 --- a/src/path/mod.rs +++ /dev/null @@ -1,812 +0,0 @@ -use std::fmt::Debug; - -use crate::{jsp_idx, jsp_obj, JsonPathParserError, JsonPathStr, JsonPathValue}; -use regex::Regex; -use serde_json::{json, Value}; - -use crate::parser::model::{Function, JsonPath, JsonPathIndex, Operand}; -use crate::parser::parse_json_path; -pub use crate::path::index::{ArrayIndex, ArraySlice, Current, FilterPath, UnionIndex}; -pub use crate::path::top::ObjectField; -use crate::path::top::*; - -/// The module is in charge of processing [[JsonPathIndex]] elements -mod index; -/// The module is responsible for processing of the [[JsonPath]] elements -mod top; - -/// The `JsonLike` trait defines a set of methods and associated types for working with JSON-like data structures. -/// -/// It provides a common interface for accessing and manipulating JSON data, -/// allowing for operations such as -/// - retrieving values by key, -/// - iterating over elements -/// - performing various comparisons and transformations. -/// -/// The trait is implemented for the `serde_json::Value` type already -pub trait JsonLike: - Default - + Clone - + Debug - + for<'a> From<&'a str> - + From> - + From - + From - + From - + From> - + From - + PartialEq - + 'static -{ - /// Retrieves a reference to the value associated with the given key. - fn get(&self, key: &str) -> Option<&Self>; - - /// Iterates over the elements with a given prefix and returns a vector of `JsonPathValue`. - fn itre(&self, pref: String) -> Vec>; - - /// Returns the length of the array as a `JsonPathValue`. - fn array_len(&self) -> JsonPathValue<'static, Self>; - - /// Initializes an instance with a specific size. - fn init_with_usize(cnt: usize) -> Self; - - /// Flattens nested structures and returns a vector of tuples containing references to the elements and their paths. - fn deep_flatten(&self, pref: String) -> Vec<(&Self, String)>; - - /// Performs a deep search by key and returns a vector of tuples containing references to the elements and their paths. - fn deep_path_by_key<'a>( - &'a self, - key: ObjectField<'a, Self>, - pref: String, - ) -> Vec<(&'a Self, String)>; - - /// Converts the element to an `Option`. - fn as_u64(&self) -> Option; - - /// Checks if the element is an array. - fn is_array(&self) -> bool; - - /// Converts the element to an `Option<&Vec>`. - fn as_array(&self) -> Option<&Vec>; - - /// Compares the size of two vectors of references to elements. - fn size(left: Vec<&Self>, right: Vec<&Self>) -> bool; - - /// Checks if the left vector is a subset of the right vector. - fn sub_set_of(left: Vec<&Self>, right: Vec<&Self>) -> bool; - - /// Checks if any element in the left vector is present in the right vector. - fn any_of(left: Vec<&Self>, right: Vec<&Self>) -> bool; - - /// Checks if the elements in the left vector match the regex pattern in the right vector. - fn regex(left: Vec<&Self>, right: Vec<&Self>) -> bool; - - /// Checks if any element in the left vector is inside the right vector. - fn inside(left: Vec<&Self>, right: Vec<&Self>) -> bool; - - /// Ensures the number on the left side is less than the number on the right side. - fn less(left: Vec<&Self>, right: Vec<&Self>) -> bool; - - /// Compares elements for equality. - fn eq(left: Vec<&Self>, right: Vec<&Self>) -> bool; - - /// Returns a null value. - fn null() -> Self; - - /// Creates an array from a vector of elements. - fn array(data: Vec) -> Self; - - /// Retrieves a reference to the element at the specified path. - /// The path is specified as a string and can be obtained from the query. - /// - /// # Arguments - /// * `path` - A json path to the element specified as a string (root, field, index only). - fn reference(&self, path: T) -> Result, JsonPathParserError> - where - T: Into; - - /// Retrieves a mutable reference to the element at the specified path. - /// - /// # Arguments - /// * `path` - A json path to the element specified as a string (root, field, index only). - /// - /// # Examples - /// - /// ``` - /// use serde_json::json; - /// use jsonpath_rust::{JsonPath, JsonPathParserError}; - /// use jsonpath_rust::path::JsonLike; - /// - /// let mut json = json!([ - /// {"verb": "RUN","distance":[1]}, - /// {"verb": "TEST"}, - /// {"verb": "DO NOT RUN"} - /// ]); - /// - /// let path: Box = Box::from(JsonPath::try_from("$.[?(@.verb == 'RUN')]").unwrap()); - /// let elem = path - /// .find_as_path(&json) - /// .get(0) - /// .cloned() - /// .ok_or(JsonPathParserError::InvalidJsonPath("".to_string())).unwrap(); - /// - /// if let Some(v) = json - /// .reference_mut(elem).unwrap() - /// .and_then(|v| v.as_object_mut()) - /// .and_then(|v| v.get_mut("distance")) - /// .and_then(|v| v.as_array_mut()) - /// { - /// v.push(json!(2)) - /// } - /// - /// assert_eq!( - /// json, - /// json!([ - /// {"verb": "RUN","distance":[1,2]}, - /// {"verb": "TEST"}, - /// {"verb": "DO NOT RUN"} - /// ]) - /// ); - /// ``` - fn reference_mut(&mut self, path: T) -> Result, JsonPathParserError> - where - T: Into; -} - -impl JsonLike for Value { - fn get(&self, key: &str) -> Option<&Self> { - self.get(key) - } - fn itre(&self, pref: String) -> Vec> { - let res = match self { - Value::Array(elems) => { - let mut res = vec![]; - for (idx, el) in elems.iter().enumerate() { - res.push(JsonPathValue::Slice(el, jsp_idx(&pref, idx))); - } - res - } - Value::Object(elems) => { - let mut res = vec![]; - for (key, el) in elems.into_iter() { - res.push(JsonPathValue::Slice(el, jsp_obj(&pref, key))); - } - res - } - _ => vec![], - }; - if res.is_empty() { - vec![JsonPathValue::NoValue] - } else { - res - } - } - - fn array_len(&self) -> JsonPathValue<'static, Self> { - match self { - Value::Array(elems) => JsonPathValue::NewValue(json!(elems.len())), - _ => JsonPathValue::NoValue, - } - } - - fn init_with_usize(cnt: usize) -> Self { - json!(cnt) - } - - fn deep_flatten(&self, pref: String) -> Vec<(&Self, String)> { - let mut acc = vec![]; - match self { - Value::Object(elems) => { - for (f, v) in elems.into_iter() { - let pref = jsp_obj(&pref, f); - acc.push((v, pref.clone())); - acc.append(&mut v.deep_flatten(pref)); - } - } - Value::Array(elems) => { - for (i, v) in elems.iter().enumerate() { - let pref = jsp_idx(&pref, i); - acc.push((v, pref.clone())); - acc.append(&mut v.deep_flatten(pref)); - } - } - _ => (), - } - acc - } - - fn deep_path_by_key<'a>( - &'a self, - key: ObjectField<'a, Self>, - pref: String, - ) -> Vec<(&'a Self, String)> { - let mut result: Vec<(&'a Value, String)> = - JsonPathValue::vec_as_pair(key.find(JsonPathValue::new_slice(self, pref.clone()))); - match self { - Value::Object(elems) => { - let mut next_levels: Vec<(&'a Value, String)> = elems - .into_iter() - .flat_map(|(k, v)| v.deep_path_by_key(key.clone(), jsp_obj(&pref, k))) - .collect(); - result.append(&mut next_levels); - result - } - Value::Array(elems) => { - let mut next_levels: Vec<(&'a Value, String)> = elems - .iter() - .enumerate() - .flat_map(|(i, v)| v.deep_path_by_key(key.clone(), jsp_idx(&pref, i))) - .collect(); - result.append(&mut next_levels); - result - } - _ => result, - } - } - - fn as_u64(&self) -> Option { - self.as_u64() - } - fn is_array(&self) -> bool { - self.is_array() - } - fn as_array(&self) -> Option<&Vec> { - self.as_array() - } - - fn size(left: Vec<&Self>, right: Vec<&Self>) -> bool { - if let Some(Value::Number(n)) = right.first() { - if let Some(sz) = n.as_f64() { - for el in left.iter() { - match el { - Value::String(v) if v.len() == sz as usize => true, - Value::Array(elems) if elems.len() == sz as usize => true, - Value::Object(fields) if fields.len() == sz as usize => true, - _ => return false, - }; - } - return true; - } - } - false - } - - fn sub_set_of(left: Vec<&Self>, right: Vec<&Self>) -> bool { - if left.is_empty() { - return true; - } - if right.is_empty() { - return false; - } - - if let Some(elems) = left.first().and_then(|e| e.as_array()) { - if let Some(Value::Array(right_elems)) = right.first() { - if right_elems.is_empty() { - return false; - } - - for el in elems { - let mut res = false; - - for r in right_elems.iter() { - if el.eq(r) { - res = true - } - } - if !res { - return false; - } - } - return true; - } - } - false - } - - fn any_of(left: Vec<&Self>, right: Vec<&Self>) -> bool { - if left.is_empty() { - return true; - } - if right.is_empty() { - return false; - } - - if let Some(Value::Array(elems)) = right.first() { - if elems.is_empty() { - return false; - } - - for el in left.iter() { - if let Some(left_elems) = el.as_array() { - for l in left_elems.iter() { - for r in elems.iter() { - if l.eq(r) { - return true; - } - } - } - } else { - for r in elems.iter() { - if el.eq(&r) { - return true; - } - } - } - } - } - - false - } - - fn regex(left: Vec<&Self>, right: Vec<&Self>) -> bool { - if left.is_empty() || right.is_empty() { - return false; - } - - match right.first() { - Some(Value::String(str)) => { - if let Ok(regex) = Regex::new(str) { - for el in left.iter() { - if let Some(v) = el.as_str() { - if regex.is_match(v) { - return true; - } - } - } - } - false - } - _ => false, - } - } - - fn inside(left: Vec<&Self>, right: Vec<&Self>) -> bool { - if left.is_empty() { - return false; - } - - match right.first() { - Some(Value::Array(elems)) => { - for el in left.iter() { - if elems.contains(el) { - return true; - } - } - false - } - Some(Value::Object(elems)) => { - for el in left.iter() { - for r in elems.values() { - if el.eq(&r) { - return true; - } - } - } - false - } - _ => false, - } - } - - /// ensure the number on the left side is less the number on the right side - fn less(left: Vec<&Self>, right: Vec<&Self>) -> bool { - if left.len() == 1 && right.len() == 1 { - match (left.first(), right.first()) { - (Some(Value::Number(l)), Some(Value::Number(r))) => l - .as_f64() - .and_then(|v1| r.as_f64().map(|v2| v1 < v2)) - .unwrap_or(false), - _ => false, - } - } else { - false - } - } - - /// compare elements - fn eq(left: Vec<&Self>, right: Vec<&Self>) -> bool { - if left.len() != right.len() { - false - } else { - left.iter().zip(right).map(|(a, b)| a.eq(&b)).all(|a| a) - } - } - - fn null() -> Self { - Value::Null - } - - fn array(data: Vec) -> Self { - Value::Array(data) - } - - fn reference(&self, path: T) -> Result, JsonPathParserError> - where - T: Into, - { - Ok(self.pointer(&path_to_json_path(path.into())?)) - } - - fn reference_mut(&mut self, path: T) -> Result, JsonPathParserError> - where - T: Into, - { - Ok(self.pointer_mut(&path_to_json_path(path.into())?)) - } -} - -fn path_to_json_path(path: JsonPathStr) -> Result { - convert_part(&parse_json_path::(path.as_str())?) -} - -fn convert_part(path: &JsonPath) -> Result { - match path { - JsonPath::Chain(elems) => elems - .iter() - .map(convert_part) - .collect::>(), - - JsonPath::Index(JsonPathIndex::Single(v)) => Ok(format!("/{}", v)), - JsonPath::Field(e) => Ok(format!("/{}", e)), - JsonPath::Root => Ok("".to_string()), - e => Err(JsonPathParserError::InvalidJsonPath(e.to_string())), - } -} - -/// The trait defining the behaviour of processing every separated element. -/// type Data usually stands for json [[Value]] -/// The trait also requires to have a root json to process. -/// It needs in case if in the filter there will be a pointer to the absolute path -pub trait Path<'a> { - type Data; - /// when every element needs to handle independently - fn find(&self, input: JsonPathValue<'a, Self::Data>) -> Vec> { - vec![input] - } - /// when the whole output needs to handle - fn flat_find( - &self, - input: Vec>, - _is_search_length: bool, - ) -> Vec> { - input.into_iter().flat_map(|d| self.find(d)).collect() - } - /// defines when we need to invoke `find` or `flat_find` - fn needs_all(&self) -> bool { - false - } -} - -/// all known Paths, mostly to avoid a dynamic Box and vtable for internal function -pub enum TopPaths<'a, T> { - RootPointer(RootPointer<'a, T>), - ObjectField(ObjectField<'a, T>), - Chain(Chain<'a, T>), - Wildcard(Wildcard), - DescentObject(DescentObject<'a, T>), - DescentWildcard(DescentWildcard), - Current(Current<'a, T>), - ArrayIndex(ArrayIndex), - ArraySlice(ArraySlice), - UnionIndex(UnionIndex<'a, T>), - FilterPath(FilterPath<'a, T>), - IdentityPath(IdentityPath), - FnPath(FnPath), -} - -impl<'a, T> Path<'a> for TopPaths<'a, T> -where - T: JsonLike, -{ - type Data = T; - - fn find(&self, input: JsonPathValue<'a, Self::Data>) -> Vec> { - match self { - TopPaths::RootPointer(inner) => inner.find(input), - TopPaths::ObjectField(inner) => inner.find(input), - TopPaths::Chain(inner) => inner.find(input), - TopPaths::Wildcard(inner) => inner.find(input), - TopPaths::DescentObject(inner) => inner.find(input), - TopPaths::DescentWildcard(inner) => inner.find(input), - TopPaths::Current(inner) => inner.find(input), - TopPaths::ArrayIndex(inner) => inner.find(input), - TopPaths::ArraySlice(inner) => inner.find(input), - TopPaths::UnionIndex(inner) => inner.find(input), - TopPaths::FilterPath(inner) => inner.find(input), - TopPaths::IdentityPath(inner) => inner.find(input), - TopPaths::FnPath(inner) => inner.find(input), - } - } - - fn flat_find( - &self, - input: Vec>, - _is_search_length: bool, - ) -> Vec> { - match self { - TopPaths::RootPointer(inner) => inner.flat_find(input, _is_search_length), - TopPaths::ObjectField(inner) => inner.flat_find(input, _is_search_length), - TopPaths::Chain(inner) => inner.flat_find(input, _is_search_length), - TopPaths::Wildcard(inner) => inner.flat_find(input, _is_search_length), - TopPaths::DescentObject(inner) => inner.flat_find(input, _is_search_length), - TopPaths::DescentWildcard(inner) => inner.flat_find(input, _is_search_length), - TopPaths::Current(inner) => inner.flat_find(input, _is_search_length), - TopPaths::ArrayIndex(inner) => inner.flat_find(input, _is_search_length), - TopPaths::ArraySlice(inner) => inner.flat_find(input, _is_search_length), - TopPaths::UnionIndex(inner) => inner.flat_find(input, _is_search_length), - TopPaths::FilterPath(inner) => inner.flat_find(input, _is_search_length), - TopPaths::IdentityPath(inner) => inner.flat_find(input, _is_search_length), - TopPaths::FnPath(inner) => inner.flat_find(input, _is_search_length), - } - } - - fn needs_all(&self) -> bool { - match self { - TopPaths::RootPointer(inner) => inner.needs_all(), - TopPaths::ObjectField(inner) => inner.needs_all(), - TopPaths::Chain(inner) => inner.needs_all(), - TopPaths::Wildcard(inner) => inner.needs_all(), - TopPaths::DescentObject(inner) => inner.needs_all(), - TopPaths::DescentWildcard(inner) => inner.needs_all(), - TopPaths::Current(inner) => inner.needs_all(), - TopPaths::ArrayIndex(inner) => inner.needs_all(), - TopPaths::ArraySlice(inner) => inner.needs_all(), - TopPaths::UnionIndex(inner) => inner.needs_all(), - TopPaths::FilterPath(inner) => inner.needs_all(), - TopPaths::IdentityPath(inner) => inner.needs_all(), - TopPaths::FnPath(inner) => inner.needs_all(), - } - } -} - -/// The basic type for instances. -pub(crate) type PathInstance<'a, T> = Box + 'a>; - -/// The major method to process the top part of json part -pub(crate) fn json_path_instance<'a, T>(json_path: &'a JsonPath, root: &'a T) -> TopPaths<'a, T> -where - T: JsonLike, -{ - match json_path { - JsonPath::Root => TopPaths::RootPointer(RootPointer::new(root)), - JsonPath::Field(key) => TopPaths::ObjectField(ObjectField::new(key)), - JsonPath::Chain(chain) => TopPaths::Chain(Chain::from(chain, root)), - JsonPath::Wildcard => TopPaths::Wildcard(Wildcard::new()), - JsonPath::Descent(key) => TopPaths::DescentObject(DescentObject::new(key)), - JsonPath::DescentW => TopPaths::DescentWildcard(DescentWildcard::new()), - JsonPath::Current(value) => TopPaths::Current(Current::from(value, root)), - JsonPath::Index(JsonPathIndex::Single(index)) => { - TopPaths::ArrayIndex(ArrayIndex::new(index.as_u64().unwrap() as usize)) - } - JsonPath::Index(JsonPathIndex::Slice(s, e, step)) => { - TopPaths::ArraySlice(ArraySlice::new(*s, *e, *step)) - } - JsonPath::Index(JsonPathIndex::UnionKeys(elems)) => { - TopPaths::UnionIndex(UnionIndex::from_keys(elems)) - } - JsonPath::Index(JsonPathIndex::UnionIndex(elems)) => { - TopPaths::UnionIndex(UnionIndex::from_indexes(elems)) - } - JsonPath::Index(JsonPathIndex::Filter(fe)) => { - TopPaths::FilterPath(FilterPath::new(fe, root)) - } - JsonPath::Empty => TopPaths::IdentityPath(IdentityPath::new()), - JsonPath::Fn(Function::Length) => TopPaths::FnPath(FnPath::new_size()), - } -} - -/// The method processes the operand inside the filter expressions -fn process_operand<'a, T>(op: &'a Operand, root: &'a T) -> PathInstance<'a, T> -where - T: JsonLike, -{ - Box::new(match op { - Operand::Static(v) => json_path_instance(&JsonPath::Root, v), - Operand::Dynamic(jp) => json_path_instance(jp, root), - }) -} - -#[cfg(test)] -mod tests { - use crate::path::JsonPathIndex; - use crate::path::{convert_part, JsonLike}; - use crate::{idx, path, JsonPath, JsonPathParserError}; - use serde_json::{json, Value}; - - #[test] - fn value_eq_test() { - let left = json!({"value":42}); - let right = json!({"value":42}); - let right_uneq = json!([42]); - assert!(&left.eq(&right)); - assert!(!&left.eq(&right_uneq)); - } - - #[test] - fn vec_value_test() { - let left = json!({"value":42}); - let left1 = json!(42); - let left2 = json!([1, 2, 3]); - let left3 = json!({"value2":[42],"value":[42]}); - - let right = json!({"value":42}); - let right1 = json!(42); - let right2 = json!([1, 2, 3]); - let right3 = json!({"value":[42],"value2":[42]}); - - assert!(JsonLike::eq(vec![&left], vec![&right])); - - assert!(!JsonLike::eq(vec![], vec![&right])); - assert!(!JsonLike::eq(vec![&right], vec![])); - - assert!(JsonLike::eq( - vec![&left, &left1, &left2, &left3], - vec![&right, &right1, &right2, &right3] - )); - - assert!(!JsonLike::eq( - vec![&left1, &left, &left2, &left3], - vec![&right, &right1, &right2, &right3] - )); - } - - #[test] - fn less_value_test() { - let left = json!(10); - let right = json!(11); - - assert!(JsonLike::less(vec![&left], vec![&right])); - assert!(!JsonLike::less(vec![&right], vec![&left])); - - let left = json!(-10); - let right = json!(-11); - - assert!(!JsonLike::less(vec![&left], vec![&right])); - assert!(JsonLike::less(vec![&right], vec![&left])); - - let left = json!(-10.0); - let right = json!(-11.0); - - assert!(!JsonLike::less(vec![&left], vec![&right])); - assert!(JsonLike::less(vec![&right], vec![&left])); - - assert!(!JsonLike::less(vec![], vec![&right])); - assert!(!JsonLike::less(vec![&right, &right], vec![&left])); - } - - #[test] - fn regex_test() { - let right = json!("[a-zA-Z]+[0-9]#[0-9]+"); - let left1 = json!("a11#"); - let left2 = json!("a1#1"); - let left3 = json!("a#11"); - let left4 = json!("#a11"); - - assert!(JsonLike::regex( - vec![&left1, &left2, &left3, &left4], - vec![&right] - )); - assert!(!JsonLike::regex(vec![&left1, &left3, &left4], vec![&right])) - } - - #[test] - fn any_of_test() { - let right = json!([1, 2, 3, 4, 5, 6]); - let left = json!([1, 100, 101]); - assert!(JsonLike::any_of(vec![&left], vec![&right])); - - let left = json!([11, 100, 101]); - assert!(!JsonLike::any_of(vec![&left], vec![&right])); - - let left1 = json!(1); - let left2 = json!(11); - assert!(JsonLike::any_of(vec![&left1, &left2], vec![&right])); - } - - #[test] - fn sub_set_of_test() { - let left1 = json!(1); - let left2 = json!(2); - let left3 = json!(3); - let left40 = json!(40); - let right = json!([1, 2, 3, 4, 5, 6]); - assert!(JsonLike::sub_set_of( - vec![&Value::Array(vec![ - left1.clone(), - left2.clone(), - left3.clone() - ])], - vec![&right] - )); - assert!(!JsonLike::sub_set_of( - vec![&Value::Array(vec![left1, left2, left3, left40])], - vec![&right] - )); - } - - #[test] - fn size_test() { - let left1 = json!("abc"); - let left2 = json!([1, 2, 3]); - let left3 = json!([1, 2, 3, 4]); - let right = json!(3); - let right1 = json!(4); - assert!(JsonLike::size(vec![&left1], vec![&right])); - assert!(JsonLike::size(vec![&left2], vec![&right])); - assert!(!JsonLike::size(vec![&left3], vec![&right])); - assert!(JsonLike::size(vec![&left3], vec![&right1])); - } - - #[test] - fn convert_paths() -> Result<(), JsonPathParserError> { - let r = convert_part(&JsonPath::Chain(vec![ - path!($), - path!("abc"), - path!(idx!(1)), - ]))?; - assert_eq!(r, "/abc/1"); - - assert!(convert_part(&JsonPath::Chain(vec![path!($), path!(.."abc")])).is_err()); - - Ok(()) - } - - #[test] - fn test_references() -> Result<(), JsonPathParserError> { - let mut json = json!({ - "a": { - "b": { - "c": 42 - } - } - }); - - let path_str = convert_part(&JsonPath::Chain(vec![path!("a"), path!("b"), path!("c")]))?; - - if let Some(v) = json.pointer_mut(&path_str) { - *v = json!(43); - } - - assert_eq!( - json, - json!({ - "a": { - "b": { - "c": 43 - } - } - }) - ); - - Ok(()) - } - #[test] - fn test_js_reference() -> Result<(), JsonPathParserError> { - let mut json = json!({ - "a": { - "b": { - "c": 42 - } - } - }); - - let path = "$.a.b.c"; - - if let Some(v) = json.reference_mut(path)? { - *v = json!(43); - } - - assert_eq!( - json, - json!({ - "a": { - "b": { - "c": 43 - } - } - }) - ); - - Ok(()) - } -} diff --git a/src/path/top.rs b/src/path/top.rs deleted file mode 100644 index 620ff71..0000000 --- a/src/path/top.rs +++ /dev/null @@ -1,640 +0,0 @@ -use crate::jsp_obj; -use crate::parser::model::*; -use crate::path::{json_path_instance, JsonPathValue, Path}; -use crate::JsonPathValue::{NewValue, NoValue, Slice}; - -use super::{JsonLike, TopPaths}; - -/// to process the element [*] -pub struct Wildcard { - _t: std::marker::PhantomData, -} - -impl Wildcard { - pub fn new() -> Self { - Self { - _t: std::marker::PhantomData, - } - } -} - -impl Default for Wildcard { - fn default() -> Self { - Self::new() - } -} - -impl<'a, T> Path<'a> for Wildcard -where - T: JsonLike, -{ - type Data = T; - - fn find(&self, data: JsonPathValue<'a, Self::Data>) -> Vec> { - data.flat_map_slice(|data, pref| data.itre(pref)) - } -} - -/// empty path. Returns incoming data. -pub struct IdentityPath { - _t: std::marker::PhantomData, -} - -impl Default for IdentityPath { - fn default() -> Self { - Self::new() - } -} - -impl IdentityPath { - pub fn new() -> Self { - Self { - _t: std::marker::PhantomData, - } - } -} - -impl<'a, T> Path<'a> for IdentityPath { - type Data = T; - - fn find(&self, data: JsonPathValue<'a, Self::Data>) -> Vec> { - vec![data] - } -} - -pub(crate) struct EmptyPath { - _t: std::marker::PhantomData, -} - -impl<'a, T> Path<'a> for EmptyPath { - type Data = T; - - fn find(&self, _data: JsonPathValue<'a, Self::Data>) -> Vec> { - vec![] - } -} - -/// process $ element -pub struct RootPointer<'a, T> { - root: &'a T, -} - -impl<'a, T> RootPointer<'a, T> { - pub(crate) fn new(root: &'a T) -> RootPointer<'a, T> { - RootPointer { root } - } -} - -impl<'a, T> Path<'a> for RootPointer<'a, T> -where - T: JsonLike, -{ - type Data = T; - - fn find(&self, _data: JsonPathValue<'a, Self::Data>) -> Vec> { - vec![JsonPathValue::from_root(self.root)] - } -} - -/// process object fields like ['key'] or .key -pub struct ObjectField<'a, T> { - key: &'a str, - _t: std::marker::PhantomData, -} - -impl<'a, T> ObjectField<'a, T> { - pub(crate) fn new(key: &'a str) -> ObjectField<'a, T> { - ObjectField { - key, - _t: std::marker::PhantomData, - } - } -} - -impl Clone for ObjectField<'_, T> { - fn clone(&self) -> Self { - ObjectField::new(self.key) - } -} - -impl<'a, T> Path<'a> for ObjectField<'a, T> -where - T: JsonLike, -{ - type Data = T; - - fn find(&self, data: JsonPathValue<'a, Self::Data>) -> Vec> { - let take_field = |v: &'a T| v.get(self.key); - - let res = match data { - Slice(js, p) => take_field(js) - .map(|v| JsonPathValue::new_slice(v, jsp_obj(&p, self.key))) - .unwrap_or_else(|| NoValue), - _ => NoValue, - }; - vec![res] - } -} - -pub enum FnPath { - Size { _t: std::marker::PhantomData }, -} - -impl FnPath { - pub fn new_size() -> Self { - FnPath::Size { - _t: std::marker::PhantomData, - } - } -} - -impl<'a, T> Path<'a> for FnPath -where - T: JsonLike, -{ - type Data = T; - - fn flat_find( - &self, - input: Vec>, - is_search_length: bool, - ) -> Vec> { - // todo rewrite - if JsonPathValue::only_no_value(&input) { - return vec![NoValue]; - } - let res = if is_search_length { - NewValue(T::init_with_usize( - input.iter().filter(|v| v.has_value()).count(), - )) - } else { - match input.first() { - Some(v) => match v { - NewValue(d) => d.array_len(), - Slice(s, _) => s.array_len(), - NoValue => NoValue, - }, - None => NoValue, - } - }; - vec![res] - } - - fn needs_all(&self) -> bool { - true - } -} - -/// the top method of the processing ..* -pub struct DescentWildcard { - _t: std::marker::PhantomData, -} - -impl Default for DescentWildcard { - fn default() -> Self { - Self::new() - } -} - -impl DescentWildcard { - pub fn new() -> Self { - DescentWildcard { - _t: std::marker::PhantomData, - } - } -} - -impl<'a, T> Path<'a> for DescentWildcard -where - T: JsonLike, -{ - type Data = T; - - fn find(&self, data: JsonPathValue<'a, Self::Data>) -> Vec> { - data.map_slice(|data, pref| data.deep_flatten(pref)) - } -} - -/// processes decent object like .. -pub struct DescentObject<'a, T> { - key: &'a str, - _t: std::marker::PhantomData, -} - -impl<'a, T> Path<'a> for DescentObject<'a, T> -where - T: JsonLike, -{ - type Data = T; - - fn find(&self, data: JsonPathValue<'a, Self::Data>) -> Vec> { - data.flat_map_slice(|data, pref| { - let res_col = data.deep_path_by_key(ObjectField::new(self.key), pref.clone()); - if res_col.is_empty() { - vec![NoValue] - } else { - JsonPathValue::map_vec(res_col) - } - }) - } -} - -impl<'a, T> DescentObject<'a, T> { - pub fn new(key: &'a str) -> Self { - DescentObject { - key, - _t: std::marker::PhantomData, - } - } -} - -/// the top method of the processing representing the chain of other operators -pub struct Chain<'a, T> { - chain: Vec>, - is_search_length: bool, -} - -impl<'a, T> Chain<'a, T> -where - T: JsonLike, -{ - pub fn new(chain: Vec>, is_search_length: bool) -> Self { - Chain { - chain, - is_search_length, - } - } - pub fn from(chain: &'a [JsonPath], root: &'a T) -> Self { - let chain_len = chain.len(); - let is_search_length = if chain_len > 2 { - let mut res = false; - // if the result of the slice expected to be a slice, union or filter - - // length should return length of resulted array - // In all other cases, including single index, we should fetch item from resulting array - // and return length of that item - res = match chain.get(chain_len - 1).expect("chain element disappeared") { - JsonPath::Fn(Function::Length) => { - for item in chain.iter() { - match (item, res) { - // if we found union, slice, filter or wildcard - set search to true - ( - JsonPath::Index(JsonPathIndex::UnionIndex(_)) - | JsonPath::Index(JsonPathIndex::UnionKeys(_)) - | JsonPath::Index(JsonPathIndex::Slice(_, _, _)) - | JsonPath::Index(JsonPathIndex::Filter(_)) - | JsonPath::Wildcard, - false, - ) => { - res = true; - } - // if we found a fetching of single index - reset search to false - (JsonPath::Index(JsonPathIndex::Single(_)), true) => { - res = false; - } - (_, _) => {} - } - } - res - } - _ => false, - }; - res - } else { - false - }; - - Chain::new( - chain.iter().map(|p| json_path_instance(p, root)).collect(), - is_search_length, - ) - } -} - -impl<'a, T> Path<'a> for Chain<'a, T> -where - T: JsonLike, -{ - type Data = T; - - fn find(&self, data: JsonPathValue<'a, Self::Data>) -> Vec> { - let mut res = vec![data]; - - for inst in self.chain.iter() { - if inst.needs_all() { - res = inst.flat_find(res, self.is_search_length) - } else { - res = res.into_iter().flat_map(|d| inst.find(d)).collect() - } - } - res - } -} - -#[cfg(test)] -mod tests { - use crate::jp_v; - use crate::parser::macros::{chain, idx}; - use crate::parser::model::{JsonPath, JsonPathIndex}; - use crate::path; - use crate::path::top::{json_path_instance, Function, ObjectField, RootPointer}; - use crate::path::{JsonPathValue, Path}; - use crate::JsonPathValue::NoValue; - use serde_json::json; - use serde_json::Value; - - #[test] - fn object_test() { - let js = json!({"product": {"key":42}}); - let res_income = jp_v!(&js); - - let key = String::from("product"); - let mut field = ObjectField::new(&key); - let js = json!({"key":42}); - assert_eq!( - field.find(res_income.clone()), - vec![jp_v!(&js;".['product']")] - ); - - let key = String::from("fake"); - field.key = &key; - assert_eq!(field.find(res_income), vec![NoValue]); - } - - #[test] - fn root_test() { - let res_income = json!({"product": {"key":42}}); - - let root = RootPointer::::new(&res_income); - - assert_eq!(root.find(jp_v!(&res_income)), jp_v!(&res_income;"$",)) - } - - #[test] - fn path_instance_test() { - let json = json!({"v": {"k":{"f":42,"array":[0,1,2,3,4,5],"object":{"field1":"val1","field2":"val2"}}}}); - let field1 = path!("v"); - let field2 = path!("k"); - let field3 = path!("f"); - let field4 = path!("array"); - let field5 = path!("object"); - - let path_inst = json_path_instance(&path!($), &json); - assert_eq!(path_inst.find(jp_v!(&json)), jp_v!(&json;"$",)); - - let path_inst = json_path_instance(&field1, &json); - let exp_json = - json!({"k":{"f":42,"array":[0,1,2,3,4,5],"object":{"field1":"val1","field2":"val2"}}}); - assert_eq!(path_inst.find(jp_v!(&json)), jp_v!(&exp_json;".['v']",)); - - let chain = chain!(path!($), field1.clone(), field2.clone(), field3); - - let path_inst = json_path_instance(&chain, &json); - let exp_json = json!(42); - assert_eq!( - path_inst.find(jp_v!(&json)), - jp_v!(&exp_json;"$.['v'].['k'].['f']",) - ); - - let chain = chain!( - path!($), - field1.clone(), - field2.clone(), - field4.clone(), - path!(idx!(3)) - ); - let path_inst = json_path_instance(&chain, &json); - let exp_json = json!(3); - assert_eq!( - path_inst.find(jp_v!(&json)), - jp_v!(&exp_json;"$.['v'].['k'].['array'][3]",) - ); - - let index = idx!([1;-1;2]); - let chain = chain!( - path!($), - field1.clone(), - field2.clone(), - field4.clone(), - path!(index) - ); - let path_inst = json_path_instance(&chain, &json); - let one = json!(1); - let tree = json!(3); - assert_eq!( - path_inst.find(jp_v!(&json)), - jp_v!(&one;"$.['v'].['k'].['array'][1]", &tree;"$.['v'].['k'].['array'][3]") - ); - - let union = idx!(idx 1,2 ); - let chain = chain!( - path!($), - field1.clone(), - field2.clone(), - field4, - path!(union) - ); - let path_inst = json_path_instance(&chain, &json); - let tree = json!(1); - let two = json!(2); - assert_eq!( - path_inst.find(jp_v!(&json)), - jp_v!(&tree;"$.['v'].['k'].['array'][1]",&two;"$.['v'].['k'].['array'][2]") - ); - - let union = idx!("field1", "field2"); - let chain = chain!(path!($), field1.clone(), field2, field5, path!(union)); - let path_inst = json_path_instance(&chain, &json); - let one = json!("val1"); - let two = json!("val2"); - assert_eq!( - path_inst.find(jp_v!(&json)), - jp_v!( - &one;"$.['v'].['k'].['object'].['field1']", - &two;"$.['v'].['k'].['object'].['field2']") - ); - } - #[test] - fn path_descent_arr_test() { - let json = json!([{"a":1}]); - let chain = chain!(path!($), path!(.."a")); - let path_inst = json_path_instance(&chain, &json); - - let one = json!(1); - let expected_res = jp_v!(&one;"$[0].['a']",); - assert_eq!(path_inst.find(jp_v!(&json)), expected_res) - } - #[test] - fn deep_path_test() { - use crate::path::JsonLike; - let value = json!([1]); - let r = value.deep_flatten("".to_string()); - assert_eq!(r, vec![(&json!(1), "[0]".to_string())]) - } - - #[test] - fn path_descent_w_array_test() { - let json = json!( - { - "key1": [1] - }); - let chain = chain!(path!($), path!(..*)); - let path_inst = json_path_instance(&chain, &json); - - let arr = json!([1]); - let one = json!(1); - - let expected_res = jp_v!(&arr;"$.['key1']",&one;"$.['key1'][0]"); - assert_eq!(path_inst.find(jp_v!(&json)), expected_res) - } - #[test] - fn path_descent_w_nested_array_test() { - let json = json!( - { - "key2" : [{"a":1},{}] - }); - let chain = chain!(path!($), path!(..*)); - let path_inst = json_path_instance(&chain, &json); - - let arr2 = json!([{"a": 1},{}]); - let obj = json!({"a": 1}); - let empty = json!({}); - - let one = json!(1); - - let expected_res = jp_v!( - &arr2;"$.['key2']", - &obj;"$.['key2'][0]", - &one;"$.['key2'][0].['a']", - ∅"$.['key2'][1]" - ); - assert_eq!(path_inst.find(jp_v!(&json)), expected_res) - } - - #[test] - fn path_descent_w_test() { - let json = json!( - { - "key1": [1], - "key2": "key", - "key3": { - "key1": "key1", - "key2": { - "key1": { - "key1": 0 - } - } - } - }); - let chain = chain!(path!($), path!(..*)); - let path_inst = json_path_instance(&chain, &json); - - let key1 = json!([1]); - let one = json!(1); - let zero = json!(0); - let key = json!("key"); - let key1_s = json!("key1"); - - let key_3 = json!( { - "key1": "key1", - "key2": { - "key1": { - "key1": 0 - } - } - }); - let key_sec = json!( { - "key1": { - "key1": 0 - } - }); - let key_th = json!( { - "key1": 0 - }); - - let expected_res = vec![ - jp_v!(&key1;"$.['key1']"), - jp_v!(&one;"$.['key1'][0]"), - jp_v!(&key;"$.['key2']"), - jp_v!(&key_3;"$.['key3']"), - jp_v!(&key1_s;"$.['key3'].['key1']"), - jp_v!(&key_sec;"$.['key3'].['key2']"), - jp_v!(&key_th;"$.['key3'].['key2'].['key1']"), - jp_v!(&zero;"$.['key3'].['key2'].['key1'].['key1']"), - ]; - assert_eq!(path_inst.find(jp_v!(&json)), expected_res) - } - #[test] - fn path_descent_test() { - let json = json!( - { - "key1": [1,2,3], - "key2": "key", - "key3": { - "key1": "key1", - "key2": { - "key1": { - "key1": 0 - } - } - } - }); - let chain = chain!(path!($), path!(.."key1")); - let path_inst = json_path_instance(&chain, &json); - - let res1 = json!([1, 2, 3]); - let res2 = json!("key1"); - let res3 = json!({"key1":0}); - let res4 = json!(0); - - let expected_res = jp_v!( - &res1;"$.['key1']", - &res2;"$.['key3'].['key1']", - &res3;"$.['key3'].['key2'].['key1']", - &res4;"$.['key3'].['key2'].['key1'].['key1']", - ); - assert_eq!(path_inst.find(jp_v!(&json)), expected_res) - } - - #[test] - fn wildcard_test() { - let json = json!({ - "key1": [1,2,3], - "key2": "key", - "key3": {} - }); - - let chain = chain!(path!($), path!(*)); - let path_inst = json_path_instance(&chain, &json); - - let res1 = json!([1, 2, 3]); - let res2 = json!("key"); - let res3 = json!({}); - - let expected_res = jp_v!(&res1;"$.['key1']", &res2;"$.['key2']", &res3;"$.['key3']"); - assert_eq!(path_inst.find(jp_v!(&json)), expected_res) - } - - #[test] - fn length_test() { - let json = json!({ - "key1": [1,2,3], - "key2": "key", - "key3": {} - }); - - let chain = chain!(path!($), path!(*), JsonPath::Fn(Function::Length)); - let path_inst = json_path_instance(&chain, &json); - - assert_eq!( - path_inst.flat_find(vec![jp_v!(&json)], true), - vec![jp_v!(json!(3))] - ); - - let chain = chain!(path!($), path!("key1"), JsonPath::Fn(Function::Length)); - let path_inst = json_path_instance(&chain, &json); - assert_eq!( - path_inst.flat_find(vec![jp_v!(&json)], false), - vec![jp_v!(json!(3))] - ); - } -} diff --git a/src/query.rs b/src/query.rs new file mode 100644 index 0000000..498cb1b --- /dev/null +++ b/src/query.rs @@ -0,0 +1,810 @@ +mod atom; +mod comparable; +mod comparison; +mod filter; +mod jp_query; +pub mod queryable; +mod segment; +mod selector; +pub mod state; +mod test; +mod test_function; + +use crate::parser::errors::JsonPathError; +use crate::parser::parse_json_path; +use crate::query::queryable::Queryable; +use crate::query::state::{Data, Pointer}; +use state::State; +use std::borrow::Cow; + +/// A type that can be queried with JSONPath, typically string +pub type QueryPath = String; + +/// A type that can be queried with JSONPath, typically Result +pub type Queried = Result; + +/// Main internal trait to implement the logic of processing jsonpath. +pub trait Query { + fn process<'a, T: Queryable>(&self, state: State<'a, T>) -> State<'a, T>; +} + +/// The resulting type of JSONPath query. +/// It can either be a value or a reference to a value with its path. +#[derive(Debug, Clone, PartialEq)] +pub struct QueryRef<'a, T: Queryable>(&'a T, QueryPath); + +impl<'a, T: Queryable> From<(&'a T, QueryPath)> for QueryRef<'a, T> { + fn from((inner, path): (&'a T, QueryPath)) -> Self { + QueryRef(inner, path) + } +} +impl<'a, T: Queryable> From<(&'a T, &str)> for QueryRef<'a, T> { + fn from((inner, path): (&'a T, &str)) -> Self { + QueryRef(inner, path.to_string()) + } +} + +impl<'a, T: Queryable> QueryRef<'a, T> { + pub fn val(self) -> &'a T { + self.0 + } + pub fn path(self) -> QueryPath { + self.1 + } +} + +impl<'a, T: Queryable> From> for QueryRef<'a, T> { + fn from(pointer: Pointer<'a, T>) -> Self { + QueryRef(pointer.inner, pointer.path) + } +} + +/// The main function to process a JSONPath query. +/// It takes a path and a value, and returns a vector of `QueryResult` thus values + paths. +pub fn js_path<'a, T: Queryable>(path: &str, value: &'a T) -> Queried>> { + match parse_json_path(path)?.process(State::root(value)).data { + Data::Ref(p) => Ok(vec![p.into()]), + Data::Refs(refs) => Ok(refs.into_iter().map(Into::into).collect()), + Data::Value(v) => Err(v.into()), + Data::Nothing => Ok(vec![]), + } +} + +/// A convenience function to process a JSONPath query and return a vector of values, omitting the path. +pub fn js_path_vals<'a, T: Queryable>(path: &str, value: &'a T) -> Queried> { + Ok(js_path(path, value)? + .into_iter() + .map(|r| r.val()) + .collect::>()) +} + +/// A convenience function to process a JSONPath query and return a vector of paths, omitting the values. +pub fn js_path_path(path: &str, value: &T) -> Queried> { + Ok(js_path(path, value)? + .into_iter() + .map(|r| r.path()) + .collect::>()) +} + +#[cfg(test)] +mod tests { + use crate::parser::errors::JsonPathError; + use crate::parser::Parsed; + use crate::query::queryable::Queryable; + use crate::query::{js_path, Queried, QueryRef}; + use crate::JsonPath; + use serde_json::{json, Value}; + + fn test<'a, R>(json: &'a str, path: &str, expected: Vec) -> Parsed<()> + where + R: Into>, + { + let json: Value = serde_json::from_str(json).map_err(|v| JsonPathError::NoRulePath)?; + let expected: Vec> = expected.into_iter().map(|v| v.into()).collect(); + assert_eq!(json.query_with_path(path)?, expected); + + Ok(()) + } + + fn template_json<'a>() -> &'a str { + r#" {"store": { "book": [ + { + "category": "reference", + "author": "Nigel Rees", + "title": "Sayings of the Century", + "price": 8.95 + }, + { + "category": "fiction", + "author": "Evelyn Waugh", + "title": "Sword of Honour", + "price": 12.99 + }, + { + "category": "fiction", + "author": "Herman Melville", + "title": "Moby Dick", + "isbn": "0-553-21311-3", + "price": 8.99 + }, + { + "category": "fiction", + "author": "J. R. R. Tolkien", + "title": "The Lord of the Rings", + "isbn": "0-395-19395-8", + "price": 22.99 + } + ], + "bicycle": { + "color": "red", + "price": 19.95 + } + }, + "array":[0,1,2,3,4,5,6,7,8,9], + "orders":[ + { + "ref":[1,2,3], + "id":1, + "filled": true + }, + { + "ref":[4,5,6], + "id":2, + "filled": false + }, + { + "ref":[7,8,9], + "id":3, + "filled": null + } + ], + "expensive": 10 }"# + } + fn update_by_path_test() -> Queried<()> { + let mut json = json!([ + {"verb": "RUN","distance":[1]}, + {"verb": "TEST"}, + {"verb": "DO NOT RUN"} + ]); + + let path = json.query_only_path("$.[?(@.verb == 'RUN')]")?; + let elem = path.first().cloned().unwrap_or_default(); + + if let Some(v) = json + .reference_mut(elem) + .and_then(|v| v.as_object_mut()) + .and_then(|v| v.get_mut("distance")) + .and_then(|v| v.as_array_mut()) + { + v.push(json!(2)) + } + + assert_eq!( + json, + json!([ + {"verb": "RUN","distance":[1,2]}, + {"verb": "TEST"}, + {"verb": "DO NOT RUN"} + ]) + ); + + Ok(()) + } + + #[test] + fn simple_test() { + let j1 = json!(2); + let _ = test("[1,2,3]", "$[1]", vec![(&j1, "$[1]".to_string())]); + } + + #[test] + fn root_test() { + let js = serde_json::from_str(template_json()).unwrap(); + let _ = test(template_json(), "$", vec![(&js, "$")]); + } + + #[test] + fn descent_test() { + let v1 = json!("reference"); + let v2 = json!("fiction"); + let _ = test( + template_json(), + "$..category", + vec![ + (&v1, "$['store']['book'][0]['category']"), + (&v2, "$['store']['book'][1]['category']"), + (&v2, "$['store']['book'][2]['category']"), + (&v2, "$['store']['book'][3]['category']"), + ], + ); + let js1 = json!(19.95); + let js2 = json!(8.95); + let js3 = json!(12.99); + let js4 = json!(8.99); + let js5 = json!(22.99); + let _ = test( + template_json(), + "$.store..price", + vec![ + (&js1, "$['store']['bicycle']['price']"), + (&js2, "$['store']['book'][0]['price']"), + (&js3, "$['store']['book'][1]['price']"), + (&js4, "$['store']['book'][2]['price']"), + (&js5, "$['store']['book'][3]['price']"), + ], + ); + let js1 = json!("Nigel Rees"); + let js2 = json!("Evelyn Waugh"); + let js3 = json!("Herman Melville"); + let js4 = json!("J. R. R. Tolkien"); + let _ = test( + template_json(), + "$..author", + vec![ + (&js1, "$['store']['book'][0]['author']"), + (&js2, "$['store']['book'][1]['author']"), + (&js3, "$['store']['book'][2]['author']"), + (&js4, "$['store']['book'][3]['author']"), + ], + ); + } + + #[test] + fn wildcard_test() { + let js1 = json!("reference"); + let js2 = json!("fiction"); + let _ = test( + template_json(), + "$..book.[*].category", + vec![ + (&js1, "$['store']['book'][0].['category']"), + (&js2, "$['store']['book'][1].['category']"), + (&js2, "$['store']['book'][2].['category']"), + (&js2, "$['store']['book'][3].['category']"), + ], + ); + let js1 = json!("Nigel Rees"); + let js2 = json!("Evelyn Waugh"); + let js3 = json!("Herman Melville"); + let js4 = json!("J. R. R. Tolkien"); + let _ = test( + template_json(), + "$.store.book[*].author", + vec![ + (&js1, "$['store']['book'][0]['author']"), + (&js2, "$['store']['book'][1]['author']"), + (&js3, "$['store']['book'][2]['author']"), + (&js4, "$['store']['book'][3]['author']"), + ], + ); + } + + #[test] + fn descendent_wildcard_test() { + let js1 = json!("0-553-21311-3"); + let js2 = json!("0-395-19395-8"); + let _ = test( + template_json(), + "$..*.[?@].isbn", + vec![ + (&js1, "$['store']['book'][2]['isbn']"), + (&js2, "$['store']['book'][3]['isbn']"), + ], + ); + } + + #[test] + fn field_test() { + let value = json!({"active":1}); + let _ = test( + r#"{"field":{"field":[{"active":1},{"passive":1}]}}"#, + "$.field.field[?@.active]", + vec![(&value, "$['field']['field'][0]")], + ); + } + + #[test] + fn index_index_test() { + let value = json!("0-553-21311-3"); + let _ = test( + template_json(), + "$..book[2].isbn", + vec![(&value, "$['store']['book'][2]['isbn']")], + ); + } + + #[test] + fn index_unit_index_test() { + let value = json!("0-553-21311-3"); + let _ = test( + template_json(), + "$..book[2,4].isbn", + vec![(&value, "$['store']['book'][2]['isbn']")], + ); + let value1 = json!("0-395-19395-8"); + let _ = test( + template_json(), + "$..book[2,3].isbn", + vec![ + (&value, "$['store']['book'][2]['isbn']"), + (&value1, "$['store']['book'][3]['isbn']"), + ], + ); + } + + #[test] + fn index_unit_keys_test() { + let js1 = json!("Moby Dick"); + let js2 = json!(8.99); + let js3 = json!("The Lord of the Rings"); + let js4 = json!(22.99); + let _ = test( + template_json(), + "$..book[2,3]['title','price']", + vec![ + (&js1, "$['store']['book'][2]['title']"), + (&js3, "$['store']['book'][3]['title']"), + (&js2, "$['store']['book'][2]['price']"), + (&js4, "$['store']['book'][3]['price']"), + ], + ); + } + + #[test] + fn index_slice_test() -> Parsed<()> { + let i0 = "$['array'][0]"; + let i1 = "$['array'][1]"; + let i2 = "$['array'][2]"; + let i3 = "$['array'][3]"; + let i4 = "$['array'][4]"; + let i5 = "$['array'][5]"; + let i6 = "$['array'][6]"; + let i7 = "$['array'][7]"; + let i8 = "$['array'][8]"; + let i9 = "$['array'][9]"; + + let j0 = json!(0); + let j1 = json!(1); + let j2 = json!(2); + let j3 = json!(3); + let j4 = json!(4); + let j5 = json!(5); + let j6 = json!(6); + let j7 = json!(7); + let j8 = json!(8); + let j9 = json!(9); + test( + template_json(), + "$.array[:]", + vec![ + (&j0, i0), + (&j1, i1), + (&j2, i2), + (&j3, i3), + (&j4, i4), + (&j5, i5), + (&j6, i6), + (&j7, i7), + (&j8, i8), + (&j9, i9), + ], + )?; + test( + template_json(), + "$.array[1:4:2]", + vec![(&j1, i1), (&j3, i3)], + )?; + test( + template_json(), + "$.array[::3]", + vec![(&j0, i0), (&j3, i3), (&j6, i6), (&j9, i9)], + )?; + test(template_json(), "$.array[-1:]", vec![(&j9, i9)])?; + test(template_json(), "$.array[-2:-1]", vec![(&j8, i8)])?; + + Ok(()) + } + + #[test] + fn index_filter_test() -> Parsed<()> { + let moby = json!("Moby Dick"); + let rings = json!("The Lord of the Rings"); + test( + template_json(), + "$..book[?@.isbn].title", + vec![ + (&moby, "$['store']['book'][2]['title']"), + (&rings, "$['store']['book'][3]['title']"), + ], + )?; + let sword = json!("Sword of Honour"); + test( + template_json(), + "$..book[?(@.price != 8.95)].title", + vec![ + (&sword, "$['store']['book'][1]['title']"), + (&moby, "$['store']['book'][2]['title']"), + (&rings, "$['store']['book'][3]['title']"), + ], + )?; + let sayings = json!("Sayings of the Century"); + test( + template_json(), + "$..book[?(@.price == 8.95)].title", + vec![(&sayings, "$['store']['book'][0]['title']")], + )?; + + let js12 = json!(12.99); + let js899 = json!(8.99); + let js2299 = json!(22.99); + test( + template_json(), + "$..book[?@.price >= 8.99].price", + vec![ + (&js12, "$['store']['book'][1]['price']"), + (&js899, "$['store']['book'][2]['price']"), + (&js2299, "$['store']['book'][3]['price']"), + ], + )?; + + test( + template_json(), + "$..book[?(@.price >= $.expensive)].price", + vec![ + (&js12, "$['store']['book'][1]['price']"), + (&js2299, "$['store']['book'][3]['price']"), + ], + )?; + Ok(()) + } + + #[test] + fn union_quotes() -> Queried<()> { + let json = json!({ + "a": "ab", + "b": "bc" + }); + + let vec = js_path("$['a',\r'b']", &json)?; + + assert_eq!( + vec, + vec![ + (&json!("ab"), "$['a']".to_string()).into(), + (&json!("bc"), "$['b']".to_string()).into(), + ] + ); + + Ok(()) + } + + #[test] + fn space_between_selectors() -> Queried<()> { + let json = json!({ + "a": { + "b": "ab" + } + }); + + let vec = js_path("$['a'] \r['b']", &json)?; + + assert_eq!(vec, vec![(&json!("ab"), "$['a']['b']".to_string()).into(),]); + + Ok(()) + } + #[test] + fn space_in_search() -> Queried<()> { + let json = json!(["foo", "123"]); + + let vec = js_path("$[?search(@\n,'[a-z]+')]", &json)?; + + assert_eq!(vec, vec![(&json!("foo"), "$[0]".to_string()).into(),]); + + Ok(()) + } + #[test] + fn filter_key() -> Queried<()> { + let json = json!([ + { + "a": "b", + "d": "e" + }, + { + "a": 1, + "d": "f" + } + ]); + + let vec = js_path("$[?@.a!=\"b\"]", &json)?; + + assert_eq!( + vec, + vec![(&json!({"a":1, "d":"f"}), "$[1]".to_string()).into(),] + ); + + Ok(()) + } + + #[test] + fn regex_key() -> Queried<()> { + let json = json!({ + "regex": "b.?b", + "values": [ + "abc", + "bcd", + "bab", + "bba", + "bbab", + "b", + true, + [], + {} + ] + }); + + let vec = js_path("$.values[?match(@, $.regex)]", &json)?; + + assert_eq!( + vec, + vec![(&json!("bab"), "$['values'][2]".to_string()).into(),] + ); + + Ok(()) + } + #[test] + fn name_sel() -> Queried<()> { + let json = json!({ + "/": "A" + }); + + let vec = js_path("$['\\/']", &json)?; + + assert_eq!(vec, vec![(&json!("A"), "$['\\/']".to_string()).into(),]); + + Ok(()) + } + #[test] + fn unicode_fns() -> Queried<()> { + let json = json!(["ж", "Ж", "1", "жЖ", true, [], {}]); + + let vec = js_path("$[?match(@, '\\\\p{Lu}')]", &json)?; + + assert_eq!(vec, vec![(&json!("Ж"), "$[1]".to_string()).into(),]); + + Ok(()) + } + #[test] + fn fn_res_can_not_compare() -> Queried<()> { + let json = json!({}); + + let vec = js_path("$[?match(@.a, 'a.*')==true]", &json); + + assert!(vec.is_err()); + + Ok(()) + } + #[test] + fn too_small() -> Queried<()> { + let json = json!({}); + + let vec = js_path("$[-9007199254740992]", &json); + + assert!(vec.is_err()); + + Ok(()) + } + #[test] + fn filter_data() -> Queried<()> { + let json = json!({ + "a": 1, + "b": 2, + "c": 3 + }); + + let vec: Vec = json.query_only_path("$[?@<3]")?.into_iter().collect(); + + assert_eq!(vec, vec!["$['a']".to_string(), "$['b']".to_string()]); + + Ok(()) + } + #[test] + fn exp_no_error() -> Queried<()> { + let json = json!([ + { + "a": 100, + "d": "e" + }, + { + "a": 100.1, + "d": "f" + }, + { + "a": "100", + "d": "g" + } + ]); + + let vec: Vec<&Value> = json.query("$[?@.a==1E2]")?; + assert_eq!(vec, vec![&json!({"a":100, "d":"e"})]); + + Ok(()) + } + #[test] + fn single_quote() -> Queried<()> { + let json = json!({ + "a'": "A", + "b": "B" + }); + + let vec = js_path("$[\"a'\"]", &json)?; + assert_eq!(vec, vec![(&json!("A"), "$['\"a\'\"']".to_string()).into(),]); + + Ok(()) + } + #[test] + fn union() -> Queried<()> { + let json = json!([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]); + + let vec: Vec> = json.query_with_path("$[1,5:7]")?; + assert_eq!( + vec, + vec![ + (&json!(1), "$[1]".to_string()).into(), + (&json!(5), "$[5]".to_string()).into(), + (&json!(6), "$[6]".to_string()).into(), + ] + ); + + Ok(()) + } + + #[test] + fn basic_descendent() -> Queried<()> { + let json = json!({ + "o": [ + 0, + 1, + [ + 2, + 3 + ] + ] + }); + + let vec = js_path("$..[1]", &json)?; + assert_eq!( + vec, + vec![ + (&json!(1), "$['o'][1]".to_string()).into(), + (&json!(3), "$['o'][2][1]".to_string()).into(), + ] + ); + + Ok(()) + } + #[test] + fn filter_absent() -> Queried<()> { + let json = json!([ + { + "list": [ + 1 + ] + } + ]); + + let vec = js_path("$[?@.absent==@.list[9]]", &json)?; + assert_eq!( + vec, + vec![(&json!({"list": [1]}), "$[0]".to_string()).into(),] + ); + + Ok(()) + } + + #[test] + fn filter_star() -> Queried<()> { + let json = json!([1,[],[2],{},{"a": 3}]); + + let vec = json.query_with_path("$[?@.*]")?; + assert_eq!( + vec, + vec![ + (&json!([2]), "$[2]".to_string()).into(), + (&json!({"a": 3}), "$[4]".to_string()).into(), + ] + ); + + Ok(()) + } + + #[test] + fn space_test() -> Queried<()> { + let json = json!({ " ": "A"}); + + let vec = json.query_with_path("$[' ']")?; + assert_eq!(vec, vec![(&json!("A"), "$[\' \']".to_string()).into(),]); + + Ok(()) + } + #[test] + fn neg_idx() -> Queried<()> { + let json = json!(["first", "second"]); + + let vec = json.query_with_path("$[-2]")?; + assert_eq!(vec, vec![(&json!("first"), "$[0]".to_string()).into(),]); + + Ok(()) + } + + #[test] + fn filter_slice() -> Queried<()> { + let json = json!([ + 1, + [], + [ + 2 + ], + [ + 2, + 3, + 4 + ], + {}, + { + "a": 3 + } + ]); + + let vec = json.query_with_path("$[?@[0:2]]")?; + assert_eq!( + vec, + vec![ + (&json!([2]), "$[2]").into(), + (&json!([2, 3, 4]), "$[3]").into(), + ] + ); + + Ok(()) + } + + #[test] + fn surr_pairs() -> Queried<()> { + let json = json!({ + "𝄞": "A" + }); + let vec = json.query_with_path("$['𝄞']")?; + assert_eq!(vec, vec![(&json!("A"), "$['𝄞']".to_string()).into()]); + + Ok(()) + } + #[test] + fn tab_key() -> Queried<()> { + let json = json!({ + "\\t": "A" + }); + let vec = json.query_with_path("$['\\t']")?; + assert_eq!(vec, vec![(&json!("A"), "$['\\t']".to_string()).into()]); + + Ok(()) + } + #[test] + fn escaped_up_hex() -> Queried<()> { + let json = json!({ + "☺": "A" + }); + let vec = json.query_with_path("$['☺']")?; + assert_eq!(vec, vec![(&json!("A"), "$['☺']".to_string()).into()]); + + Ok(()) + } + #[test] + fn carr_return() -> Queried<()> { + let json = json!({ + "\\r": "A" + }); + let vec = json.query_with_path("$['\\r']")?; + assert_eq!(vec, vec![(&json!("A"), "$['\\r']".to_string()).into()]); + + Ok(()) + } +} diff --git a/src/query/atom.rs b/src/query/atom.rs new file mode 100644 index 0000000..f51b03e --- /dev/null +++ b/src/query/atom.rs @@ -0,0 +1,122 @@ +use crate::parser::model::FilterAtom; +use crate::query::queryable::Queryable; +use crate::query::state::{Data, State}; +use crate::query::Query; + +impl Query for FilterAtom { + fn process<'a, T: Queryable>(&self, state: State<'a, T>) -> State<'a, T> { + match self { + FilterAtom::Filter { expr, not } => { + let bool_res = expr.process(state); + if *not { + invert_bool(bool_res) + } else { + bool_res + } + } + FilterAtom::Test { expr, not } => { + let new_state = |b| State::bool(b, state.root); + let res = expr.process(state.clone()); + if expr.is_res_bool() { + if *not { + invert_bool(res) + } else { + res + } + } else { + let struct_check = |s: &T| { + if let Some(arr) = s.as_array() { + !arr.is_empty() + } else if let Some(obj) = s.as_object() { + !obj.is_empty() + } else if let Some(str) = s.as_str() { + !str.is_empty() + } else { + true + } + }; + + let struct_presented = match res.data { + Data::Ref(v) => struct_check(v.inner), + Data::Refs(e) if e.is_empty() => false, + Data::Refs(elems) => elems.iter().map(|v| v.inner).all(struct_check), + _ => false, + }; + + if struct_presented { + new_state(!*not) + } else { + new_state(*not) + } + } + } + FilterAtom::Comparison(cmp) => cmp.process(state), + } + } +} + +fn invert_bool(state: State) -> State { + let root = state.root; + State::bool( + !state.ok_val().and_then(|v| v.as_bool()).unwrap_or_default(), + root, + ) +} + +#[cfg(test)] +mod tests { + use crate::parser::model::Comparable; + use crate::parser::model::Literal; + use crate::parser::model::SingularQuery; + use crate::parser::model::SingularQuerySegment; + use crate::parser::model::{Comparison, FilterAtom}; + use crate::q_segment; + use crate::query::queryable::Queryable; + use crate::query::state::State; + use crate::query::Query; + use crate::{atom, comparable, lit}; + use crate::{cmp, singular_query}; + use crate::{filter_, q_segments}; + use serde_json::json; + + #[test] + fn test_comparison() { + let json = json!({"i": 1}); + let atom = atom!(comparable!(lit!(i 1)), ">=", comparable!(lit!(i 1))); + let state = State::root(&json); + let res = atom.process(state); + assert_eq!(res.ok_val().and_then(|v| v.as_bool()), Some(true)); + } + + #[test] + fn test_not_filter_atom() { + let json = json!({"a": 1 , "b": 2}); + let state = State::root(&json); + + let f1 = filter_!(atom!( + comparable!(> SingularQuery::Current(vec![])), + ">", + comparable!(lit!(i 2)) + )); + let f2 = filter_!(atom!( + comparable!(> singular_query!(b)), + "!=", + comparable!(> singular_query!(a)) + )); + + let atom_or = atom!(!filter_!(or f1.clone(), f2.clone())); + let atom_and = atom!(!filter_!(and f1, f2)); + + assert_eq!( + atom_or + .process(state.clone()) + .ok_val() + .and_then(|v| v.as_bool()), + Some(true) + ); + assert_eq!( + atom_and.process(state).ok_val().and_then(|v| v.as_bool()), + Some(true) + ); + } +} diff --git a/src/query/comparable.rs b/src/query/comparable.rs new file mode 100644 index 0000000..a9a06e5 --- /dev/null +++ b/src/query/comparable.rs @@ -0,0 +1,165 @@ +use crate::parser::model::{Comparable, Literal, SingularQuery, SingularQuerySegment}; +use crate::query::queryable::Queryable; +use crate::query::selector::{process_index, process_key}; +use crate::query::state::{Data, State}; +use crate::query::Query; + +impl Query for Comparable { + fn process<'a, T: Queryable>(&self, step: State<'a, T>) -> State<'a, T> { + match self { + Comparable::Literal(lit) => lit.process(step), + Comparable::Function(tf) => tf.process(step), + Comparable::SingularQuery(query) => query.process(step), + } + } +} + +impl Query for Literal { + fn process<'a, T: Queryable>(&self, state: State<'a, T>) -> State<'a, T> { + let val = match self { + Literal::Int(v) => (*v).into(), + Literal::Float(v) => (*v).into(), + Literal::String(v) => v.as_str().into(), + Literal::Bool(v) => (*v).into(), + Literal::Null => T::null(), + }; + + State::data(state.root, Data::Value(val)) + } +} + +impl Query for SingularQuery { + fn process<'a, T: Queryable>(&self, step: State<'a, T>) -> State<'a, T> { + match self { + SingularQuery::Current(segments) => segments.process(step), + SingularQuery::Root(segments) => segments.process(step.shift_to_root()), + } + } +} + +impl Query for SingularQuerySegment { + fn process<'a, T: Queryable>(&self, step: State<'a, T>) -> State<'a, T> { + match self { + SingularQuerySegment::Index(idx) => step.flat_map(|d| process_index(d, idx)), + SingularQuerySegment::Name(key) => step.flat_map(|d| process_key(d, key)), + } + } +} + +impl Query for Vec { + fn process<'a, T: Queryable>(&self, state: State<'a, T>) -> State<'a, T> { + self.iter() + .fold(state, |next, segment| segment.process(next)) + } +} + +#[cfg(test)] +mod tests { + use crate::parser::model::{Comparable, Literal, SingularQuery, SingularQuerySegment}; + use crate::query::state::{Data, Pointer, State}; + use crate::query::Query; + use serde_json::json; + + #[test] + fn singular_query() { + let value = json!({ + "result": [ + { + "message": "Hello, Emmy! Your order number is: #100", + "phoneNumber": "255-301-9429", + "phoneVariation": "+90 398 588 10 73", + "status": "active", + "name": { + "first": "Blaise", + "middle": "Kyle", + "last": "Fadel" + } + } + ] + }); + + let query = SingularQuery::Current(vec![ + SingularQuerySegment::Name("result".to_string()), + SingularQuerySegment::Index(0), + SingularQuerySegment::Name("name".to_string()), + SingularQuerySegment::Name("first".to_string()), + ]); + + let state = State::root(&value); + + let result = query.process(state); + assert_eq!( + result.ok_ref(), + Some(vec![Pointer::new( + &json!("Blaise"), + "$['result'][0]['name']['first']".to_string() + )]) + ); + } + + #[test] + fn singular_query_root() { + let value = json!({ + "result": [ + { + "message": "Hello, Emmy! Your order number is: #100", + "phoneNumber": "255-301-9429", + "phoneVariation": "+90 398 588 10 73", + "status": "active", + "name": { + "first": "Blaise", + "middle": "Kyle", + "last": "Fadel" + } + } + ] + }); + + let query = SingularQuery::Root(vec![ + SingularQuerySegment::Name("result".to_string()), + SingularQuerySegment::Index(0), + SingularQuerySegment::Name("name".to_string()), + SingularQuerySegment::Name("first".to_string()), + ]); + + let state = State::data( + &value, + Data::new_ref(Pointer::new(&value, "$.name".to_string())), + ); + + let result = query.process(state); + assert_eq!( + result.ok_ref(), + Some(vec![Pointer::new( + &json!("Blaise"), + "$['result'][0]['name']['first']".to_string() + )]) + ); + } + + #[test] + fn literal() { + let value = json!({ + "result": [ + { + "message": "Hello, Emmy! Your order number is: #100", + "phoneNumber": "255-301-9429", + "phoneVariation": "+90 398 588 10 73", + "status": "active", + "name": { + "first": "Blaise", + "middle": "Kyle", + "last": "Fadel" + } + } + ] + }); + + let query = Comparable::Literal(Literal::String("Hello".to_string())); + + let state = State::root(&value); + + let result = query.process(state); + assert_eq!(result.ok_val(), Some(json!("Hello"))); + } +} diff --git a/src/query/comparison.rs b/src/query/comparison.rs new file mode 100644 index 0000000..9b0e98e --- /dev/null +++ b/src/query/comparison.rs @@ -0,0 +1,174 @@ +use crate::parser::model::{Comparable, Comparison, Literal, SingularQuery, SingularQuerySegment}; +use crate::query::queryable::Queryable; +use crate::query::state::{Data, Pointer, State}; +use crate::query::Query; + +impl Query for Comparison { + fn process<'a, T: Queryable>(&self, state: State<'a, T>) -> State<'a, T> { + let root = state.root; + let (lhs, rhs) = self.vals(); + let lhs = lhs.process(state.clone()); + let rhs = rhs.process(state); + match self { + Comparison::Eq(..) => State::bool(eq(lhs, rhs), root), + Comparison::Ne(..) => State::bool(!eq(lhs, rhs), root), + Comparison::Gt(..) => State::bool(lt(rhs, lhs), root), + Comparison::Gte(..) => State::bool(lt(rhs.clone(), lhs.clone()) || eq(lhs, rhs), root), + Comparison::Lt(..) => State::bool(lt(lhs, rhs), root), + Comparison::Lte(..) => State::bool(lt(lhs.clone(), rhs.clone()) || eq(lhs, rhs), root), + } + } +} + +fn lt<'a, T: Queryable>(lhs: State<'a, T>, rhs: State<'a, T>) -> bool { + let cmp = |lhs: &T, rhs: &T| { + let lhs_f64 = lhs.as_f64().or_else(|| lhs.as_i64().map(|v| v as f64)); + let rhs_f64 = rhs.as_f64().or_else(|| rhs.as_i64().map(|v| v as f64)); + if let (Some(lhs_num), Some(rhs_num)) = (lhs_f64, rhs_f64) { + lhs_num < rhs_num + } else if let (Some(lhs), Some(rhs)) = (lhs.as_str(), rhs.as_str()) { + lhs < rhs + } else { + false + } + }; + + match (lhs.data, rhs.data) { + (Data::Value(lhs), Data::Value(rhs)) => cmp(&lhs, &rhs), + (Data::Value(v), Data::Ref(p)) => cmp(&v, p.inner), + (Data::Ref(p), Data::Value(v)) => cmp(p.inner, &v), + (Data::Ref(lhs), Data::Ref(rhs)) => cmp(lhs.inner, rhs.inner), + _ => false, + } +} + +fn eq<'a, T: Queryable>(lhs_state: State<'a, T>, rhs_state: State<'a, T>) -> bool { + match (lhs_state.data, rhs_state.data) { + (Data::Value(lhs), Data::Value(rhs)) => eq_json(&lhs, &rhs), + (Data::Value(v), Data::Ref(p)) => eq_json(&v, p.inner), + (Data::Ref(p), Data::Value(v)) => eq_json(&v, p.inner), + (Data::Ref(lhs), Data::Ref(rhs)) => eq_json(lhs.inner, rhs.inner), + (Data::Refs(lhs), Data::Refs(rhs)) => lhs == rhs, + (Data::Ref(r), Data::Refs(rhs)) => eq_ref_to_array(r, &rhs), + (Data::Nothing, Data::Nothing) => true, + _ => false, + } +} +/// Compare two JSON values for equality. +/// For numbers, it should implement interoperability for integer and float +fn eq_json(lhs: &T, rhs: &T) -> bool { + let lhs_f64 = lhs.as_f64().or_else(|| lhs.as_i64().map(|v| v as f64)); + let rhs_f64 = rhs.as_f64().or_else(|| rhs.as_i64().map(|v| v as f64)); + + if let (Some(lhs_num), Some(rhs_num)) = (lhs_f64, rhs_f64) { + (lhs_num - rhs_num).abs() < f64::EPSILON + } else { + lhs == rhs + } +} +fn eq_ref_to_array(r: Pointer, rhs: &Vec>) -> bool { + r.inner.as_array().map_or(false, |array| { + eq_arrays(array, &rhs.iter().map(|p| p.inner).collect::>()) + }) +} + +fn eq_arrays(lhs: &Vec, rhs: &Vec<&T>) -> bool { + lhs.len() == rhs.len() && lhs.iter().zip(rhs.iter()).all(|(a, b)| eq_json(a, *b)) +} + +#[cfg(test)] +mod tests { + use crate::parser::model::{ + Comparable, Comparison, Literal, SingularQuery, SingularQuerySegment, + }; + use crate::query::state::{Data, Pointer, State}; + use crate::query::Query; + use crate::singular_query; + use crate::{cmp, comparable, lit, q_segment, q_segments}; + use serde_json::json; + #[test] + fn eq_comp_val() { + let data = json!({"key": "value"}); + let state = State::root(&data); + + let comparison = Comparison::Eq(comparable!(lit!(s "key")), comparable!(lit!(s "key"))); + let result = comparison.process(state); + assert_eq!(result.ok_val(), Some(json!(true))); + } + + #[test] + fn eq_comp_ref() { + let data = json!({"key": "value"}); + let state = State::root(&data); + + let comparison = Comparison::Eq( + comparable!(lit!(s "value")), + comparable!(> singular_query!(@ key)), + ); + + let result = comparison.process(state); + assert_eq!(result.ok_val(), Some(json!(true))); + } + + #[test] + fn eq_comp_queries() { + let data = json!({"key": "value", "key2": "value"}); + let state = State::root(&data); + + let comparison = Comparison::Eq( + comparable!(> singular_query!(@ key)), + comparable!(> singular_query!(key2)), + ); + let result = comparison.process(state); + assert_eq!(result.ok_val(), Some(json!(true))); + } + + #[test] + fn neq_comp_val() { + let data = json!({"key": "value"}); + let state = State::root(&data); + + let comparison = Comparison::Ne(comparable!(lit!(s "key")), comparable!(lit!(s "key"))); + let result = comparison.process(state); + assert_eq!(result.ok_val(), Some(json!(false))); + } + + #[test] + fn less_than() { + let data = json!({"key": 3}); + let state = State::root(&data); + + let comparison = Comparison::Lt( + comparable!(lit!(i 2)), + comparable!(> singular_query!(@ key)), + ); + let result = comparison.process(state); + assert_eq!(result.ok_val(), Some(json!(true))); + } + + #[test] + fn less_than_false() { + let data = json!({"key": 1}); + let state = State::root(&data); + + let comparison = Comparison::Lt( + comparable!(lit!(i 2)), + comparable!(> singular_query!(@ key)), + ); + let result = comparison.process(state); + assert_eq!(result.ok_val(), Some(json!(false))); + } + + #[test] + fn less_true() { + let data = json!({"key": 1}); + let state = State::root(&data); + + let comparison = Comparison::Lt( + comparable!(> singular_query!(@ key)), + comparable!(lit!(i 2)), + ); + let result = comparison.process(state); + assert_eq!(result.ok_val(), Some(json!(true))); + } +} diff --git a/src/query/filter.rs b/src/query/filter.rs new file mode 100644 index 0000000..bfa0721 --- /dev/null +++ b/src/query/filter.rs @@ -0,0 +1,125 @@ +use crate::parser::model::Filter; +use crate::query::queryable::Queryable; +use crate::query::state::{Data, Pointer, State}; +use crate::query::Query; + +impl Query for Filter { + fn process<'a, T: Queryable>(&self, state: State<'a, T>) -> State<'a, T> { + let root = state.root; + state.flat_map(|p| { + if p.is_internal() { + Data::Value(self.filter_item(p, root).into()) + } else if let Some(items) = p.inner.as_array() { + Data::Refs( + items + .into_iter() + .enumerate() + .filter(|(_, item)| self.filter_item(Pointer::empty(*item), root)) + .map(|(idx, item)| Pointer::idx(item, p.path.clone(), idx)) + .collect(), + ) + } else if let Some(items) = p.inner.as_object() { + Data::Refs( + items + .into_iter() + .filter(|(_, item)| self.filter_item(Pointer::empty(*item), root)) + .map(|(key, item)| Pointer::key(item, p.path.clone(), key)) + .collect(), + ) + } else { + return Data::Nothing; + } + }) + } +} + +impl Filter { + fn process_elem<'a, T: Queryable>(&self, state: State<'a, T>) -> State<'a, T> { + let process_cond = |filter: &Filter| { + filter + .process(state.clone()) + .ok_val() + .and_then(|v| v.as_bool()) + .unwrap_or_default() + }; + match self { + Filter::Or(ors) => State::bool(ors.iter().any(process_cond), state.root), + Filter::And(ands) => State::bool(ands.iter().all(process_cond), state.root), + Filter::Atom(atom) => atom.process(state), + } + } + + fn filter_item<'a, T: Queryable>(&self, item: Pointer<'a, T>, root: &T) -> bool { + self.process_elem(State::data(root, Data::Ref(item.clone()))) + .ok_val() + .and_then(|v| v.as_bool()) + .unwrap_or_default() + } +} + +#[cfg(test)] +mod tests { + use crate::query::js_path; + use serde_json::json; + + #[test] + fn smoke_ok() { + let json = json!({"a" : [1,2,3]}); + + assert_eq!( + js_path("$.a[? @ > 1]", &json), + Ok(vec![ + (&json!(2), "$['a'][1]".to_string()).into(), + (&json!(3), "$['a'][2]".to_string()).into(), + ]) + ); + } + + #[test] + fn existence() { + let json = json!({ + "a": { + "a":{"b":1}, + "c": { + "b": 2 + }, + "d": { + "b1": 3 + } + } + }); + assert_eq!( + js_path("$.a[?@.b]", &json), + Ok(vec![ + (&json!({"b":1}), "$['a']['a']".to_string()).into(), + (&json!({"b":2}), "$['a']['c']".to_string()).into(), + ]) + ); + } + + #[test] + fn existence_or() { + let json = json!({ + "a": { + "a":{"b":1}, + "c": { + "b": 2 + }, + "d": { + "b1": 3 + }, + "e": { + "b2": 3 + } + } + }); + assert_eq!( + js_path("$.a[?@.b || @.b1]", &json), + Ok(vec![ + (&json!({"b":1}), "$['a']['a']".to_string()).into(), + (&json!({"b":2}), "$['a']['c']".to_string()).into(), + (&json!({"b1":3}), "$['a']['d']".to_string()).into(), + ]) + ); + } +} diff --git a/src/query/jp_query.rs b/src/query/jp_query.rs new file mode 100644 index 0000000..b4c981d --- /dev/null +++ b/src/query/jp_query.rs @@ -0,0 +1,62 @@ +use crate::parser::model::{JpQuery, Segment}; +use crate::query::queryable::Queryable; +use crate::query::state::State; +use crate::query::Query; + +impl Query for JpQuery { + fn process<'a, T: Queryable>(&self, state: State<'a, T>) -> State<'a, T> { + self.segments.process(state) + } +} + +impl Query for Vec { + fn process<'a, T: Queryable>(&self, state: State<'a, T>) -> State<'a, T> { + self.iter() + .fold(state, |next, segment| segment.process(next)) + } +} + +#[cfg(test)] +mod tests { + use crate::parser::model::{JpQuery, Segment, Selector}; + use crate::query::state::{Data, Pointer, State}; + use crate::query::Query; + use serde_json::json; + + #[test] + fn test_process() { + let value = json!({ + "result": [ + { + "message": "Hello, Emmy! Your order number is: #100", + "phoneNumber": "255-301-9429", + "phoneVariation": "+90 398 588 10 73", + "status": "active", + "name": { + "first": "Blaise", + "middle": "Kyle", + "last": "Fadel" + } + } + ] + }); + + let query = JpQuery::new(vec![ + Segment::Selector(Selector::Name("result".to_string())), + Segment::Selector(Selector::Index(0)), + Segment::Selector(Selector::Name("name".to_string())), + Segment::Selector(Selector::Name("first".to_string())), + ]); + + let state = State::data(&value, Data::new_ref(Pointer::new(&value, "$".to_string()))); + + let result = query.process(state); + assert_eq!( + result.ok_ref(), + Some(vec![Pointer::new( + &json!("Blaise"), + "$['result'][0]['name']['first']".to_string() + )]) + ); + } +} diff --git a/src/query/queryable.rs b/src/query/queryable.rs new file mode 100644 index 0000000..4b1e30a --- /dev/null +++ b/src/query/queryable.rs @@ -0,0 +1,449 @@ +use crate::parser::errors::JsonPathError; +use crate::parser::model::{JpQuery, Segment, Selector}; +use crate::parser::{parse_json_path, Parsed}; +use crate::query::QueryPath; +use serde_json::Value; +use std::borrow::Cow; +use std::fmt::Debug; + +/// A trait that abstracts JSON-like data structures for JSONPath queries +/// +/// This trait provides the essential operations needed to traverse and query +/// hierarchical data structures in a JSONPath-compatible way. Implementors of +/// this trait can be used with the JSONPath query engine. +/// +/// The trait requires several standard type conversions to be implemented to +/// ensure that query operations can properly handle various data types. +/// +/// # Type Requirements +/// +/// Implementing types must satisfy these trait bounds: +/// - `Default`: Provides a default value for the type +/// - `Clone`: Allows creation of copies of values +/// - `Debug`: Enables debug formatting +/// - `From<&str>`: Conversion from string slices +/// - `From`: Conversion from boolean values +/// - `From`: Conversion from 64-bit integers +/// - `From`: Conversion from 64-bit floating point values +/// - `From>`: Conversion from vectors of the same type +/// - `From`: Conversion from owned strings +/// - `PartialEq`: Allows equality comparisons +/// +/// # Examples +/// +/// The trait is primarily implemented for `serde_json::Value` to enable +/// JSONPath queries on JSON data structures: +/// +/// ``` +/// use serde_json::json; +/// use jsonpath_rust::JsonPath; +/// +/// let data = json!({ +/// "store": { +/// "books": [ +/// {"title": "Book 1", "price": 10}, +/// {"title": "Book 2", "price": 15} +/// ] +/// } +/// }); +/// +/// // Access data using the Queryable trait +/// let books = data.query("$.store.books[*].title").expect("no errors"); +/// ``` +pub trait Queryable +where + Self: Default + + Clone + + Debug + + for<'a> From<&'a str> + + From + + From + + From + + From> + + From + + PartialEq, +{ + /// Retrieves a reference to the value associated with the given key. + /// It is the responsibility of the implementation to handle enclosing single and double quotes. + /// The key will be normalized (quotes trimmed, whitespace handled, the escape symbols handled) before lookup. + fn get(&self, key: &str) -> Option<&Self>; + + fn as_array(&self) -> Option<&Vec>; + + fn as_object(&self) -> Option>; + + fn as_str(&self) -> Option<&str>; + + fn as_i64(&self) -> Option; + fn as_f64(&self) -> Option; + fn as_bool(&self) -> Option; + + /// Returns a null value. + fn null() -> Self; + + fn extension_custom(_name: &str, _args: Vec>) -> Self { + Self::null() + } + + /// Retrieves a reference to the element at the specified path. + /// The path is specified as a string and can be obtained from the query. + /// + /// # Arguments + /// * `path` - A json path to the element specified as a string (root, field, index only). + fn reference(&self, _path: T) -> Option<&Self> + where + T: Into, + { + None + } + + /// Retrieves a mutable reference to the element at the specified path. + /// + /// # Arguments + /// * `path` - A json path to the element specified as a string (root, field, index only). + /// + /// # Examples + /// + /// ``` + /// use serde_json::json; + /// use jsonpath_rust::JsonPath; + /// use jsonpath_rust::query::queryable::Queryable; + /// let mut json = json!({ + /// "a": { + /// "b": { + /// "c": 42 + /// } + /// } + /// }); + /// if let Some(path) = json.query_only_path("$.a.b.c").unwrap().first() { + /// if let Some(v) = json.reference_mut("$.a.b.c") { + /// *v = json!(43); + /// } + /// + /// assert_eq!( + /// json, + /// json!({ + /// "a": { + /// "b": { + /// "c": 43 + /// } + /// } + /// }) + /// ); + /// } + //// ``` + fn reference_mut(&mut self, _path: T) -> Option<&mut Self> + where + T: Into, + { + None + } +} + +impl Queryable for Value { + fn get(&self, key: &str) -> Option<&Self> { + let key = if key.starts_with("'") && key.ends_with("'") { + key.trim_matches(|c| c == '\'') + } else if key.starts_with('"') && key.ends_with('"') { + key.trim_matches(|c| c == '"') + } else { + key + }; + self.get(key) + } + + fn as_array(&self) -> Option<&Vec> { + self.as_array() + } + + fn as_object(&self) -> Option> { + self.as_object() + .map(|v| v.into_iter().map(|(k, v)| (k, v)).collect()) + } + + fn as_str(&self) -> Option<&str> { + self.as_str() + } + + fn as_i64(&self) -> Option { + self.as_i64() + } + + fn as_f64(&self) -> Option { + self.as_f64() + } + + fn as_bool(&self) -> Option { + self.as_bool() + } + + fn null() -> Self { + Value::Null + } + + /// Custom extension function for JSONPath queries. + /// + /// This function allows for custom operations to be performed on JSON data + /// based on the provided `name` and `args`. + /// + /// # Arguments + /// + /// * `name` - A string slice that holds the name of the custom function. + /// * `args` - A vector of `Cow` that holds the arguments for the custom function. + /// + /// # Returns + /// + /// Returns a `Self` value which is the result of the custom function. If the function + /// name is not recognized, it returns `Self::null()`. + /// + /// # Custom Functions + /// + /// * `"in"` - Checks if the first argument is in the array provided as the second argument. + /// Example: `$.elems[?in(@, $.list)]` - Returns elements from $.elems that are present in $.list + /// + /// * `"nin"` - Checks if the first argument is not in the array provided as the second argument. + /// Example: `$.elems[?nin(@, $.list)]` - Returns elements from $.elems that are not present in $.list + /// + /// * `"none_of"` - Checks if none of the elements in the first array are in the second array. + /// Example: `$.elems[?none_of(@, $.list)]` - Returns arrays from $.elems that have no elements in common with $.list + /// + /// * `"any_of"` - Checks if any of the elements in the first array are in the second array. + /// Example: `$.elems[?any_of(@, $.list)]` - Returns arrays from $.elems that have at least one element in common with $.list + /// + /// * `"subset_of"` - Checks if all elements in the first array are in the second array. + /// Example: `$.elems[?subset_of(@, $.list)]` - Returns arrays from $.elems where all elements are present in $.list + fn extension_custom(name: &str, args: Vec>) -> Self { + match name { + "in" => match args.as_slice() { + [lhs, rhs] => match rhs.as_array() { + Some(elements) => elements.iter().any(|item| item == lhs.as_ref()).into(), + None => Self::null(), + }, + _ => Self::null(), + }, + "nin" => match args.as_slice() { + [lhs, rhs] => match rhs.as_array() { + Some(elements) => (!elements.iter().any(|item| item == lhs.as_ref())).into(), + None => Self::null(), + }, + _ => Self::null(), + }, + "none_of" => match args.as_slice() { + [lhs, rhs] => match (lhs.as_array(), rhs.as_array()) { + (Some(lhs_arr), Some(rhs_arr)) => lhs_arr + .iter() + .all(|lhs| !rhs_arr.iter().any(|rhs| lhs == rhs)) + .into(), + _ => Self::null(), + }, + _ => Self::null(), + }, + "any_of" => match args.as_slice() { + [lhs, rhs] => match (lhs.as_array(), rhs.as_array()) { + (Some(lhs_arr), Some(rhs_arr)) => lhs_arr + .iter() + .any(|lhs| rhs_arr.iter().any(|rhs| lhs == rhs)) + .into(), + _ => Self::null(), + }, + _ => Self::null(), + }, + "subset_of" => match args.as_slice() { + [lhs, rhs] => match (lhs.as_array(), rhs.as_array()) { + (Some(lhs_arr), Some(rhs_arr)) => lhs_arr + .iter() + .all(|lhs| rhs_arr.iter().any(|rhs| lhs == rhs)) + .into(), + _ => Self::null(), + }, + _ => Self::null(), + }, + _ => Self::null(), + } + } + + fn reference(&self, path: T) -> Option<&Self> + where + T: Into, + { + convert_js_path(&path.into()) + .ok() + .and_then(|p| self.pointer(p.as_str())) + } + + fn reference_mut(&mut self, path: T) -> Option<&mut Self> + where + T: Into, + { + convert_js_path(&path.into()) + .ok() + .and_then(|p| self.pointer_mut(p.as_str())) + } +} + +fn convert_js_path(path: &str) -> Parsed { + let JpQuery { segments } = parse_json_path(path)?; + + let mut path = String::new(); + for segment in segments { + match segment { + Segment::Selector(Selector::Name(name)) => { + path.push_str(&format!("/{}", name.trim_matches(|c| c == '\''))); + } + Segment::Selector(Selector::Index(index)) => { + path.push_str(&format!("/{}", index)); + } + s => { + return Err(JsonPathError::InvalidJsonPath(format!( + "Invalid segment: {:?}", + s + ))); + } + } + } + Ok(path) +} + +#[cfg(test)] +mod tests { + use crate::parser::Parsed; + use crate::query::queryable::{convert_js_path, Queryable}; + use crate::query::Queried; + use crate::JsonPath; + use serde_json::json; + + #[test] + fn in_smoke() -> Queried<()> { + let json = json!({ + "elems": ["test", "t1", "t2"], + "list": ["test", "test2", "test3"], + }); + + let res = json.query("$.elems[?in(@, $.list)]")?; + + assert_eq!(res, [&json!("test")]); + + Ok(()) + } + #[test] + fn nin_smoke() -> Queried<()> { + let json = json!({ + "elems": ["test", "t1", "t2"], + "list": ["test", "test2", "test3"], + }); + + let res = json.query("$.elems[?nin(@, $.list)]")?; + + assert_eq!(res, [&json!("t1"), &json!("t2")]); + + Ok(()) + } + #[test] + fn none_of_smoke() -> Queried<()> { + let json = json!({ + "elems": [ ["t1", "_"], ["t2", "t5"], ["t4"]], + "list": ["t1","t2", "t3"], + }); + + let res = json.query("$.elems[?none_of(@, $.list)]")?; + + assert_eq!(res, [&json!(["t4"])]); + + Ok(()) + } + #[test] + fn any_of_smoke() -> Queried<()> { + let json = json!({ + "elems": [ ["t1", "_"], ["t4", "t5"], ["t4"]], + "list": ["t1","t2", "t3"], + }); + + let res = json.query("$.elems[?any_of(@, $.list)]")?; + + assert_eq!(res, [&json!(["t1", "_"])]); + + Ok(()) + } + #[test] + fn subset_of_smoke() -> Queried<()> { + let json = json!({ + "elems": [ ["t1", "t2"], ["t4", "t5"], ["t6"]], + "list": ["t1","t2", "t3"], + }); + + let res = json.query("$.elems[?subset_of(@, $.list)]")?; + + assert_eq!(res, [&json!(["t1", "t2"])]); + + Ok(()) + } + + #[test] + fn convert_paths() -> Parsed<()> { + let r = convert_js_path("$.a.b[2]")?; + assert_eq!(r, "/a/b/2"); + + Ok(()) + } + + #[test] + fn test_references() -> Parsed<()> { + let mut json = json!({ + "a": { + "b": { + "c": 42 + } + } + }); + + let r = convert_js_path("$.a.b.c")?; + + if let Some(v) = json.pointer_mut(r.as_str()) { + *v = json!(43); + } + + assert_eq!( + json, + json!({ + "a": { + "b": { + "c": 43 + } + } + }) + ); + + Ok(()) + } + #[test] + fn test_js_reference() -> Parsed<()> { + let mut json = json!({ + "a": { + "b": { + "c": 42 + } + } + }); + + if let Some(path) = json.query_only_path("$.a.b.c")?.first() { + if let Some(v) = json.reference_mut(path) { + *v = json!(43); + } + + assert_eq!( + json, + json!({ + "a": { + "b": { + "c": 43 + } + } + }) + ); + } else { + panic!("no path found"); + } + + Ok(()) + } +} diff --git a/src/query/segment.rs b/src/query/segment.rs new file mode 100644 index 0000000..305645d --- /dev/null +++ b/src/query/segment.rs @@ -0,0 +1,110 @@ +use crate::parser::model::{Segment, Selector}; +use crate::query::queryable::Queryable; +use crate::query::state::{Data, Pointer, State}; +use crate::query::Query; + +impl Query for Segment { + fn process<'a, T: Queryable>(&self, step: State<'a, T>) -> State<'a, T> { + match self { + Segment::Descendant(segment) => segment.process(step.flat_map(process_descendant)), + Segment::Selector(selector) => selector.process(step), + Segment::Selectors(selectors) => process_selectors(step, selectors), + } + } +} + +fn process_selectors<'a, T: Queryable>( + step: State<'a, T>, + selectors: &Vec, +) -> State<'a, T> { + selectors + .into_iter() + .map(|s| s.process(step.clone())) + .reduce(State::reduce) + .unwrap_or(step.root.into()) +} + +fn process_descendant(data: Pointer) -> Data { + if let Some(array) = data.inner.as_array() { + Data::Ref(data.clone()).reduce( + Data::new_refs( + array + .iter() + .enumerate() + .map(|(i, elem)| Pointer::idx(elem, data.path.clone(), i)) + .collect(), + ) + .flat_map(process_descendant), + ) + } else if let Some(object) = data.inner.as_object() { + Data::Ref(data.clone()).reduce( + Data::new_refs( + object + .into_iter() + .map(|(key, value)| Pointer::key(value, data.path.clone(), key)) + .collect(), + ) + .flat_map(process_descendant), + ) + } else { + Data::Nothing + } +} + +#[cfg(test)] +mod tests { + use crate::parser::model::{Segment, Selector}; + use crate::query::state::{Pointer, State}; + use crate::query::Query; + use serde_json::json; + + #[test] + fn test_process_selectors() { + let value = json!({"firstName": "John", "lastName" : "doe",}); + let segment = Segment::Selectors(vec![ + Selector::Name("firstName".to_string()), + Selector::Name("lastName".to_string()), + ]); + let step = segment.process(State::root(&value)); + + assert_eq!( + step.ok_ref(), + Some(vec![ + Pointer::new(&json!("John"), "$['firstName']".to_string()), + Pointer::new(&json!("doe"), "$['lastName']".to_string()) + ]) + ); + } + + #[test] + fn test_process_descendant() { + let value = json!([{"name": "John"}, {"name": "doe"}]); + let segment = Segment::Descendant(Box::new(Segment::Selector(Selector::Wildcard))); + let step = segment.process(State::root(&value)); + + assert_eq!( + step.ok_ref(), + Some(vec![ + Pointer::new(&json!({"name": "John"}), "$[0]".to_string()), + Pointer::new(&json!({"name": "doe"}), "$[1]".to_string()), + Pointer::new(&json!("John"), "$[0]['name']".to_string()), + Pointer::new(&json!("doe"), "$[1]['name']".to_string()), + ]) + ); + } + + #[test] + fn test_process_descendant2() { + let value = json!({"o": [0,1,[2,3]]}); + let segment = Segment::Descendant(Box::new(Segment::Selector(Selector::Index(1)))); + let step = segment.process(State::root(&value)); + + assert_eq!( + step.ok_ref(), + Some(vec![ + Pointer::new(&json!(1), "$['o'][1]".to_string()), + Pointer::new(&json!(3), "$['o'][2][1]".to_string()), + ]) + ); + } +} diff --git a/src/query/selector.rs b/src/query/selector.rs new file mode 100644 index 0000000..0ca00d4 --- /dev/null +++ b/src/query/selector.rs @@ -0,0 +1,400 @@ +use crate::parser::model::Selector; +use crate::query::queryable::Queryable; +use crate::query::state::{Data, Pointer, State}; +use crate::query::Query; +use std::cmp::{max, min}; + +impl Query for Selector { + fn process<'a, T: Queryable>(&self, step: State<'a, T>) -> State<'a, T> { + match self { + Selector::Name(key) => step.flat_map(|d| process_key(d, key)), + Selector::Index(idx) => step.flat_map(|d| process_index(d, idx)), + Selector::Wildcard => step.flat_map(process_wildcard), + Selector::Slice(start, end, sl_step) => { + step.flat_map(|d| process_slice(d, start, end, sl_step)) + } + Selector::Filter(f) => f.process(step), + } + } +} + +fn process_wildcard( + Pointer { + inner: pointer, + path, + }: Pointer, +) -> Data { + if let Some(array) = pointer.as_array() { + if array.is_empty() { + Data::Nothing + } else { + Data::new_refs( + array + .iter() + .enumerate() + .map(|(i, elem)| Pointer::idx(elem, path.clone(), i)) + .collect(), + ) + } + } else if let Some(object) = pointer.as_object() { + if object.is_empty() { + Data::Nothing + } else { + Data::new_refs( + object + .into_iter() + .map(|(key, value)| Pointer::key(value, path.clone(), key)) + .collect(), + ) + } + } else { + Data::Nothing + } +} + +fn process_slice<'a, T: Queryable>( + Pointer { inner, path }: Pointer<'a, T>, + start: &Option, + end: &Option, + step: &Option, +) -> Data<'a, T> { + let extract_elems = |elements: &'a Vec| -> Vec<(&'a T, usize)> { + let len = elements.len() as i64; + let norm = |i: i64| { + if i >= 0 { + i + } else { + len + i + } + }; + + match step.unwrap_or(1) { + e if e > 0 => { + let n_start = norm(start.unwrap_or(0)); + let n_end = norm(end.unwrap_or(len)); + let lower = min(max(n_start, 0), len); + let upper = min(max(n_end, 0), len); + + let mut idx = lower; + let mut res = vec![]; + while idx < upper { + let i = idx as usize; + if let Some(elem) = elements.get(i) { + res.push((elem, i)); + } + idx += e; + } + res + } + e if e < 0 => { + let n_start = norm(start.unwrap_or(len - 1)); + let n_end = norm(end.unwrap_or(-len - 1)); + let lower = min(max(n_end, -1), len - 1); + let upper = min(max(n_start, -1), len - 1); + let mut idx = upper; + let mut res = vec![]; + while lower < idx { + let i = idx as usize; + if let Some(elem) = elements.get(i) { + res.push((elem, i)); + } + idx += e; + } + res + } + _ => vec![], + } + }; + + let elems_to_step = |v: Vec<(&'a T, usize)>| { + Data::new_refs( + v.into_iter() + .map(|(elem, i)| Pointer::idx(elem, path.clone(), i)) + .collect(), + ) + }; + + inner + .as_array() + .map(extract_elems) + .map(elems_to_step) + .unwrap_or_default() +} + +/// Processes escape sequences in JSON strings +/// - Replaces `\\` with `\` +/// - Replaces `\/` with `/` +/// - Preserves other valid escapes like `\"` and `\'` +fn normalize_json_key(input: &str) -> String { + let mut result = String::with_capacity(input.len()); + let mut chars = input.chars().peekable(); + + while let Some(c) = chars.next() { + if c == '\\' { + if let Some(&next) = chars.peek() { + match next { + '\\' => { + result.push('\\'); + chars.next(); // consume the second backslash + } + '/' => { + result.push('/'); + chars.next(); // consume the forward slash + } + '\'' => { + result.push('\\'); + result.push('\''); + chars.next(); // consume the quote + } + '"' => { + result.push('\\'); + result.push('"'); + chars.next(); // consume the quote + } + 'b' | 'f' | 'n' | 'r' | 't' | 'u' => { + // Preserve these standard JSON escape sequences + result.push('\\'); + result.push(next); + chars.next(); + } + _ => { + // Invalid escape - just keep as-is + result.push('\\'); + } + } + } else { + // Trailing backslash + result.push('\\'); + } + } else { + result.push(c); + } + } + result +} + +pub fn process_key<'a, T: Queryable>( + Pointer { inner, path }: Pointer<'a, T>, + key: &str, +) -> Data<'a, T> { + inner + .get(normalize_json_key(key).as_str()) + .map(|v| Data::new_ref(Pointer::key(v, path, key))) + .unwrap_or_default() +} + +pub fn process_index<'a, T: Queryable>( + Pointer { inner, path }: Pointer<'a, T>, + idx: &i64, +) -> Data<'a, T> { + inner + .as_array() + .map(|array| { + if *idx >= 0 { + if *idx >= array.len() as i64 { + Data::Nothing + } else { + let i = *idx as usize; + Data::new_ref(Pointer::idx(&array[i], path, i)) + } + } else { + let abs_idx = idx.abs() as usize; + if abs_idx > array.len() { + Data::Nothing + } else { + let i = array.len() - abs_idx; + Data::new_ref(Pointer::idx(&array[i], path, i)) + } + } + }) + .unwrap_or_default() +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::parser::model::Segment; + use crate::query::{js_path, Queried}; + use serde_json::json; + use std::vec; + #[test] + fn test_process_empty_key() { + let value = json!({" ": "value"}); + let segment = Segment::Selector(Selector::Name(" ".to_string())); + + let step = segment.process(State::root(&value)); + + assert_eq!( + step.ok_ref(), + Some(vec![Pointer::new(&json!("value"), "$[' ']".to_string())]) + ); + } + #[test] + fn test_process_key() { + let value = json!({"key": "value"}); + let segment = Segment::Selector(Selector::Name("key".to_string())); + + let step = segment.process(State::root(&value)); + + assert_eq!( + step.ok_ref(), + Some(vec![Pointer::new(&json!("value"), "$['key']".to_string())]) + ); + } + + #[test] + fn test_process_key_failed() { + let value = json!({"key": "value"}); + let segment = Segment::Selector(Selector::Name("key2".to_string())); + let step = segment.process(State::root(&value)); + + assert_eq!(step, State::nothing(&value)); + } + + #[test] + fn test_process_index() { + let value = json!([1, 2, 3]); + let segment = Segment::Selector(Selector::Index(1)); + let step = segment.process(State::root(&value)); + + assert_eq!( + step.ok_ref(), + Some(vec![Pointer::new(&json!(2), "$[1]".to_string())]) + ); + } + + #[test] + fn test_process_index_failed() { + let value = json!([1, 2, 3]); + let segment = Segment::Selector(Selector::Index(3)); + let step = segment.process(State::root(&value)); + + assert_eq!(step, State::nothing(&value)); + } + + #[test] + fn test_process_slice1() { + let value = json!([1, 2, 3, 4, 5]); + let segment = Segment::Selector(Selector::Slice(Some(1), Some(4), Some(1))); + let step = segment.process(State::root(&value)); + + assert_eq!( + step.ok_ref(), + Some(vec![ + Pointer::new(&json!(2), "$[1]".to_string()), + Pointer::new(&json!(3), "$[2]".to_string()), + Pointer::new(&json!(4), "$[3]".to_string()) + ]) + ); + } + + #[test] + fn test_process_slice2() { + let value = json!([1, 2, 3, 4, 5]); + let segment = Segment::Selector(Selector::Slice(Some(2), Some(0), Some(-1))); + let step = segment.process(State::root(&value)); + + assert_eq!( + step.ok_ref(), + Some(vec![ + Pointer::new(&json!(3), "$[2]".to_string()), + Pointer::new(&json!(2), "$[1]".to_string()), + ]) + ); + } + + #[test] + fn test_process_slice3() { + let value = json!([1, 2, 3, 4, 5]); + let segment = Segment::Selector(Selector::Slice(Some(0), Some(5), Some(2))); + let step = segment.process(State::root(&value)); + + assert_eq!( + step.ok_ref(), + Some(vec![ + Pointer::new(&json!(1), "$[0]".to_string()), + Pointer::new(&json!(3), "$[2]".to_string()), + Pointer::new(&json!(5), "$[4]".to_string()) + ]) + ); + } + + #[test] + fn test_process_slice_failed() { + let value = json!([1, 2, 3, 4, 5]); + let segment = Segment::Selector(Selector::Slice(Some(0), Some(5), Some(0))); + let step = segment.process(State::root(&value)); + + assert_eq!(step.ok_ref(), Some(vec![])); + } + + #[test] + fn test_process_wildcard() { + let value = json!({"key": "value", "key2": "value2"}); + let segment = Segment::Selector(Selector::Wildcard); + let step = segment.process(State::root(&value)); + + assert_eq!( + step.ok_ref(), + Some(vec![ + Pointer::new(&json!("value"), "$['key']".to_string()), + Pointer::new(&json!("value2"), "$['key2']".to_string()) + ]) + ); + } + + #[test] + fn test_process_wildcard_array() { + let value = json!([1, 2, 3]); + let segment = Segment::Selector(Selector::Wildcard); + let step = segment.process(State::root(&value)); + + assert_eq!( + step.ok_ref(), + Some(vec![ + Pointer::new(&json!(1), "$[0]".to_string()), + Pointer::new(&json!(2), "$[1]".to_string()), + Pointer::new(&json!(3), "$[2]".to_string()) + ]) + ); + } + + #[test] + fn test_process_wildcard_failed() { + let value = json!(1); + let segment = Segment::Selector(Selector::Wildcard); + let step = segment.process(State::root(&value)); + + assert_eq!(step, State::nothing(&value)); + } + + #[test] + fn multi_selector() -> Queried<()> { + let json = json!([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]); + + let vec = js_path("$['a',1]", &json)?; + + assert_eq!(vec, vec![(&json!(1), "$[1]".to_string()).into(),]); + + Ok(()) + } + #[test] + fn multi_selector_space() -> Queried<()> { + let json = json!({ + "a": "ab", + "b": "bc" + }); + + let vec = js_path("$['a',\r'b']", &json)?; + + assert_eq!( + vec, + vec![ + (&json!("ab"), "$['a']".to_string()).into(), + (&json!("bc"), "$['b']".to_string()).into(), + ] + ); + + Ok(()) + } +} diff --git a/src/query/state.rs b/src/query/state.rs new file mode 100644 index 0000000..eb14ff6 --- /dev/null +++ b/src/query/state.rs @@ -0,0 +1,238 @@ +use crate::query::queryable::Queryable; +use crate::query::QueryPath; +use std::fmt::{Display, Formatter}; + +/// Represents the state of a query, including the current data and the root object. +/// It is used to track the progress of a query as it traverses through the data structure. +#[derive(Debug, Clone, PartialEq)] +pub struct State<'a, T: Queryable> { + pub data: Data<'a, T>, + pub root: &'a T, +} + +impl<'a, T: Queryable> Display for State<'a, T> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.data) + } +} + +impl<'a, T: Queryable> From<&'a T> for State<'a, T> { + fn from(root: &'a T) -> Self { + State::root(root) + } +} + +impl<'a, T: Queryable> State<'a, T> { + pub fn bool(b: bool, root: &T) -> State { + State::data(root, Data::Value(b.into())) + } + + pub fn i64(i: i64, root: &T) -> State { + State::data(root, Data::Value(i.into())) + } + pub fn str(v: &str, root: &'a T) -> State<'a, T> { + State::data(root, Data::Value(v.into())) + } + + pub fn shift_to_root(self) -> State<'a, T> { + State::root(self.root) + } + + pub fn root(root: &'a T) -> Self { + State { + root, + data: Data::new_ref(Pointer::new(root, "$".to_string())), + } + } + + pub fn nothing(root: &'a T) -> Self { + State { + root, + data: Data::Nothing, + } + } + + pub fn data(root: &'a T, data: Data<'a, T>) -> Self { + State { root, data } + } + + pub fn ok_ref(self) -> Option>> { + self.data.ok_ref() + } + + pub fn ok_val(self) -> Option { + match self.data { + Data::Value(v) => Some(v), + _ => None, + } + } + + pub fn is_nothing(&self) -> bool { + matches!(&self.data, Data::Nothing) + } + + pub fn reduce(self, other: State<'a, T>) -> State<'a, T> { + State { + root: self.root, + data: self.data.reduce(other.data), + } + } + pub fn flat_map(self, f: F) -> State<'a, T> + where + F: Fn(Pointer<'a, T>) -> Data<'a, T>, + { + State { + root: self.root, + data: self.data.flat_map(f), + } + } +} + +/// Represents the data that is being processed in the query. +/// It can be a reference to a single object, a collection of references, +#[derive(Debug, Clone, PartialEq)] +pub enum Data<'a, T: Queryable> { + Ref(Pointer<'a, T>), + Refs(Vec>), + Value(T), + Nothing, +} + +impl<'a, T: Queryable> Display for Data<'a, T> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Data::Ref(p) => write!(f, "&{}", p), + Data::Refs(p) => write!( + f, + "{}", + p.iter() + .map(|ptr| ptr.to_string()) + .collect::>() + .join("; ") + ), + Data::Value(v) => write!(f, "{:?}", v), + Data::Nothing => write!(f, "Nothing"), + } + } +} + +impl<'a, T: Queryable> Default for Data<'a, T> { + fn default() -> Self { + Data::Nothing + } +} + +impl<'a, T: Queryable> Data<'a, T> { + pub fn reduce(self, other: Data<'a, T>) -> Data<'a, T> { + match (self, other) { + (Data::Ref(data), Data::Ref(data2)) => Data::Refs(vec![data, data2]), + (Data::Ref(data), Data::Refs(data_vec)) => { + Data::Refs(vec![data].into_iter().chain(data_vec).collect()) + } + (Data::Refs(data_vec), Data::Ref(data)) => { + Data::Refs(data_vec.into_iter().chain(vec![data]).collect()) + } + (Data::Refs(data_vec), Data::Refs(data_vec2)) => { + Data::Refs(data_vec.into_iter().chain(data_vec2).collect()) + } + (d @ (Data::Ref(_) | Data::Refs(..)), Data::Nothing) => d, + (Data::Nothing, d @ (Data::Ref(_) | Data::Refs(..))) => d, + _ => Data::Nothing, + } + } + + pub fn flat_map(self, f: F) -> Data<'a, T> + where + F: Fn(Pointer<'a, T>) -> Data<'a, T>, + { + match self { + Data::Ref(data) => f(data), + Data::Refs(data_vec) => Data::Refs( + data_vec + .into_iter() + .flat_map(|data| match f(data) { + Data::Ref(data) => vec![data], + Data::Refs(data_vec) => data_vec, + _ => vec![], + }) + .collect::>(), + ), + _ => Data::Nothing, + } + } + + /// Returns the inner value if it is a single reference. + /// If it is a collection of references, it returns the first one. + /// If it is a value, it returns None. + pub fn ok_ref(self) -> Option>> { + match self { + Data::Ref(data) => Some(vec![data]), + Data::Refs(data) => Some(data), + _ => None, + } + } + + /// Returns the inner value if it is a single value. + /// If it is a reference or a collection of references, it returns None. + pub fn ok_val(self) -> Option { + match self { + Data::Value(v) => Some(v), + _ => None, + } + } + + pub fn new_ref(data: Pointer<'a, T>) -> Data<'a, T> { + Data::Ref(data) + } + + pub fn new_refs(data: Vec>) -> Data<'a, T> { + Data::Refs(data) + } +} + +/// Represents a pointer to a specific location in the data structure. +/// It contains a reference to the data and a path that indicates the location of the data in the structure. +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct Pointer<'a, T: Queryable> { + pub inner: &'a T, + pub path: QueryPath, +} + +impl<'a, T: Queryable> Display for Pointer<'a, T> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}='{}'", self.inner, self.path) + } +} + +impl<'a, T: Queryable> Pointer<'a, T> { + pub fn new(inner: &'a T, path: QueryPath) -> Self { + Pointer { inner, path } + } + + pub fn key(inner: &'a T, path: QueryPath, key: &str) -> Self { + let path = if key.starts_with("'") && key.ends_with("'") { + format!("{}[{}]", path, key) + } else { + format!("{}['{}']", path, key) + }; + + Pointer { inner, path } + } + pub fn idx(inner: &'a T, path: QueryPath, index: usize) -> Self { + Pointer { + inner, + path: format!("{}[{}]", path, index), + } + } + + pub fn empty(inner: &'a T) -> Self { + Pointer { + inner, + path: String::new(), + } + } + + pub fn is_internal(&self) -> bool { + self.path.is_empty() + } +} diff --git a/src/query/test.rs b/src/query/test.rs new file mode 100644 index 0000000..4b17ee0 --- /dev/null +++ b/src/query/test.rs @@ -0,0 +1,14 @@ +use crate::parser::model::Test; +use crate::query::queryable::Queryable; +use crate::query::state::State; +use crate::query::Query; + +impl Query for Test { + fn process<'a, T: Queryable>(&self, state: State<'a, T>) -> State<'a, T> { + match self { + Test::RelQuery(segments) => segments.process(state), + Test::AbsQuery(jquery) => jquery.process(state.shift_to_root()), + Test::Function(tf) => tf.process(state), + } + } +} diff --git a/src/query/test_function.rs b/src/query/test_function.rs new file mode 100644 index 0000000..faa12c7 --- /dev/null +++ b/src/query/test_function.rs @@ -0,0 +1,246 @@ +use crate::parser::model::{FnArg, TestFunction}; +use crate::query::queryable::Queryable; +use crate::query::state::{Data, Pointer, State}; +use crate::query::Query; +use regex::Regex; +use std::borrow::Cow; + +impl TestFunction { + pub fn apply<'a, T: Queryable>(&self, state: State<'a, T>) -> State<'a, T> { + match self { + TestFunction::Length(arg) => length(arg.process(state)), + TestFunction::Count(arg) => count(arg.process(state)), + TestFunction::Match(lhs, rhs) => { + regex(lhs.process(state.clone()), rhs.process(state), false) + } + TestFunction::Search(lhs, rhs) => { + regex(lhs.process(state.clone()), rhs.process(state), true) + } + TestFunction::Custom(name, args) => custom(name, args, state), + TestFunction::Value(arg) => value(arg.process(state)), + _ => State::nothing(state.root), + } + } +} + +impl Query for FnArg { + fn process<'a, T: Queryable>(&self, step: State<'a, T>) -> State<'a, T> { + match self { + FnArg::Literal(lit) => lit.process(step), + FnArg::Test(test) => test.process(step), + FnArg::Filter(filter) => filter.process(step), + } + } +} + +impl Query for TestFunction { + fn process<'a, T: Queryable>(&self, step: State<'a, T>) -> State<'a, T> { + self.apply(step) + } +} + +fn custom<'a, T: Queryable>(name: &str, args: &Vec, state: State<'a, T>) -> State<'a, T> { + let args = args + .into_iter() + .map(|v| v.process(state.clone())) + .flat_map(|v| match v.data { + Data::Value(v) => vec![Cow::Owned(v)], + Data::Ref(Pointer { inner, .. }) => vec![Cow::Borrowed(inner)], + Data::Refs(v) => v.into_iter().map(|v| Cow::Borrowed(v.inner)).collect(), + _ => vec![], + }) + .collect::>(); + + State::data( + state.root, + Data::Value(Queryable::extension_custom(name, args)), + ) +} + +/// Returns the length/size of the object. +/// +/// # Returns +/// +/// Returns a `Progress` enum containing either: +/// - `Progress::Data` with a vector of references to self and the query path for strings/arrays/objects +/// - `Progress::Nothing` for other types +/// +/// The returned length follows JSON path length() function semantics based on the type: +/// - String type: Number of Unicode scalar values +/// - Array type: Number of elements +/// - Object type: Number of members +/// - Other types: Nothing +fn length(state: State) -> State { + let from_item = |item: &T| { + if let Some(v) = item.as_str() { + State::i64(v.chars().count() as i64, state.root) + } else if let Some(items) = item.as_array() { + State::i64(items.len() as i64, state.root) + } else if let Some(items) = item.as_object() { + State::i64(items.len() as i64, state.root) + } else { + State::nothing(state.root) + } + }; + + match state.data { + Data::Ref(Pointer { inner, .. }) => from_item(inner), + Data::Refs(items) => State::i64(items.len() as i64, state.root), + Data::Value(item) => from_item(&item), + Data::Nothing => State::nothing(state.root), + } +} + +/// The count() function extension provides a way +/// to obtain the number of nodes in a nodelist +/// and make that available for further processing in the filter expression +fn count(state: State) -> State { + let to_state = |count: i64| State::i64(count, state.root); + + match state.data { + Data::Ref(..) | Data::Value(..) => to_state(1), + Data::Refs(items) => to_state(items.len() as i64), + Data::Nothing => State::nothing(state.root), + } +} +/// The match() function extension provides +/// a way to check whether (the entirety of; see Section 2.4.7) +/// a given string matches a given regular expression, +/// which is in the form described in [RFC9485]. +/// +/// Its arguments are instances of ValueType +/// (possibly taken from a singular query, +/// as for the first argument in the example above). +/// If the first argument is not a string +/// or the second argument is not a string conforming to [RFC9485], +/// the result is LogicalFalse. Otherwise, the string that is the first argument is matched against +/// the I-Regexp contained in the string that is the second argument; the result is LogicalTrue +/// if the string matches the I-Regexp and is LogicalFalse otherwise. +fn regex<'a, T: Queryable>(lhs: State<'a, T>, rhs: State<'a, T>, substr: bool) -> State<'a, T> { + let to_state = |b| State::bool(b, lhs.root); + let regex = |v: &str, r: Regex| { + if substr { + r.find(v).is_some() + } else { + r.is_match(v) + } + }; + let to_str = |s: State<'a, T>| match s.data { + Data::Value(v) => v.as_str().map(|s| s.to_string()), + Data::Ref(Pointer { inner, .. }) => inner.as_str().map(|s| s.to_string()), + _ => None, + }; + + match (to_str(lhs), to_str(rhs)) { + (Some(lhs), Some(rhs)) => Regex::new(&prepare_regex(rhs, substr)) + .map(|re| to_state(regex(&lhs, re))) + .unwrap_or(to_state(false)), + _ => to_state(false), + } +} + +fn prepare_regex(pattern: String, substring: bool) -> String { + let pattern = if !substring { + let pattern = if pattern.starts_with('^') { + pattern + } else { + format!("^{}", pattern) + }; + let pattern = if pattern.ends_with('$') { + pattern + } else { + format!("{}$", pattern) + }; + pattern + } else { + pattern.to_string() + }; + let pattern = if pattern.contains("\\\\") { + pattern.replace("\\\\", "\\") + } else { + pattern.to_string() + }; + + pattern.trim_matches(|c| c == '\'' || c == '"').to_string() +} + +fn value(state: State) -> State { + match state.data { + Data::Ref(..) | Data::Value(..) => state, + Data::Refs(items) if items.len() == 1 => { + State::data(state.root, Data::Ref(items[0].clone())) + } + _ => State::nothing(state.root), + } +} + +#[cfg(test)] +mod tests { + use crate::parser::model::Segment; + use crate::parser::model::Selector; + use crate::parser::model::Test; + use crate::parser::model::TestFunction; + use crate::query::state::{Data, Pointer, State}; + use crate::query::test_function::{regex, FnArg}; + use crate::query::Query; + use crate::{arg, q_segment, segment, selector, test, test_fn}; + use serde_json::json; + + #[test] + fn test_len() { + let json = json!({"array": [1,2,3]}); + let state = State::root(&json); + + let query = test_fn!(length arg!(t test!(@ segment!(selector!(array))))); + let res = query.process(state); + + assert_eq!(res.ok_val(), Some(json!(3))); + } + + #[test] + fn test_match_1() { + let json = json!({"a": "abc sdgfudsf","b": "abc.*"}); + let state = State::root(&json); + + let query = test_fn!(match + arg!(t test!(@ segment!(selector!(a)))), + arg!(t test!(@ segment!(selector!(b)))) + ); + let res = query.process(state); + + assert_eq!(res.ok_val(), Some(json!(true))); + } + + #[test] + fn test_count_1() { + let json = json!({"array": [1,2,3]}); + let state = State::root(&json); + + let query = test_fn!(count arg!(t test!(@ segment!(selector!(array))))); + let res = query.process(state); + + assert_eq!(res.ok_val(), Some(json!(1))); + } + + #[test] + fn test_search() { + let json = json!("123"); + let state = State::root(&json); + let reg = State::str("[a-z]+", &json); + + let res = regex(state, reg, true); + + assert_eq!(res.ok_val(), Some(json!(false))); + } + + #[test] + fn test_match() { + let json = json!("bbab"); + let state = State::root(&json); + let reg = State::str("^b.?b$", &json); + + let res = regex(state, reg, false); + + assert_eq!(res.ok_val(), Some(json!(false))); + } +}