From 4b81febb6e31e10f3877ba89edfb1df4125ea289 Mon Sep 17 00:00:00 2001 From: V0ldek Date: Tue, 31 Dec 2024 01:07:15 +0100 Subject: [PATCH] feat: serde support for `JsonPathQuery` and `MainEngine` Implemented `serde::Serialize` and `serde::Deserialize` for `MainEngine` in rsonpath-lib, and `JsonPathQuery` in rsonpath-syntax with all of its constituent substructs. The `serde` dependency is guarded behind an optional feature. To properly proptest this feature `rsonpath-lib` needs access to the arbitrary query generation from `rsonpath-syntax`. The cleanest way to do this is to extract the logic to a separate crate, giving rise to `rsonpath-syntax-proptest`. The serialization format is not stable, as the `Automaton` is expected to evolve. Thus, serialization includes a version and deserialization will fail if the version disagrees. Also added snapshot tests based on `insta` to both the rsonpath-syntax and rsonpath-lib serialization features. --- .cargo/config.toml | 2 +- .github/workflows/rust.yml | 4 +- Cargo.lock | 137 +++- Cargo.toml | 10 + Justfile | 6 +- crates/rsonpath-benchmarks/src/dataset.rs | 1 + crates/rsonpath-lib/Cargo.toml | 9 +- crates/rsonpath-lib/README.md | 15 +- crates/rsonpath-lib/src/automaton.rs | 5 + crates/rsonpath-lib/src/automaton/state.rs | 2 + crates/rsonpath-lib/src/engine.rs | 4 +- crates/rsonpath-lib/src/engine/main.rs | 10 +- crates/rsonpath-lib/src/engine/serde.rs | 211 +++++ crates/rsonpath-lib/src/string_pattern.rs | 1 + .../tests/engine_serialization_snapshots.rs | 42 + ...alization_snapshots__ron__empty_query.snap | 21 + ...napshots__ron__jsonpath_example_query.snap | 84 ++ ...lization_snapshots__ron__readme_query.snap | 37 + ...ation_snapshots__ron__real_life_query.snap | 81 ++ ...e_serialization_snapshots__ron__slice.snap | 50 ++ crates/rsonpath-syntax-proptest/Cargo.toml | 28 + crates/rsonpath-syntax-proptest/README.md | 33 + crates/rsonpath-syntax-proptest/src/lib.rs | 721 ++++++++++++++++++ crates/rsonpath-syntax/Cargo.toml | 8 +- crates/rsonpath-syntax/README.md | 32 +- crates/rsonpath-syntax/src/lib.rs | 15 + crates/rsonpath-syntax/src/num.rs | 5 + crates/rsonpath-syntax/src/parser.rs | 2 +- crates/rsonpath-syntax/src/str.rs | 1 + .../query_parser_tests.proptest-regressions | Bin 8996 -> 27985 bytes .../tests/query_parser_tests.rs | 574 ++------------ .../tests/query_serialization_snapshots.rs | 54 ++ ...alization_snapshots__ron__empty_query.snap | 8 + ..._serialization_snapshots__ron__filter.snap | 47 ++ ...napshots__ron__jsonpath_example_query.snap | 28 + ...on_snapshots__ron__multiple_selectors.snap | 29 + ...ization_snapshots__ron__nested_filter.snap | 56 ++ ...lization_snapshots__ron__readme_query.snap | 21 + ...ation_snapshots__ron__real_life_query.snap | 51 ++ ...y_serialization_snapshots__ron__slice.snap | 25 + crates/rsonpath-test/Cargo.toml | 4 +- fuzz/Cargo.lock | 9 +- fuzz/Cargo.toml | 3 +- 43 files changed, 1925 insertions(+), 561 deletions(-) create mode 100644 crates/rsonpath-lib/src/engine/serde.rs create mode 100644 crates/rsonpath-lib/tests/engine_serialization_snapshots.rs create mode 100644 crates/rsonpath-lib/tests/snapshots/engine_serialization_snapshots__ron__empty_query.snap create mode 100644 crates/rsonpath-lib/tests/snapshots/engine_serialization_snapshots__ron__jsonpath_example_query.snap create mode 100644 crates/rsonpath-lib/tests/snapshots/engine_serialization_snapshots__ron__readme_query.snap create mode 100644 crates/rsonpath-lib/tests/snapshots/engine_serialization_snapshots__ron__real_life_query.snap create mode 100644 crates/rsonpath-lib/tests/snapshots/engine_serialization_snapshots__ron__slice.snap create mode 100644 crates/rsonpath-syntax-proptest/Cargo.toml create mode 100644 crates/rsonpath-syntax-proptest/README.md create mode 100644 crates/rsonpath-syntax-proptest/src/lib.rs create mode 100644 crates/rsonpath-syntax/tests/query_serialization_snapshots.rs create mode 100644 crates/rsonpath-syntax/tests/snapshots/query_serialization_snapshots__ron__empty_query.snap create mode 100644 crates/rsonpath-syntax/tests/snapshots/query_serialization_snapshots__ron__filter.snap create mode 100644 crates/rsonpath-syntax/tests/snapshots/query_serialization_snapshots__ron__jsonpath_example_query.snap create mode 100644 crates/rsonpath-syntax/tests/snapshots/query_serialization_snapshots__ron__multiple_selectors.snap create mode 100644 crates/rsonpath-syntax/tests/snapshots/query_serialization_snapshots__ron__nested_filter.snap create mode 100644 crates/rsonpath-syntax/tests/snapshots/query_serialization_snapshots__ron__readme_query.snap create mode 100644 crates/rsonpath-syntax/tests/snapshots/query_serialization_snapshots__ron__real_life_query.snap create mode 100644 crates/rsonpath-syntax/tests/snapshots/query_serialization_snapshots__ron__slice.snap diff --git a/.cargo/config.toml b/.cargo/config.toml index ab55fa40..a30ab5b5 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -2,7 +2,7 @@ rustflags = ["-C", "link-arg=-fuse-ld=lld"] [alias] -rsontest = "hack test -q --feature-powerset --skip default --features arbitrary --ignore-unknown-features" +rsontest = "hack test -q --feature-powerset --skip default -F arbitrary -F serde --ignore-unknown-features" [env] RUST_BACKTRACE = "1" diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 5b885669..e5746ce1 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -145,7 +145,7 @@ jobs: env: CARGO_TARGET_DIR: target/ - name: Build all feature sets - run: cargo hack build --workspace --feature-powerset --skip default --target ${{ matrix.target_triple }} --features arbitrary --ignore-unknown-features + run: cargo hack build --workspace --feature-powerset --skip default --target ${{ matrix.target_triple }} -F arbitrary -F serde --ignore-unknown-features env: RUSTFLAGS: ${{ matrix.rustflags }} - name: Download rsonpath-test artifact @@ -154,7 +154,7 @@ jobs: name: ${{ needs.test-gen.outputs.artifact-name }} path: ${{ needs.test-gen.outputs.artifact-path }} - name: Test all feature sets - run: cargo hack test --workspace --feature-powerset --skip default --target ${{ matrix.target_triple }} --features arbitrary --ignore-unknown-features + run: cargo hack test --workspace --feature-powerset --skip default --target ${{ matrix.target_triple }} -F arbitrary -F serde --ignore-unknown-features env: RUSTFLAGS: ${{ matrix.rustflags }} diff --git a/Cargo.lock b/Cargo.lock index 7cd3bae4..de2233f9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -122,6 +122,12 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + [[package]] name = "bit-set" version = "0.8.0" @@ -137,6 +143,12 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.6.0" @@ -198,6 +210,33 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + [[package]] name = "clap" version = "4.5.23" @@ -325,6 +364,12 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + [[package]] name = "darling" version = "0.20.10" @@ -539,7 +584,7 @@ version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b903b73e45dc0c6c596f2d37eccece7c1c8bb6e4407b001096387c63d0d93724" dependencies = [ - "bitflags", + "bitflags 2.6.0", "libc", "libgit2-sys", "log", @@ -552,6 +597,16 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" +[[package]] +name = "half" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888" +dependencies = [ + "cfg-if", + "crunchy", +] + [[package]] name = "hashbrown" version = "0.15.2" @@ -750,6 +805,8 @@ dependencies = [ "console", "lazy_static", "linked-hash-map", + "ron", + "serde", "similar", ] @@ -813,7 +870,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ - "bitflags", + "bitflags 2.6.0", "libc", "redox_syscall", ] @@ -967,6 +1024,12 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb37767f6569cd834a413442455e0f066d0d522de8630436e2a1761d9726ba56" +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "percent-encoding" version = "2.3.1" @@ -1021,7 +1084,7 @@ checksum = "14cae93065090804185d3b75f0bf93b8eeda30c7a9b4a33d3bdb3988d6229e50" dependencies = [ "bit-set", "bit-vec", - "bitflags", + "bitflags 2.6.0", "lazy_static", "num-traits", "rand 0.8.5", @@ -1154,7 +1217,7 @@ version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" dependencies = [ - "bitflags", + "bitflags 2.6.0", ] [[package]] @@ -1186,6 +1249,39 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "rmp" +version = "0.8.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "228ed7c16fa39782c3b3468e974aec2795e9089153cd08ee2e9aefb3613334c4" +dependencies = [ + "byteorder", + "num-traits", + "paste", +] + +[[package]] +name = "rmp-serde" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52e599a477cf9840e92f2cde9a7189e67b42c57532749bf90aea6ec10facd4db" +dependencies = [ + "byteorder", + "rmp", + "serde", +] + +[[package]] +name = "ron" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88073939a61e5b7680558e6be56b419e208420c2adb92be54921fa6b72283f1a" +dependencies = [ + "base64", + "bitflags 1.3.2", + "serde", +] + [[package]] name = "rsonpath" version = "0.9.3" @@ -1208,14 +1304,19 @@ dependencies = [ name = "rsonpath-lib" version = "0.9.3" dependencies = [ - "arbitrary", "cfg-if", + "ciborium", + "insta", "itertools", "log", "memmap2", "pretty_assertions", "proptest", + "rmp-serde", "rsonpath-syntax", + "rsonpath-syntax-proptest", + "serde", + "serde_json", "smallvec", "static_assertions", "test-case", @@ -1228,16 +1329,29 @@ name = "rsonpath-syntax" version = "0.3.2" dependencies = [ "arbitrary", + "ciborium", "insta", "nom", "owo-colors 4.1.0", "pretty_assertions", "proptest", + "rmp-serde", + "rsonpath-syntax-proptest", + "serde", + "serde_json", "test-case", "thiserror", "unicode-width", ] +[[package]] +name = "rsonpath-syntax-proptest" +version = "0.3.2" +dependencies = [ + "proptest", + "rsonpath-syntax", +] + [[package]] name = "rsonpath-test" version = "0.9.3" @@ -1292,7 +1406,7 @@ version = "0.38.42" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85" dependencies = [ - "bitflags", + "bitflags 2.6.0", "errno", "libc", "linux-raw-sys", @@ -1343,18 +1457,18 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.216" +version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e" +checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.216" +version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e" +checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" dependencies = [ "proc-macro2", "quote", @@ -1411,6 +1525,9 @@ name = "smallvec" version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +dependencies = [ + "serde", +] [[package]] name = "snapbox" diff --git a/Cargo.toml b/Cargo.toml index 5daa6677..7500ab03 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ members = [ "crates/rsonpath", "crates/rsonpath-lib", "crates/rsonpath-syntax", + "crates/rsonpath-syntax-proptest", "crates/rsonpath-test", ] @@ -24,15 +25,24 @@ edition = "2021" # Project crates rsonpath-lib = { version = "0.9.3", path = "./crates/rsonpath-lib", package = "rsonpath-lib", default-features = false } rsonpath-syntax = { version = "0.3.2", path = "./crates/rsonpath-syntax" } +rsonpath-syntax-proptest = { version = "0.3.2", path = "./crates/rsonpath-syntax-proptest" } # Main dependencies arbitrary = { version = "1.4.1" } cfg-if = "1.0.0" log = "0.4.22" thiserror = "2.0.9" # Dev-dependencies +ciborium = { version = "0.2.2", default-features = false } +insta = { version = "1.41.1" } itertools = "0.13.0" pretty_assertions = "1.4.1" proptest = "1.5.0" +rmp-serde = "1.3.0" +serde = { version = "1.0.217" } +serde_json = { version = "1.0.133", default-features = true, features = [ + "std", + "float_roundtrip", +] } test-case = "3.3.1" [workspace.lints.rust] diff --git a/Justfile b/Justfile index fe457ee6..53dbcdcd 100644 --- a/Justfile +++ b/Justfile @@ -267,7 +267,7 @@ assert-benchmarks-committed: # === RELEASE === -# Execute prerequisits for a release for the given version. +# Execute prerequisites for a release for the given version. release ver: cargo update just release-patch {{ver}} @@ -276,7 +276,7 @@ release ver: cargo build cargo +nightly fuzz build -# Execute prerequisits for a release of `rsonpath-syntax` for the given version. +# Execute prerequisites for a release of `rsonpath-syntax` for the given version. release-syntax ver: #!/usr/bin/env nu let ver = "{{ver}}"; @@ -297,10 +297,12 @@ release-readme: #!/usr/bin/env nu let rsonpath_deps = (cargo tree --package rsonpath --edges normal --edges build --depth 1 --target=all --all-features); let rsonpath_lib_deps = (cargo tree --package rsonpath-lib --edges normal --edges build --depth 1 --target=all --all-features); + let rsonpath_syntax_deps = (cargo tree --package rsonpath-syntax --edges normal --edges build --depth 1 --target=all --all-features); let rsonpath_full_deps = (cargo tree --package rsonpath --edges normal --edges build --target=all --all-features); let params = [ [$rsonpath_deps, "rsonpath", "./README.md"], [$rsonpath_lib_deps, "rsonpath-lib", "./README.md"], + [$rsonpath_syntax_deps, "rsonpath-syntax", "./crates/rsonpath-syntax/README.md"], [$rsonpath_lib_deps, "rsonpath-lib", "./crates/rsonpath-lib/README.md"], [$rsonpath_full_deps, "rsonpath-full", "./README.md"] ]; diff --git a/crates/rsonpath-benchmarks/src/dataset.rs b/crates/rsonpath-benchmarks/src/dataset.rs index cc7b4b89..a7ccc0ce 100644 --- a/crates/rsonpath-benchmarks/src/dataset.rs +++ b/crates/rsonpath-benchmarks/src/dataset.rs @@ -248,6 +248,7 @@ where S: Into>, { use indicatif::{ProgressBar, ProgressStyle}; + #[allow(clippy::literal_string_with_formatting_args)] let style = ProgressStyle::with_template( "{msg} {spinner} {wide_bar:.green/white} {bytes:>12}/{total_bytes:>12} ({bytes_per_sec:>12}) {eta:>10}", ) diff --git a/crates/rsonpath-lib/Cargo.toml b/crates/rsonpath-lib/Cargo.toml index bc4fdf59..be9516ef 100644 --- a/crates/rsonpath-lib/Cargo.toml +++ b/crates/rsonpath-lib/Cargo.toml @@ -27,25 +27,30 @@ rustdoc-args = ["--cfg", "docsrs"] all-features = true [dependencies] -arbitrary = { workspace = true, features = ["derive"], optional = true } cfg-if = { workspace = true } log = { workspace = true } memmap2 = "0.9.5" rsonpath-syntax = { workspace = true } +serde = { workspace = true, optional = true, features = ["derive", "rc"] } smallvec = { version = "1.13.2", features = ["union"] } static_assertions = "1.1.0" thiserror = { workspace = true } vector-map = "1.0.1" [dev-dependencies] +ciborium = { workspace = true, default-features = true } +insta = { workspace = true, features = ["ron"] } itertools = { workspace = true } pretty_assertions = { workspace = true } proptest = { workspace = true } +rmp-serde = { workspace = true } +rsonpath-syntax-proptest = { workspace = true } +serde_json = { workspace = true } test-case = { workspace = true } [features] default = ["simd"] -arbitrary = ["dep:arbitrary"] +serde = ["dep:serde", "smallvec/serde", "rsonpath-syntax/serde"] simd = [] [[example]] diff --git a/crates/rsonpath-lib/README.md b/crates/rsonpath-lib/README.md index 4b97adad..9b827f48 100644 --- a/crates/rsonpath-lib/README.md +++ b/crates/rsonpath-lib/README.md @@ -46,26 +46,26 @@ and how data flows from the user's input (query, document) through the pipeline The `simd` feature is enabled by default and is recommended to make use of the performance benefits of the project. -The `arbitrary` feature is optional and enables the [`arbitrary` dependency](https://lib.rs/crates/arbitrary), -which provides an implementation of [`Arbitrary`](https://docs.rs/arbitrary/latest/arbitrary/trait.Arbitrary.html) -for the query struct. +The `serde` feature is optional and enables the [`serde` dependency](https://lib.rs/crates/serde), +which allows serializing and deserializing the engine after compilation. Note: the binary format of the engine +is expected to evolve and so changing it is considered a minor update for semver purposes. ## Dependencies Showing direct dependencies. ```bash -cargo tree --package rsonpath-lib --edges normal --depth 1 +cargo tree --package rsonpath-lib --edges normal --depth 1 --target=all --all-features ``` ```ini rsonpath-lib v0.9.3 (/home/mat/src/rsonpath/crates/rsonpath-lib) -├── arbitrary v1.4.1 ├── cfg-if v1.0.0 ├── log v0.4.22 ├── memmap2 v0.9.5 ├── rsonpath-syntax v0.3.2 (/home/mat/src/rsonpath/crates/rsonpath-syntax) +|-- serde v1.0.217 ├── smallvec v1.13.2 ├── static_assertions v1.1.0 ├── thiserror v2.0.9 @@ -76,10 +76,9 @@ rsonpath-lib v0.9.3 (/home/mat/src/rsonpath/crates/rsonpath-lib) ### Justification - `cfg-if` – used to support SIMD and no-SIMD versions. -- `memchr` – rapid, SIMDified substring search for fast-forwarding to labels. +- `log` – Rust standard logging idiom. - `memmap2` – for fast reading of source files via a memory map instead of buffered copies. -- `nom` – for parser implementation. -- `replace_with` – for safe handling of internal classifier state when switching classifiers. +- `serde` – optional dependency for serialization and deserialization of compiled engines. - `smallvec` – crucial for small-stack performance. - `static_assertions` – additional reliability by some constant assumptions validated at compile time. - `thiserror` – idiomatic `Error` implementations. diff --git a/crates/rsonpath-lib/src/automaton.rs b/crates/rsonpath-lib/src/automaton.rs index 86b5e45f..58b96337 100644 --- a/crates/rsonpath-lib/src/automaton.rs +++ b/crates/rsonpath-lib/src/automaton.rs @@ -15,6 +15,7 @@ use smallvec::SmallVec; use std::{fmt::Display, ops::Index, sync::Arc}; /// A minimal, deterministic automaton representing a JSONPath query. +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Clone, Debug, PartialEq, Eq)] pub struct Automaton { states: Vec, @@ -25,6 +26,7 @@ pub type MemberTransition = (Arc, State); /// Transition on elements of an array with indices specified by either a single index /// or a simple slice expression. +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Clone, Debug, PartialEq, Eq)] pub struct ArrayTransition { label: ArrayTransitionLabel, @@ -32,6 +34,7 @@ pub struct ArrayTransition { } /// Represent the distinct methods of moving on a match between states. +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, Copy, PartialEq, Clone, Eq)] pub(super) enum ArrayTransitionLabel { /// Transition on the n-th element of an array, with n specified by a [`JsonUInt`]. @@ -44,6 +47,7 @@ pub(super) enum ArrayTransitionLabel { /// /// Contains transitions triggered by matching member names or array indices, and a fallback transition /// triggered when none of the labelled transitions match. +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, Clone)] pub struct StateTable { attributes: StateAttributes, @@ -52,6 +56,7 @@ pub struct StateTable { fallback_state: State, } +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, Copy, PartialEq, Clone, Eq)] pub(crate) struct SimpleSlice { start: JsonUInt, diff --git a/crates/rsonpath-lib/src/automaton/state.rs b/crates/rsonpath-lib/src/automaton/state.rs index 78925f5b..10ef1ecf 100644 --- a/crates/rsonpath-lib/src/automaton/state.rs +++ b/crates/rsonpath-lib/src/automaton/state.rs @@ -30,6 +30,7 @@ pub(crate) struct StateAttributesBuilder { } /// A set of attributes that can be associated with a [`State`]. +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default)] pub struct StateAttributes(u8); @@ -167,6 +168,7 @@ impl StateAttributes { } /// State of an [`Automaton`](`super::Automaton`). Thin wrapper over a state's identifier. +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] pub struct State( // Only `pub` for the `automaton` module, since it needs to construct and deconstruct the wrapper. diff --git a/crates/rsonpath-lib/src/engine.rs b/crates/rsonpath-lib/src/engine.rs index a2abde6c..fd05198b 100644 --- a/crates/rsonpath-lib/src/engine.rs +++ b/crates/rsonpath-lib/src/engine.rs @@ -6,9 +6,11 @@ pub mod error; mod head_skipping; pub mod main; +mod select_root_query; +#[cfg(feature = "serde")] +mod serde; mod tail_skipping; pub use main::MainEngine as RsonpathEngine; -mod select_root_query; use self::error::EngineError; use crate::{ diff --git a/crates/rsonpath-lib/src/engine/main.rs b/crates/rsonpath-lib/src/engine/main.rs index b617d033..f5ec5b26 100644 --- a/crates/rsonpath-lib/src/engine/main.rs +++ b/crates/rsonpath-lib/src/engine/main.rs @@ -69,7 +69,6 @@ use smallvec::{smallvec, SmallVec}; /// /// The engine is stateless, meaning that it can be executed /// on any number of separate inputs, even on separate threads. - #[derive(Clone, Debug)] pub struct MainEngine { automaton: Automaton, @@ -78,6 +77,15 @@ pub struct MainEngine { static_assertions::assert_impl_all!(MainEngine: Send, Sync); +impl MainEngine { + /// Get a reference to the underlying compiled query. + #[inline(always)] + #[must_use] + pub fn automaton(&self) -> &Automaton { + &self.automaton + } +} + impl Compiler for MainEngine { type E = Self; diff --git a/crates/rsonpath-lib/src/engine/serde.rs b/crates/rsonpath-lib/src/engine/serde.rs new file mode 100644 index 00000000..24b2b1b1 --- /dev/null +++ b/crates/rsonpath-lib/src/engine/serde.rs @@ -0,0 +1,211 @@ +use crate::{ + automaton::Automaton, + engine::{main::MainEngine, Compiler}, +}; +use serde::{ + de::{self, Visitor}, + ser::SerializeTuple, + Deserialize, Serialize, +}; + +#[derive(Debug, Serialize, Deserialize)] +enum BinaryVersion { + /// Placeholder for any version in the past, used for tests. + Past, + /// Introduced binary serialization in v0.9.4. + V1, +} + +impl de::Expected for BinaryVersion { + fn fmt(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + match *self { + Self::Past => write!(formatter, "Past"), + Self::V1 => write!(formatter, "v0.9.4"), + } + } +} + +impl Serialize for MainEngine { + #[inline] + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let mut tuple_ser = serializer.serialize_tuple(2)?; + tuple_ser.serialize_element(&BinaryVersion::V1)?; + tuple_ser.serialize_element(&self.automaton())?; + tuple_ser.end() + } +} + +impl<'de> Deserialize<'de> for MainEngine { + #[inline] + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let automaton = deserializer.deserialize_tuple(2, EngineVisitor)?; + Ok(Self::from_compiled_query(automaton)) + } +} + +struct EngineVisitor; + +impl<'de> Visitor<'de> for EngineVisitor { + type Value = Automaton; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(formatter, "the binary version and the Automaton") + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: serde::de::SeqAccess<'de>, + { + let version = seq.next_element::()?; + match version { + Some(BinaryVersion::V1) => (), + Some(v) => return Err(de::Error::custom(format!("binary version {:?} is incompatible", v))), + None => return Err(de::Error::missing_field("version")), + } + let automaton = seq.next_element::()?; + match automaton { + Some(a) => Ok(a), + None => Err(de::Error::missing_field("automaton")), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + automaton::Automaton, + engine::{Compiler, RsonpathEngine}, + }; + use serde::{ser::SerializeTuple, Serialize, Serializer}; + use std::error::Error; + + #[test] + fn automaton_round_trip() -> Result<(), Box> { + let query_str = "$..phoneNumbers[*].number"; + let query = rsonpath_syntax::parse(query_str)?; + let automaton = Automaton::new(&query)?; + let engine = RsonpathEngine::from_compiled_query(automaton.clone()); + + let json_string = serde_json::to_string(&engine)?; + + let round_trip: RsonpathEngine = serde_json::from_str(&json_string)?; + + assert_eq!(&automaton, round_trip.automaton()); + + Ok(()) + } + + #[test] + fn deserializing_from_older_version() -> Result<(), Box> { + struct OldEngine { + automaton: Automaton, + } + impl Serialize for OldEngine { + #[inline] + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut tuple_ser = serializer.serialize_tuple(2)?; + tuple_ser.serialize_element(&BinaryVersion::Past)?; + tuple_ser.serialize_element(&self.automaton)?; + tuple_ser.end() + } + } + + let query_str = "$..phoneNumbers[*].number"; + let query = rsonpath_syntax::parse(query_str)?; + let automaton = Automaton::new(&query)?; + let engine = OldEngine { automaton }; + + let json_string = serde_json::to_string(&engine)?; + + match serde_json::from_str::(&json_string) { + Ok(_) => panic!("expected error"), + Err(e) => assert!(e.to_string().contains("binary version Past is incompatible")), + } + + Ok(()) + } + + mod proptests { + use super::{Automaton, Compiler, RsonpathEngine}; + use pretty_assertions::assert_eq; + use proptest::prelude::*; + use rsonpath_syntax_proptest::{ArbitraryJsonPathQuery, ArbitraryJsonPathQueryParams}; + + proptest! { + #[test] + fn main_engine_cbor_roundtrips(ArbitraryJsonPathQuery { parsed, .. } in prop::arbitrary::any_with::( + ArbitraryJsonPathQueryParams { + only_rsonpath_supported_subset: true, + ..Default::default() + } + )) { + use std::io; + struct ReadBuf<'a> { + buf: &'a [u8], + idx: usize, + } + impl<'a> io::Read for &mut ReadBuf<'a> { + fn read(&mut self, buf: &mut [u8]) -> Result { + let len = std::cmp::min(self.buf.len() - self.idx, buf.len()); + buf.copy_from_slice(&self.buf[self.idx..self.idx + len]); + self.idx += len; + Ok(len) + } + } + + let automaton = Automaton::new(&parsed)?; + let engine = RsonpathEngine::from_compiled_query(automaton.clone()); + + let mut buf = vec![]; + ciborium::into_writer(&engine, &mut buf)?; + + let mut read = ReadBuf { buf: &buf, idx: 0 }; + let engine_deser = ciborium::from_reader::(&mut read)?; + + assert_eq!(&automaton, engine_deser.automaton()); + } + + #[test] + fn main_engine_json_roundtrips(ArbitraryJsonPathQuery { parsed, .. } in prop::arbitrary::any_with::( + ArbitraryJsonPathQueryParams { + only_rsonpath_supported_subset: true, + ..Default::default() + } + )) { + let automaton = Automaton::new(&parsed)?; + let engine = RsonpathEngine::from_compiled_query(automaton.clone()); + + let json_str = serde_json::to_string(&engine)?; + let engine_deser = serde_json::from_str::(&json_str)?; + + assert_eq!(&automaton, engine_deser.automaton()); + } + + #[test] + fn main_engine_message_pack_roundtrips(ArbitraryJsonPathQuery { parsed, .. } in prop::arbitrary::any_with::( + ArbitraryJsonPathQueryParams { + only_rsonpath_supported_subset: true, + ..Default::default() + } + )) { + let automaton = Automaton::new(&parsed)?; + let engine = RsonpathEngine::from_compiled_query(automaton.clone()); + + let buf = rmp_serde::to_vec(&engine)?; + let engine_deser = rmp_serde::from_slice::(&buf)?; + + assert_eq!(&automaton, engine_deser.automaton()); + } + } + } +} diff --git a/crates/rsonpath-lib/src/string_pattern.rs b/crates/rsonpath-lib/src/string_pattern.rs index c4957efe..f6a9c688 100644 --- a/crates/rsonpath-lib/src/string_pattern.rs +++ b/crates/rsonpath-lib/src/string_pattern.rs @@ -3,6 +3,7 @@ use rsonpath_syntax::str::JsonString; /// String pattern coming from a JSONPath query that can be matched against strings in a JSON. /// /// Right now the only pattern is matching against a given [`JsonString`]. +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, Clone)] pub struct StringPattern(JsonString); diff --git a/crates/rsonpath-lib/tests/engine_serialization_snapshots.rs b/crates/rsonpath-lib/tests/engine_serialization_snapshots.rs new file mode 100644 index 00000000..8fa71e52 --- /dev/null +++ b/crates/rsonpath-lib/tests/engine_serialization_snapshots.rs @@ -0,0 +1,42 @@ +#[cfg(feature = "serde")] +mod ron { + use insta::assert_ron_snapshot; + use rsonpath::engine::{Compiler, RsonpathEngine}; + use std::error::Error; + + fn engine(string: &str) -> Result> { + let query = rsonpath_syntax::parse(string)?; + let engine = RsonpathEngine::compile_query(&query)?; + Ok(engine) + } + + #[test] + fn empty_query() -> Result<(), Box> { + assert_ron_snapshot!(&engine("$")?); + Ok(()) + } + + #[test] + fn readme_query() -> Result<(), Box> { + assert_ron_snapshot!(&engine("$.jsonpath[*]")?); + Ok(()) + } + + #[test] + fn jsonpath_example_query() -> Result<(), Box> { + assert_ron_snapshot!(&engine("$..phoneNumbers[*].number")?); + Ok(()) + } + + #[test] + fn real_life_query() -> Result<(), Box> { + assert_ron_snapshot!(&engine("$.personal.details.contact.information.phones.home")?); + Ok(()) + } + + #[test] + fn slice() -> Result<(), Box> { + assert_ron_snapshot!(&engine("$..entries[3:5:7]")?); + Ok(()) + } +} diff --git a/crates/rsonpath-lib/tests/snapshots/engine_serialization_snapshots__ron__empty_query.snap b/crates/rsonpath-lib/tests/snapshots/engine_serialization_snapshots__ron__empty_query.snap new file mode 100644 index 00000000..7b6e463d --- /dev/null +++ b/crates/rsonpath-lib/tests/snapshots/engine_serialization_snapshots__ron__empty_query.snap @@ -0,0 +1,21 @@ +--- +source: crates/rsonpath-lib/tests/engine_serialization_snapshots.rs +expression: "&engine(\"$\")?" +snapshot_kind: text +--- +(V1, Automaton( + states: [ + StateTable( + attributes: StateAttributes(2), + member_transitions: [], + array_transitions: [], + fallback_state: State(0), + ), + StateTable( + attributes: StateAttributes(1), + member_transitions: [], + array_transitions: [], + fallback_state: State(0), + ), + ], +)) diff --git a/crates/rsonpath-lib/tests/snapshots/engine_serialization_snapshots__ron__jsonpath_example_query.snap b/crates/rsonpath-lib/tests/snapshots/engine_serialization_snapshots__ron__jsonpath_example_query.snap new file mode 100644 index 00000000..f9042dd5 --- /dev/null +++ b/crates/rsonpath-lib/tests/snapshots/engine_serialization_snapshots__ron__jsonpath_example_query.snap @@ -0,0 +1,84 @@ +--- +source: crates/rsonpath-lib/tests/engine_serialization_snapshots.rs +expression: "&engine(\"$..phoneNumbers[*].number\")?" +snapshot_kind: text +--- +(V1, Automaton( + states: [ + StateTable( + attributes: StateAttributes(2), + member_transitions: [], + array_transitions: [], + fallback_state: State(0), + ), + StateTable( + attributes: StateAttributes(0), + member_transitions: [ + (StringPattern(JsonString( + quoted: "\"phoneNumbers\"", + )), State(2)), + ], + array_transitions: [], + fallback_state: State(1), + ), + StateTable( + attributes: StateAttributes(0), + member_transitions: [ + (StringPattern(JsonString( + quoted: "\"phoneNumbers\"", + )), State(4)), + ], + array_transitions: [], + fallback_state: State(3), + ), + StateTable( + attributes: StateAttributes(8), + member_transitions: [ + (StringPattern(JsonString( + quoted: "\"phoneNumbers\"", + )), State(2)), + (StringPattern(JsonString( + quoted: "\"number\"", + )), State(6)), + ], + array_transitions: [], + fallback_state: State(1), + ), + StateTable( + attributes: StateAttributes(8), + member_transitions: [ + (StringPattern(JsonString( + quoted: "\"phoneNumbers\"", + )), State(4)), + (StringPattern(JsonString( + quoted: "\"number\"", + )), State(5)), + ], + array_transitions: [], + fallback_state: State(3), + ), + StateTable( + attributes: StateAttributes(9), + member_transitions: [ + (StringPattern(JsonString( + quoted: "\"phoneNumbers\"", + )), State(2)), + (StringPattern(JsonString( + quoted: "\"number\"", + )), State(6)), + ], + array_transitions: [], + fallback_state: State(1), + ), + StateTable( + attributes: StateAttributes(1), + member_transitions: [ + (StringPattern(JsonString( + quoted: "\"phoneNumbers\"", + )), State(2)), + ], + array_transitions: [], + fallback_state: State(1), + ), + ], +)) diff --git a/crates/rsonpath-lib/tests/snapshots/engine_serialization_snapshots__ron__readme_query.snap b/crates/rsonpath-lib/tests/snapshots/engine_serialization_snapshots__ron__readme_query.snap new file mode 100644 index 00000000..e40ccafb --- /dev/null +++ b/crates/rsonpath-lib/tests/snapshots/engine_serialization_snapshots__ron__readme_query.snap @@ -0,0 +1,37 @@ +--- +source: crates/rsonpath-lib/tests/engine_serialization_snapshots.rs +expression: "&engine(\"$.jsonpath[*]\")?" +snapshot_kind: text +--- +(V1, Automaton( + states: [ + StateTable( + attributes: StateAttributes(2), + member_transitions: [], + array_transitions: [], + fallback_state: State(0), + ), + StateTable( + attributes: StateAttributes(4), + member_transitions: [ + (StringPattern(JsonString( + quoted: "\"jsonpath\"", + )), State(2)), + ], + array_transitions: [], + fallback_state: State(0), + ), + StateTable( + attributes: StateAttributes(8), + member_transitions: [], + array_transitions: [], + fallback_state: State(3), + ), + StateTable( + attributes: StateAttributes(1), + member_transitions: [], + array_transitions: [], + fallback_state: State(0), + ), + ], +)) diff --git a/crates/rsonpath-lib/tests/snapshots/engine_serialization_snapshots__ron__real_life_query.snap b/crates/rsonpath-lib/tests/snapshots/engine_serialization_snapshots__ron__real_life_query.snap new file mode 100644 index 00000000..29240077 --- /dev/null +++ b/crates/rsonpath-lib/tests/snapshots/engine_serialization_snapshots__ron__real_life_query.snap @@ -0,0 +1,81 @@ +--- +source: crates/rsonpath-lib/tests/engine_serialization_snapshots.rs +expression: "&engine(\"$.personal.details.contact.information.phones.home\")?" +snapshot_kind: text +--- +(V1, Automaton( + states: [ + StateTable( + attributes: StateAttributes(2), + member_transitions: [], + array_transitions: [], + fallback_state: State(0), + ), + StateTable( + attributes: StateAttributes(4), + member_transitions: [ + (StringPattern(JsonString( + quoted: "\"personal\"", + )), State(2)), + ], + array_transitions: [], + fallback_state: State(0), + ), + StateTable( + attributes: StateAttributes(4), + member_transitions: [ + (StringPattern(JsonString( + quoted: "\"details\"", + )), State(3)), + ], + array_transitions: [], + fallback_state: State(0), + ), + StateTable( + attributes: StateAttributes(4), + member_transitions: [ + (StringPattern(JsonString( + quoted: "\"contact\"", + )), State(4)), + ], + array_transitions: [], + fallback_state: State(0), + ), + StateTable( + attributes: StateAttributes(4), + member_transitions: [ + (StringPattern(JsonString( + quoted: "\"information\"", + )), State(5)), + ], + array_transitions: [], + fallback_state: State(0), + ), + StateTable( + attributes: StateAttributes(4), + member_transitions: [ + (StringPattern(JsonString( + quoted: "\"phones\"", + )), State(6)), + ], + array_transitions: [], + fallback_state: State(0), + ), + StateTable( + attributes: StateAttributes(12), + member_transitions: [ + (StringPattern(JsonString( + quoted: "\"home\"", + )), State(7)), + ], + array_transitions: [], + fallback_state: State(0), + ), + StateTable( + attributes: StateAttributes(1), + member_transitions: [], + array_transitions: [], + fallback_state: State(0), + ), + ], +)) diff --git a/crates/rsonpath-lib/tests/snapshots/engine_serialization_snapshots__ron__slice.snap b/crates/rsonpath-lib/tests/snapshots/engine_serialization_snapshots__ron__slice.snap new file mode 100644 index 00000000..212a92ec --- /dev/null +++ b/crates/rsonpath-lib/tests/snapshots/engine_serialization_snapshots__ron__slice.snap @@ -0,0 +1,50 @@ +--- +source: crates/rsonpath-lib/tests/engine_serialization_snapshots.rs +expression: "&engine(\"$..entries[3:5:7]\")?" +snapshot_kind: text +--- +(V1, Automaton( + states: [ + StateTable( + attributes: StateAttributes(2), + member_transitions: [], + array_transitions: [], + fallback_state: State(0), + ), + StateTable( + attributes: StateAttributes(0), + member_transitions: [ + (StringPattern(JsonString( + quoted: "\"entries\"", + )), State(2)), + ], + array_transitions: [], + fallback_state: State(1), + ), + StateTable( + attributes: StateAttributes(56), + member_transitions: [ + (StringPattern(JsonString( + quoted: "\"entries\"", + )), State(2)), + ], + array_transitions: [ + ArrayTransition( + label: Index(JsonUInt(3)), + target: State(3), + ), + ], + fallback_state: State(1), + ), + StateTable( + attributes: StateAttributes(1), + member_transitions: [ + (StringPattern(JsonString( + quoted: "\"entries\"", + )), State(2)), + ], + array_transitions: [], + fallback_state: State(1), + ), + ], +)) diff --git a/crates/rsonpath-syntax-proptest/Cargo.toml b/crates/rsonpath-syntax-proptest/Cargo.toml new file mode 100644 index 00000000..023105fe --- /dev/null +++ b/crates/rsonpath-syntax-proptest/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "rsonpath-syntax-proptest" +authors.workspace = true +license.workspace = true +repository.workspace = true +homepage.workspace = true +edition.workspace = true +version = "0.3.2" +description = "A JSONPath parser." +readme = "README.md" +keywords = ["json", "jsonpath", "query", "search", "parser"] +exclude = ["tests", "src/cli.rs"] +categories = ["parser-implementations", "text-processing"] +rust-version = "1.67.1" + +[package.metadata.docs.rs] +rustdoc-args = ["--cfg", "docsrs"] +all-features = true + +[dependencies] +rsonpath-syntax = { workspace = true } +proptest = { workspace = true } + +[features] +default = [] + +[lints] +workspace = true diff --git a/crates/rsonpath-syntax-proptest/README.md b/crates/rsonpath-syntax-proptest/README.md new file mode 100644 index 00000000..bc98af85 --- /dev/null +++ b/crates/rsonpath-syntax-proptest/README.md @@ -0,0 +1,33 @@ +# `rsonpath-syntax-proptest` – `proptest::Arbitrary` implementation for [`rsonpath-syntax`](https://crates.io/crates/rsonpath-syntax) + +[![Rust](https://github.com/V0ldek/rsonpath/actions/workflows/rust.yml/badge.svg)](https://github.com/V0ldek/rsonpath/actions/workflows/rust.yml) +[![docs.rs](https://img.shields.io/docsrs/rsonpath-syntax-proptest?logo=docs.rs)](https://docs.rs/crate/rsonpath-syntax-proptest/latest) + +[![Crates.io](https://img.shields.io/crates/v/rsonpath-syntax-proptest?logo=docs.rs)](https://crates.io/crates/rsonpath-syntax-proptest) + +[![License](https://img.shields.io/crates/l/rsonpath)](https://choosealicense.com/licenses/mit/) + +Utilities for property testing with types in `rsonpath-syntax`. + +The crate exposes two types, `ArbitraryJsonPathQuery` and `ArbitraryJsonPathQueryParam`. +The `ArbitraryJsonPathQuery` implements [`proptest::Arbitrary`](https://docs.rs/proptest/latest/proptest/arbitrary/trait.Arbitrary.html) +which generates an arbitrary JSONPath query string representation and [`rsonpath_syntax::JsonPathQuery`](https://docs.rs/rsonpath-syntax/latest/rsonpath_syntax/) object. + +## Usage + +This is mostly used for internal testing of `rsonpath-lib` and `rsonpath-syntax`, but it is in general useful +for property-testing or fuzzing code that relies on JSONPath queries as input. + +Example usage with `proptest`: + +```rust +use proptest::prelude::*; +use rsonpath_syntax_proptest::ArbitraryJsonPathQuery; + +proptest! { + #[test] + fn example(ArbitraryJsonPathQuery { parsed, string } in prop::arbitrary::any::()) { + // Your test using parsed (JsonPathQuery) and/or string (String). + } +} +``` diff --git a/crates/rsonpath-syntax-proptest/src/lib.rs b/crates/rsonpath-syntax-proptest/src/lib.rs new file mode 100644 index 00000000..e8840eed --- /dev/null +++ b/crates/rsonpath-syntax-proptest/src/lib.rs @@ -0,0 +1,721 @@ +//! Utilities for property testing with types in [`rsonpath-syntax`](https://docs.rs/rsonpath-syntax/latest/rsonpath_syntax/). +//! +//! Implementation of [`proptest::arbitrary::Arbitrary`] +//! for JSONPath queries via the [`ArbitraryJsonPathQuery`] struct. +//! +//! # Examples +//! +//! ```rust +//! use proptest::prelude::*; +//! use rsonpath_syntax_proptest::ArbitraryJsonPathQuery; +//! +//! proptest! { +//! #[test] +//! fn example(ArbitraryJsonPathQuery { parsed, string } in prop::arbitrary::any::()) { +//! assert_eq!(parsed, rsonpath_syntax::parse(&string)?); +//! } +//! } +//! ``` +use proptest::{option, prelude::*, strategy}; +use rsonpath_syntax::{ + builder::SliceBuilder, num::JsonInt, str::JsonString, JsonPathQuery, LogicalExpr, Segment, Selector, Selectors, +}; + +/// A valid JSONPath string and the [`JsonPathQuery`] object parsed from it. +/// +/// This is the struct through which an [`proptest::arbitrary::Arbitrary`] implementation +/// for [`JsonPathQuery`] is provided. +#[derive(Debug)] +pub struct ArbitraryJsonPathQuery { + /// The JSONPath query string. + pub string: String, + /// The parsed JSONPath query. + pub parsed: JsonPathQuery, +} + +/// Parameters of the [`ArbitraryJsonPathQuery`] [`Arbitrary`](`proptest::arbitrary::Arbitrary`) implementation. +#[derive(Debug)] +pub struct ArbitraryJsonPathQueryParams { + /// Depth limit for recursion for generated JSONPath queries. Default value: 3. + /// + /// JSONPath queries are recursive since a filter selector can contain an arbitrary JSONPath query. + /// This limits the nesting level. + /// See [proptest::strategy::Strategy::prop_recursive] for details of how this affects the recursive generation. + pub recursive_depth: u32, + /// Desired size in terms of tree nodes of a generated JSONPath query. Default value: 10. + /// + /// JSONPath queries are recursive since a filter selector can contain an arbitrary JSONPath query. + /// This limits the nesting level. + /// See [proptest::strategy::Strategy::prop_recursive] for details of how this affects the recursive generation. + pub desired_size: u32, + /// Limit on the number of segments in the generated query, not including the initial root `$` selector. + /// Default value: 10. + pub max_segments: usize, + /// Minimum number of selectors in each of the generated segments. Default value: 1. + /// + /// Must be non-zero. + pub min_selectors: usize, + /// Maximum number of selectors in each of the generated segments. Default value: 5. + /// + /// Must be at least `min_segments`. + pub max_selectors: usize, + /// Only generate query elements that are supported by the [`rsonpath`](https://docs.rs/rsonpath-lib/latest/rsonpath/) crate. + /// + /// Consult rsonpath's documentation for details on what this entails. + pub only_rsonpath_supported_subset: bool, +} + +impl ArbitraryJsonPathQuery { + #[inline] + #[must_use] + pub fn new(string: String, parsed: JsonPathQuery) -> Self { + Self { string, parsed } + } +} + +impl Default for ArbitraryJsonPathQueryParams { + #[inline] + fn default() -> Self { + Self { + only_rsonpath_supported_subset: false, + recursive_depth: 3, + desired_size: 10, + max_segments: 10, + min_selectors: 1, + max_selectors: 5, + } + } +} + +impl proptest::arbitrary::Arbitrary for ArbitraryJsonPathQuery { + type Parameters = ArbitraryJsonPathQueryParams; + type Strategy = BoxedStrategy; + + #[inline] + fn arbitrary_with(args: Self::Parameters) -> Self::Strategy { + assert!(args.min_selectors > 0); + assert!(args.max_selectors >= args.min_selectors); + + if args.only_rsonpath_supported_subset { + rsonpath_valid_query(&args).prop_map(|x| Self::new(x.0, x.1)).boxed() + } else { + any_valid_query(&args).prop_map(|x| Self::new(x.0, x.1)).boxed() + } + } +} + +/* Approach: we generate the query string bit by bit, each time attaching what the expected + * typed element is. At the end we have the input string all ready, and the expected + * parser result can be easily obtained by a 1-1 translation. + */ +#[derive(Debug, Clone)] +enum PropSegment { + // .* + ShortChildWildcard, + // .name + ShortChildName(JsonString), + // ..* + ShortDescendantWildcard, + // ..name + ShortDescendantName(JsonString), + // [] + BracketedChild(Vec), + // ..[] + BracketedDescendant(Vec), +} + +#[derive(Debug, Clone)] +enum PropSelector { + Wildcard, + Name(JsonString), + Index(JsonInt), + Slice(Option, Option, Option), + Filter(LogicalExpr), +} + +fn any_valid_query(props: &ArbitraryJsonPathQueryParams) -> impl Strategy { + let ArbitraryJsonPathQueryParams { + min_selectors, + max_selectors, + max_segments, + recursive_depth, + desired_size, + .. + } = *props; + + prop::collection::vec(any_segment(None, min_selectors, max_selectors), 0..max_segments) + .prop_map(map_prop_segments) + .prop_recursive(recursive_depth, desired_size, 5, move |query_strategy| { + prop::collection::vec( + any_segment(Some(query_strategy), min_selectors, max_selectors), + 0..max_segments, + ) + .prop_map(map_prop_segments) + }) +} + +fn rsonpath_valid_query(props: &ArbitraryJsonPathQueryParams) -> impl Strategy { + let ArbitraryJsonPathQueryParams { max_segments, .. } = *props; + prop::collection::vec(rsonpath_valid_segment(), 0..max_segments).prop_map(map_prop_segments) +} + +fn map_prop_segments(segments: Vec<(String, PropSegment)>) -> (String, JsonPathQuery) { + let mut s = "$".to_string(); + let mut v = vec![]; + + for (segment_s, segment) in segments { + s.push_str(&segment_s); + match segment { + PropSegment::ShortChildWildcard => v.push(Segment::Child(Selectors::one(Selector::Wildcard))), + PropSegment::ShortChildName(n) => v.push(Segment::Child(Selectors::one(Selector::Name(n)))), + PropSegment::ShortDescendantWildcard => v.push(Segment::Descendant(Selectors::one(Selector::Wildcard))), + PropSegment::ShortDescendantName(n) => v.push(Segment::Descendant(Selectors::one(Selector::Name(n)))), + PropSegment::BracketedChild(ss) => v.push(Segment::Child(Selectors::many( + ss.into_iter().map(map_prop_selector).collect(), + ))), + PropSegment::BracketedDescendant(ss) => v.push(Segment::Descendant(Selectors::many( + ss.into_iter().map(map_prop_selector).collect(), + ))), + } + } + + (s, JsonPathQuery::from_iter(v)) +} + +fn map_prop_selector(s: PropSelector) -> Selector { + match s { + PropSelector::Wildcard => Selector::Wildcard, + PropSelector::Name(n) => Selector::Name(n), + PropSelector::Index(i) => Selector::Index(i.into()), + PropSelector::Slice(start, end, step) => Selector::Slice({ + let mut builder = SliceBuilder::new(); + if let Some(start) = start { + builder.with_start(start); + } + if let Some(step) = step { + builder.with_step(step); + } + if let Some(end) = end { + builder.with_end(end); + } + builder.into() + }), + PropSelector::Filter(logical) => Selector::Filter(logical), + } +} + +fn any_segment( + recursive_query_strategy: Option>, + min_selectors: usize, + max_selectors: usize, +) -> impl Strategy { + return prop_oneof![ + strategy::Just((".*".to_string(), PropSegment::ShortChildWildcard)), + strategy::Just(("..*".to_string(), PropSegment::ShortDescendantWildcard)), + any_short_name().prop_map(|name| (format!(".{name}"), PropSegment::ShortChildName(JsonString::new(&name)))), + any_short_name().prop_map(|name| ( + format!("..{name}"), + PropSegment::ShortDescendantName(JsonString::new(&name)) + )), + prop::collection::vec( + any_selector(recursive_query_strategy.clone()), + min_selectors..max_selectors + ) + .prop_map(|reprs| { + let mut s = "[".to_string(); + let v = collect_reprs(reprs, &mut s); + s.push(']'); + (s, PropSegment::BracketedChild(v)) + }), + prop::collection::vec(any_selector(recursive_query_strategy), min_selectors..max_selectors).prop_map(|reprs| { + let mut s = "..[".to_string(); + let v = collect_reprs(reprs, &mut s); + s.push(']'); + (s, PropSegment::BracketedDescendant(v)) + }), + ]; + + fn collect_reprs(reprs: Vec<(String, PropSelector)>, s: &mut String) -> Vec { + let mut result = Vec::with_capacity(reprs.len()); + let mut first = true; + for (repr_s, prop_selector) in reprs { + if !first { + s.push(','); + } + first = false; + s.push_str(&repr_s); + result.push(prop_selector); + } + result + } +} + +fn rsonpath_valid_segment() -> impl Strategy { + prop_oneof![ + strategy::Just((".*".to_string(), PropSegment::ShortChildWildcard)), + strategy::Just(("..*".to_string(), PropSegment::ShortDescendantWildcard)), + any_short_name().prop_map(|name| (format!(".{name}"), PropSegment::ShortChildName(JsonString::new(&name)))), + any_short_name().prop_map(|name| ( + format!("..{name}"), + PropSegment::ShortDescendantName(JsonString::new(&name)) + )), + rsonpath_valid_selector().prop_map(|repr| { + let mut s = "[".to_string(); + s.push_str(&repr.0); + s.push(']'); + (s, PropSegment::BracketedChild(vec![repr.1])) + }), + rsonpath_valid_selector().prop_map(|repr| { + let mut s = "..[".to_string(); + s.push_str(&repr.0); + s.push(']'); + (s, PropSegment::BracketedDescendant(vec![repr.1])) + }), + ] +} + +fn any_selector( + recursive_query_strategy: Option>, +) -> impl Strategy { + prop_oneof![ + strategy::Just(("*".to_string(), PropSelector::Wildcard)), + strings::any_json_string().prop_map(|(raw, s)| (raw, PropSelector::Name(s))), + any_json_int().prop_map(|(raw, i)| (raw, PropSelector::Index(i))), + any_slice().prop_map(|(raw, a, b, c)| (raw, PropSelector::Slice(a, b, c))), + filters::any_logical_expr(recursive_query_strategy) + .prop_map(|(raw, expr)| (format!("?{raw}"), PropSelector::Filter(expr))) + ] +} + +fn rsonpath_valid_selector() -> impl Strategy { + prop_oneof![ + strategy::Just(("*".to_string(), PropSelector::Wildcard)), + strings::any_json_string().prop_map(|(raw, s)| (raw, PropSelector::Name(s))), + rsonpath_valid_json_int().prop_map(|(raw, i)| (raw, PropSelector::Index(i))), + rsonpath_valid_slice().prop_map(|(raw, a, b, c)| (raw, PropSelector::Slice(a, b, c))), + ] +} + +fn any_json_int() -> impl Strategy { + (-((1_i64 << 53) + 1)..((1_i64 << 53) - 1)).prop_map(|i| (i.to_string(), JsonInt::try_from(i).unwrap())) +} + +fn rsonpath_valid_json_int() -> impl Strategy { + (0..((1_i64 << 53) - 1)).prop_map(|i| (i.to_string(), JsonInt::try_from(i).unwrap())) +} + +fn any_slice() -> impl Strategy, Option, Option)> { + ( + option::of(any_json_int()), + option::of(any_json_int()), + option::of(any_json_int()), + ) + .prop_map(|(a, b, c)| { + let mut s = String::new(); + let a = a.map(|(a_s, a_i)| { + s.push_str(&a_s); + a_i + }); + s.push(':'); + let b = b.map(|(b_s, b_i)| { + s.push_str(&b_s); + b_i + }); + s.push(':'); + let c = c.map(|(c_s, c_i)| { + s.push_str(&c_s); + c_i + }); + (s, a, b, c) + }) +} + +fn rsonpath_valid_slice() -> impl Strategy, Option, Option)> { + ( + option::of(rsonpath_valid_json_int()), + option::of(rsonpath_valid_json_int()), + option::of(rsonpath_valid_json_int()), + ) + .prop_map(|(a, b, c)| { + let mut s = String::new(); + let a = a.map(|(a_s, a_i)| { + s.push_str(&a_s); + a_i + }); + s.push(':'); + let b = b.map(|(b_s, b_i)| { + s.push_str(&b_s); + b_i + }); + s.push(':'); + let c = c.map(|(c_s, c_i)| { + s.push_str(&c_s); + c_i + }); + (s, a, b, c) + }) +} + +fn any_short_name() -> impl Strategy { + r"([A-Za-z]|_|[^\u0000-\u007F])([A-Za-z0-9]|_|[^\u0000-\u007F])*" +} + +mod strings { + use proptest::{prelude::*, sample::SizeRange}; + use rsonpath_syntax::str::JsonString; + + #[derive(Debug, PartialEq, Eq, Clone, Copy)] + enum JsonStringToken { + EncodeNormally(char), + ForceUnicodeEscape(char), + } + + #[derive(Debug, PartialEq, Eq, Clone, Copy)] + enum JsonStringTokenEncodingMode { + SingleQuoted, + DoubleQuoted, + } + + impl JsonStringToken { + fn raw(self) -> char { + match self { + Self::EncodeNormally(x) | Self::ForceUnicodeEscape(x) => x, + } + } + + fn encode(self, mode: JsonStringTokenEncodingMode) -> String { + return match self { + Self::EncodeNormally('\u{0008}') => r"\b".to_owned(), + Self::EncodeNormally('\t') => r"\t".to_owned(), + Self::EncodeNormally('\n') => r"\n".to_owned(), + Self::EncodeNormally('\u{000C}') => r"\f".to_owned(), + Self::EncodeNormally('\r') => r"\r".to_owned(), + Self::EncodeNormally('"') => match mode { + JsonStringTokenEncodingMode::DoubleQuoted => r#"\""#.to_owned(), + JsonStringTokenEncodingMode::SingleQuoted => r#"""#.to_owned(), + }, + Self::EncodeNormally('\'') => match mode { + JsonStringTokenEncodingMode::DoubleQuoted => r#"'"#.to_owned(), + JsonStringTokenEncodingMode::SingleQuoted => r#"\'"#.to_owned(), + }, + Self::EncodeNormally('/') => r"\/".to_owned(), + Self::EncodeNormally('\\') => r"\\".to_owned(), + Self::EncodeNormally(c @ ..='\u{001F}') | Self::ForceUnicodeEscape(c) => encode_unicode_escape(c), + Self::EncodeNormally(c) => c.to_string(), + }; + + fn encode_unicode_escape(c: char) -> String { + let mut buf = [0; 2]; + let enc = c.encode_utf16(&mut buf); + let mut res = String::new(); + for x in enc { + res += &format!("\\u{x:0>4x}"); + } + res + } + } + } + + pub(super) fn any_json_string() -> impl Strategy { + prop_oneof![ + Just(JsonStringTokenEncodingMode::SingleQuoted), + Just(JsonStringTokenEncodingMode::DoubleQuoted) + ] + .prop_flat_map(|mode| { + prop::collection::vec( + (prop::char::any(), prop::bool::ANY).prop_map(|(c, b)| { + if b { + JsonStringToken::EncodeNormally(c) + } else { + JsonStringToken::ForceUnicodeEscape(c) + } + }), + SizeRange::default(), + ) + .prop_map(move |v| { + let q = match mode { + JsonStringTokenEncodingMode::SingleQuoted => '\'', + JsonStringTokenEncodingMode::DoubleQuoted => '"', + }; + let mut s = String::new(); + let mut l = String::new(); + for x in v { + s += &x.encode(mode); + l.push(x.raw()); + } + (format!("{q}{s}{q}"), JsonString::new(&l)) + }) + }) + } +} + +mod filters { + use proptest::{num, prelude::*, strategy}; + use rsonpath_syntax::{ + num::{JsonFloat, JsonNumber}, + str::JsonString, + Comparable, ComparisonExpr, ComparisonOp, JsonPathQuery, Literal, LogicalExpr, SingularJsonPathQuery, + SingularSegment, TestExpr, + }; + + pub(super) fn any_logical_expr( + test_query_strategy: Option>, + ) -> impl Strategy { + any_atomic_logical_expr(test_query_strategy).prop_recursive(8, 32, 2, |inner| { + prop_oneof![ + (inner.clone(), proptest::bool::ANY).prop_map(|((s, f), force_paren)| ( + match f { + LogicalExpr::Test(_) if !force_paren => format!("!{s}"), + _ => format!("!({s})"), + }, + LogicalExpr::Not(Box::new(f)) + )), + (inner.clone(), inner.clone(), proptest::bool::ANY, proptest::bool::ANY).prop_map( + |((lhs_s, lhs_e), (rhs_s, rhs_e), force_left_paren, force_right_paren)| { + let put_left_paren = force_left_paren || matches!(lhs_e, LogicalExpr::Or(_, _)); + let put_right_paren = + force_right_paren || matches!(rhs_e, LogicalExpr::Or(_, _) | LogicalExpr::And(_, _)); + let s = match (put_left_paren, put_right_paren) { + (true, true) => format!("({lhs_s})&&({rhs_s})"), + (true, false) => format!("({lhs_s})&&{rhs_s}"), + (false, true) => format!("{lhs_s}&&({rhs_s})"), + (false, false) => format!("{lhs_s}&&{rhs_s}"), + }; + (s, LogicalExpr::And(Box::new(lhs_e), Box::new(rhs_e))) + } + ), + (inner.clone(), inner.clone(), proptest::bool::ANY, proptest::bool::ANY).prop_map( + |((lhs_s, lhs_e), (rhs_s, rhs_e), force_left_paren, force_right_paren)| { + let put_left_paren = force_left_paren || matches!(lhs_e, LogicalExpr::Or(_, _)); + let put_right_paren = force_right_paren; + let s = match (put_left_paren, put_right_paren) { + (true, true) => format!("({lhs_s})||({rhs_s})"), + (true, false) => format!("({lhs_s})||{rhs_s}"), + (false, true) => format!("{lhs_s}||({rhs_s})"), + (false, false) => format!("{lhs_s}||{rhs_s}"), + }; + (s, LogicalExpr::Or(Box::new(lhs_e), Box::new(rhs_e))) + } + ) + ] + }) + } + + fn any_atomic_logical_expr( + test_query_strategy: Option>, + ) -> impl Strategy { + if let Some(test_query_strategy) = test_query_strategy { + prop_oneof![ + any_test(test_query_strategy).prop_map(|(s, t)| (s, LogicalExpr::Test(t))), + any_comparison().prop_map(|(s, c)| (s, LogicalExpr::Comparison(c))), + ] + .boxed() + } else { + any_comparison() + .prop_map(|(s, c)| (s, LogicalExpr::Comparison(c))) + .boxed() + } + } + + fn any_test( + test_query_strategy: BoxedStrategy<(String, JsonPathQuery)>, + ) -> impl Strategy { + (proptest::bool::ANY, test_query_strategy).prop_map(|(relative, (mut s, q))| { + if relative { + assert_eq!(s.as_bytes()[0], b'$'); + s.replace_range(0..1, "@"); + (s, TestExpr::Relative(q)) + } else { + (s, TestExpr::Absolute(q)) + } + }) + } + + fn any_comparison() -> impl Strategy { + (any_comparable(), any_comparison_op(), any_comparable()).prop_map( + |((lhs_s, lhs_e), (op_s, op_e), (rhs_s, rhs_e))| { + ( + format!("{lhs_s}{op_s}{rhs_s}"), + ComparisonExpr::from_parts(lhs_e, op_e, rhs_e), + ) + }, + ) + } + + fn any_comparable() -> impl Strategy { + prop_oneof![ + any_literal().prop_map(|(s, l)| (s, Comparable::Literal(l))), + (proptest::bool::ANY, any_singular_query()).prop_map(|(relative, (mut s, q))| { + if relative { + assert_eq!(s.as_bytes()[0], b'$'); + s.replace_range(0..1, "@"); + (s, Comparable::RelativeSingularQuery(q)) + } else { + (s, Comparable::AbsoluteSingularQuery(q)) + } + }) + ] + } + + prop_compose! { + fn any_singular_query()(segments in prop::collection::vec(any_singular_segment(), 0..10)) -> (String, SingularJsonPathQuery) { + let mut s = "$".to_string(); + let mut v = vec![]; + + for (segment_s, segment) in segments { + s.push_str(&segment_s); + v.push(segment); + } + + (s, SingularJsonPathQuery::from_iter(v)) + } + } + + fn any_singular_segment() -> impl Strategy { + prop_oneof![ + super::any_json_int().prop_map(|(s, i)| (format!("[{s}]"), SingularSegment::Index(i.into()))), + super::any_short_name().prop_map(|n| (format!(".{n}"), SingularSegment::Name(JsonString::new(&n)))), + super::strings::any_json_string().prop_map(|(s, n)| (format!("[{s}]"), SingularSegment::Name(n))), + ] + } + + fn any_literal() -> impl Strategy { + prop_oneof![ + strategy::Just(("null".to_string(), Literal::Null)), + proptest::bool::ANY.prop_map(|b| (b.to_string(), Literal::Bool(b))), + any_json_number().prop_map(|(s, n)| (s, Literal::Number(n))), + super::strings::any_json_string().prop_map(|(raw, s)| (raw, Literal::String(s))) + ] + } + + fn any_json_number() -> impl Strategy { + prop_oneof![ + super::any_json_int().prop_map(|(s, i)| (s, JsonNumber::Int(i))), + any_json_float().prop_map(|(s, f)| (s, JsonNumber::Float(f))), + ] + .prop_map(|(x, n)| (x, n.normalize())) + } + + fn any_json_float() -> impl Strategy { + // We first generate the target f64 value we want and then pick one of its possible string reprs. + // Because an "int float" is also interesting we generate those half the time. + // If there is no exponent, there is only one possible representation. + // If we include an exponent we can move the floating point however far we want one way or the other. + return prop_oneof![ + any_float().prop_map(|f| (f.to_string(), JsonFloat::try_from(f).unwrap())), + any_float() + .prop_flat_map(|f| arbitrary_exp_repr(f).prop_map(move |s| (s, JsonFloat::try_from(f).unwrap()))), + ]; + + fn any_float() -> impl Strategy { + prop_oneof![num::f64::NORMAL, num::f64::NORMAL.prop_map(f64::trunc)] + } + + fn arbitrary_exp_repr(f: f64) -> impl Strategy { + let s = f.to_string(); + let fp_pos: isize = s.find('.').unwrap_or(s.len()).try_into().unwrap(); + let num_digits = if fp_pos == s.len() as isize { + s.len() + } else { + s.len() - 1 + } - if f.is_sign_negative() { + // Subtract the minus char. + 1 + } else { + 0 + }; + (-1024..=1024_isize, proptest::bool::ANY, proptest::bool::ANY).prop_map( + move |(exp, force_sign, uppercase_e)| { + let new_pos = fp_pos - exp; + let mut res = String::new(); + if f.is_sign_negative() { + res.push('-'); + } + let mut orig_digits = s.chars().filter(|c| *c != '.'); + + // There are three cases: + // 1. the new point is before all existing digits; + // in this case we need to append 0.000... at the front + // 2. the new point position falls within the existing string; + // this is straightforward, we just emplace it there + // 3. the new point is after all existing digits; + // in this case we need to append 0000... at the end + // After this operation we need to manually trim the zeroes. + if new_pos <= 0 { + // Case 1. + res.push_str("0."); + for _ in 0..(-new_pos) { + res.push('0'); + } + for orig_digit in orig_digits { + res.push(orig_digit); + } + } else if new_pos < num_digits as isize { + // Case 2. + let mut pos = 0; + let mut pushed_non_zero = false; + loop { + if pos == new_pos { + if !pushed_non_zero { + res.push('0'); + } + pushed_non_zero = true; + res.push('.'); + } else { + let Some(orig_digit) = orig_digits.next() else { break }; + if orig_digit == '0' { + if pushed_non_zero { + res.push(orig_digit); + } + } else { + pushed_non_zero = true; + res.push(orig_digit); + } + } + pos += 1; + } + } else if f == 0.0 { + // Case 3. special case. + // Note that -0.0 is handled here as well, as it is equal to 0.0 and the sign is appended above. + res.push('0'); + } else { + // Case 3. + // First skip zeroes. There has to be at least one non-zero since we checked + // f == 0.0 above. + let skip_zeroes = orig_digits.skip_while(|x| *x == '0'); + for orig_digit in skip_zeroes { + res.push(orig_digit); + } + for _ in 0..(new_pos - num_digits as isize) { + res.push('0'); + } + } + + res.push(if uppercase_e { 'E' } else { 'e' }); + + if exp > 0 { + if force_sign { + res.push('+'); + } + res.push_str(&exp.to_string()); + } else { + res.push_str(&exp.to_string()); + } + + res + }, + ) + } + } + + fn any_comparison_op() -> impl Strategy { + prop_oneof![ + strategy::Just(("==".to_string(), ComparisonOp::EqualTo)), + strategy::Just(("!=".to_string(), ComparisonOp::NotEqualTo)), + strategy::Just(("<".to_string(), ComparisonOp::LessThan)), + strategy::Just((">".to_string(), ComparisonOp::GreaterThan)), + strategy::Just(("<=".to_string(), ComparisonOp::LesserOrEqualTo)), + strategy::Just((">=".to_string(), ComparisonOp::GreaterOrEqualTo)), + ] + } +} diff --git a/crates/rsonpath-syntax/Cargo.toml b/crates/rsonpath-syntax/Cargo.toml index 899e05c4..9a9b87ef 100644 --- a/crates/rsonpath-syntax/Cargo.toml +++ b/crates/rsonpath-syntax/Cargo.toml @@ -21,19 +21,25 @@ all-features = true arbitrary = { workspace = true, features = ["derive"], optional = true } owo-colors = { version = "4.1.0", default-features = false, optional = true } nom = "7.1.3" +serde = { workspace = true, optional = true, features = ["derive"] } thiserror = { workspace = true } unicode-width = "0.2.0" [dev-dependencies] -insta = "1.41.1" +ciborium = { workspace = true, default-features = true } +insta = { workspace = true, features = ["ron"] } pretty_assertions = { workspace = true } proptest = { workspace = true } +rmp-serde = { workspace = true } +rsonpath-syntax-proptest = { workspace = true } +serde_json = { workspace = true } test-case = { workspace = true } [features] default = [] arbitrary = ["dep:arbitrary"] color = ["dep:owo-colors"] +serde = ["dep:serde"] [lints] workspace = true \ No newline at end of file diff --git a/crates/rsonpath-syntax/README.md b/crates/rsonpath-syntax/README.md index 7bb2b044..8127351e 100644 --- a/crates/rsonpath-syntax/README.md +++ b/crates/rsonpath-syntax/README.md @@ -22,10 +22,11 @@ For advanced usage consult the crate documentation. ## Feature flags -There are two optional features: +There are three optional features: - `arbitrary`, which enables a dependency on the [`arbitrary` crate](https://docs.rs/arbitrary/latest/arbitrary/) to provide `Arbitrary` implementations on query types; this is used e.g. for fuzzing. - `color`, which enables a dependency on the [`owo_colors` crate](https://docs.rs/owo-colors/latest/owo_colors/) to provide colorful `Display` representations of `ParseError` with the `colored` function. +- `serde`, which enables a dependency on the [`serde` crate](https://docs.rs/serde/latest/serde/) to provide serialization and deserialization of `JsonPathQuery` and all the underlying types. ## Examples @@ -42,3 +43,32 @@ However, these are fully supported, tested, and fuzzed. The planned roadmap is: - [ ] support functions (including type check) - [ ] polish the API - [ ] 1.0.0 stable release + +## Dependencies + +Showing direct dependencies. + +```bash +cargo tree --package rsonpath-lib --edges normal --depth 1 --target=all --all-features +``` + + +```ini +rsonpath-syntax v0.3.2 (/home/mat/src/rsonpath/crates/rsonpath-syntax) +├── arbitrary v1.4.1 +├── nom v7.1.3 +├── owo-colors v4.1.0 +├── serde v1.0.217 +├── thiserror v2.0.9 +└── unicode-width v0.2.0 +``` + + +### Justification + +- `arbitrary` – optional `Arbitrary` support for fuzzing. +- `nom` – combinator-based parsing used throughout the crate. +- `owo-colors` – optional feature for pretty error messages. +- `serde` – optional dependency for serialization and deserialization of compiled engines. +- `thiserror` – idiomatic `Error` implementations. +- `unicode-width` – used to display error messages correctly in presence of wider Unicode characters in the query string. diff --git a/crates/rsonpath-syntax/src/lib.rs b/crates/rsonpath-syntax/src/lib.rs index e5c43fd5..a7bd0e96 100644 --- a/crates/rsonpath-syntax/src/lib.rs +++ b/crates/rsonpath-syntax/src/lib.rs @@ -316,6 +316,7 @@ impl Parser { /// subsequent segments. #[derive(Debug, PartialEq, Eq, Clone, Hash)] #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum Segment { /// A child segment contains a sequence of selectors, /// each of which selects zero or more children of a node. @@ -343,6 +344,7 @@ impl<'a> arbitrary::Arbitrary<'a> for Selectors { /// /// Guaranteed to be non-empty. #[derive(Debug, PartialEq, Eq, Clone, Hash)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Selectors { inner: Vec, } @@ -351,6 +353,7 @@ pub struct Selectors { /// A selector produces one or more children/descendants of the node it is applied to. #[derive(Debug, PartialEq, Eq, Clone, Hash)] #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum Selector { /// A name selector selects at most one object member value under the key equal to the /// selector's [`JsonString`](str::JsonString). @@ -398,6 +401,7 @@ impl From for Selector { /// Directional index into a JSON array. #[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum Index { /// Zero-based index from the start of the array. FromStart(num::JsonUInt), @@ -432,6 +436,7 @@ impl> From for Index { /// Directional step offset within a JSON array. #[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum Step { /// Step forward by a given offset amount. Forward(num::JsonUInt), @@ -483,6 +488,7 @@ impl From for Step { /// ``` #[derive(Debug, PartialEq, Eq, Clone, Hash)] #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Slice { start: Index, end: Option, @@ -541,6 +547,7 @@ impl Default for Slice { /// JSON literal value available in comparison expressions of a filter selector. #[derive(Debug, PartialEq, Eq, Clone, Hash)] #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum Literal { /// [`JsonString`](str::JsonString) literal. String(str::JsonString), @@ -598,6 +605,7 @@ impl From for Literal { /// (OR, AND, NOT) store their children as [`Boxes`](Box). #[derive(Debug, PartialEq, Eq, Clone, Hash)] #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum LogicalExpr { /// Logical disjunction of two child expressions. Or(LogicalExprNode, LogicalExprNode), @@ -629,6 +637,7 @@ type LogicalExprNode = Box; /// Existence test based on a relative or absolute [`JsonPathQuery`]. #[derive(Debug, PartialEq, Eq, Clone, Hash)] #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum TestExpr { /// Relative test – query from the selected node. Relative(JsonPathQuery), @@ -656,6 +665,7 @@ pub enum TestExpr { /// ``` #[derive(Debug, PartialEq, Eq, Clone, Hash)] #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct ComparisonExpr { lhs: Comparable, op: ComparisonOp, @@ -695,6 +705,7 @@ impl ComparisonExpr { /// Comparison operator usable in a [`ComparisonExpr`]. #[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)] #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum ComparisonOp { /// Compares two values for equality; `==` EqualTo, @@ -713,6 +724,7 @@ pub enum ComparisonOp { /// One of the sides of a [`ComparisonExpr`], either a constant literal or a singular JSONPath query. #[derive(Debug, PartialEq, Eq, Clone, Hash)] #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum Comparable { /// Constant [`Literal`] value. Literal(Literal), @@ -739,6 +751,7 @@ impl From for Comparable { /// if it exists. #[derive(Debug, PartialEq, Eq, Clone, Hash)] #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct SingularJsonPathQuery { segments: Vec, } @@ -754,6 +767,7 @@ impl SingularJsonPathQuery { /// Segment allowed in a [`SingularJsonPathQuery`]. #[derive(Debug, PartialEq, Eq, Clone, Hash)] #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum SingularSegment { /// Child name selector. Equivalent of [`Selector::Name`]. Name(str::JsonString), @@ -783,6 +797,7 @@ impl From for Segment { /// JSONPath query structure represented by a sequence of [`Segments`](Segment). #[derive(Debug, PartialEq, Eq, Clone, Hash)] #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct JsonPathQuery { segments: Vec, } diff --git a/crates/rsonpath-syntax/src/num.rs b/crates/rsonpath-syntax/src/num.rs index 5e4e7378..c5a09c99 100644 --- a/crates/rsonpath-syntax/src/num.rs +++ b/crates/rsonpath-syntax/src/num.rs @@ -59,6 +59,7 @@ use std::{ /// let too_small = JsonInt::try_from(-(1_i64 << 53)).expect_err("out of range"); /// ``` #[derive(Clone, Copy, Default, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct JsonInt(i64); /// Unsigned interoperable JSON integer. @@ -84,6 +85,7 @@ pub struct JsonInt(i64); /// /// let too_big = JsonUInt::try_from(1_u64 << 53).expect_err("out of range"); /// ``` +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Clone, Copy, Default, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct JsonUInt(u64); @@ -110,6 +112,7 @@ pub struct JsonUInt(u64); /// let too_big = JsonNonZeroUInt::try_from(1_u64 << 53).expect_err("out of range"); /// ``` #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct JsonNonZeroUInt(NonZeroU64); /// IEEE 754 conformant floating-point number expressible in JSON. @@ -129,6 +132,7 @@ pub struct JsonNonZeroUInt(NonZeroU64); /// [`JsonFloat`] is [`TryInto`], where the conversion succeeds if and only if /// the float is an exactly representable integer in the range \[-253+1, (253)-1]. #[derive(Clone, Copy, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct JsonFloat(f64); // This is correct since the allowed values for `JsonFloat` don't include NaNs or infinities. @@ -182,6 +186,7 @@ impl std::hash::Hash for JsonFloat { /// ``` #[derive(Clone, Copy, Debug)] #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum JsonNumber { /// A [`JsonInt`] number. Int(JsonInt), diff --git a/crates/rsonpath-syntax/src/parser.rs b/crates/rsonpath-syntax/src/parser.rs index f281b6cb..4a9acb07 100644 --- a/crates/rsonpath-syntax/src/parser.rs +++ b/crates/rsonpath-syntax/src/parser.rs @@ -840,7 +840,7 @@ fn string(mode: StringParseMode) -> impl FnMut(&str) -> IResult<&str, JsonString let low = read_hexadecimal_escape(q_len, i, chars)?; match low { 0xDC00..=0xDFFF => { - let n = ((raw_c - 0xD800) << 10 | (low - 0xDC00)) + 0x10000; + let n = (((raw_c - 0xD800) << 10) | (low - 0xDC00)) + 0x10000; Ok(char::from_u32(n).expect("high and low surrogate pair is always a valid char")) } _ => Err(SyntaxError::new( diff --git a/crates/rsonpath-syntax/src/str.rs b/crates/rsonpath-syntax/src/str.rs index ea3cbae7..a50df347 100644 --- a/crates/rsonpath-syntax/src/str.rs +++ b/crates/rsonpath-syntax/src/str.rs @@ -18,6 +18,7 @@ /// assert_eq!(needle.unquoted(), "needle"); /// assert_eq!(needle.quoted(), "\"needle\""); /// ``` +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Clone)] pub struct JsonString { quoted: String, diff --git a/crates/rsonpath-syntax/tests/query_parser_tests.proptest-regressions b/crates/rsonpath-syntax/tests/query_parser_tests.proptest-regressions index 801105bb4c1e61b16266ed80a3bf5d38511c7c2f..507c7f476a84c7029ff27f8719afc9d15a8fa23f 100644 GIT binary patch literal 27985 zcmdsg>01?Nwl808{{+755C)}nTf64k2SU<0raOsACoxIqwAh;(qX|kHFo_{Mir|ET z1C9vd0D_7GgD5yX4}9^f|G_PqeU`F^2!O-?+13yPx=GOG5$xH1R@si4K+* zY;DFkjSbsku$%0!F%9D)`5V_w=(2BEhUo-~lG1F+Ff~n+T*WtoRA5Smu1TIED}kz6 zwrR;JO_OEa^%V4web;jA#H)#>Esa~Ze~Eb;0P9yfniJKDl1&x0wG|bcOK6Hf3M5TS zNuG(J4A%&JOLlZcR~_Gxjg+LP6y5i1+cjlZQB~EHE&Rs#mLWNc%oEOV&TzhCYUGNGa;$*{(+y z-*CR-257#cCGdkFo5qbS$ux9hCBIW?D9>@YhilSBlEnC`Zt^cpS?GnAnuVXH?^&(@ zkQ)Or0Sy$0u98ZN;ekqULsRl^)#8gR@kOH9f*{}vfwu!A8-W;B5ug|XK8cp<`7-tQ z97&-|U}?3r>u@JYiu(aQG0hl=lz7U&OpoUFJlnEMe}@TNPqL+KuK(l&J+^#)>wCth z+(%Z$^tOhkj5G1rFR!NN4&&VOQiPL~6j+9lt>STbg#C&1I~=1-bDm76xq)I=eE)q)Ruxs3uRCaqN z!}Ghgi7nsJqN%EzF?7GMQvE ze)$HrkH*1!*X9OT8|Xay-3OV>%mj6HZONu4;7ZWXw{+)1#%2Hgac_o6LbGMr&F=gz z`3+5g;kAHAQWP7soXK6Cx~sp3H;&;DZPQ3t3`6hgz=izJPlq!mFHEycRmsi_JegKD z)e-@DPC&HixsH#HwhxIRwn<|0FjGtyzAng1anVRqJ%t;2mLjOa=8J9dW_UKykOabH ztj0vA65)8Z1H!_7VcUQb$Y~?{Wcq7*WJ<)1o@dcel4MXjNs{c3XhJ(B>jElSP`|`v zUSQLBn;Bx}3=n~3Sp{+zBXSqd@u{chN?Q6q#>eT_W^g@M6D%ot>D=(?%a|2{ROcZ8 z8{@)v6J_$&%sOZ!W0?e-+k97~Q^I`ZaA5{pDhQr+2$fLHe%&B+pE zP{FdI8VVqjG()vEmzS3&laCwXGk746_|vyakH5ZzrlSEN~8wbQq#x#pQpR(t>5PkEXT@yLU=cC$oms$!^eHuJ~X z2is_N=pJ3pHf5+KK-yIE%BoeXnj3fcuT;Ojy1ZOUg7%0XQz;dgS5u~Hq;x#{FZo-a zl1wr0s=%&q@~dLbObOxU2|dNUXZsd-k7N%iEn*QoCK41R8YnT}5W#{4wSq7Az zzGui|Fr*sXad>dI}D)Qv5CugPS^wk08)9XiAFhzdxK5Lx@?0Kmhds zwt}9EpJBR^)vMQ3t|4O}*1(?0r0jVHEe4jIuwrg1gme_l>$ z+=sM}XNfKF40&ddXA+uq(M)LMXf(d(nr51~+gBCK%(fJit;U!%WyX5}ACgy7n2&lm zQbCc*Mr}#CK;`1~Fi{Zr*n~xb1OgVU@ZJ|S;tlXAq={P~6@Cu1lq_`hOku?MYAlNQ z73^~ZEU8S5IU!&q#!?I@Fu*9Q%eDyU9!QBXeLs$1Qy~JvWymj(?BYUVZ{tBYqZ(1 zL5L+dPlv1O(A1GVr2vE=8Nfmt2t&dk;Ftvw zZQK~PZ2HW^u%Bryp6?KI8=kLNtdQo%dKZu3TFlD{ubSICLdbYh@F}_juVUL%89NI~14>&LcxVO4Gmo~Iz>H7lFoE?TMzk6?%d(EbcCGq>Xsr+V<| za+k-u=oXYEo{0Ct5ORfluzY?P_ykl;?}qJbQjFaoSzEGZt)ZB*nF7uwMNxFwOs_RT zJi2AdilrKoEGcQ0^`5QqaK1^Vtt0s=FhI*efItlB`#@586&?mDi!5edy#E7VWsPRsGH_BRY4;jk$KFBi{IA+fsv z0Wu+F!2|oCA~p#il4u%5@V~7x$s%wP3orqU%{CKcxL7tBzuW6J)EC&BbvDKzjX}uF z4WAswt!G)xvBHj_7i%zoEdDCyQKr*+@shSqN3_YzLSZ!WGg_wU_cXQzSyJB1t3k5)`_>mHb$OWU;@>Svf#BQ}| zD{jo%$|6!BiH6ReB@CND7ZOCMeY1cw!5%Q((Ue3S(1zvOTE*to-+vD)iU`*BZAekb z9b1P3Jr5l5CT!2x;Z<}yyJPk|-0QE=!h;930@GJevrkt`QMbcEsKpa8#*P5x-%f$wp{Lc|~ZEi{@Hi@v{M!z00#fHWv z1P-@uM{F2x-r8E<>^H9ZW84Jtq14cDlWnA zw-X6;M$_z$UDYko@!_k{(MKoOReg{fe{ifO+<)aN0#H%so(n|c`9s%Fe_EQI8ASVJ zd3AJtpnLAf;N7b5+{g$;kfQNJM}M0e>c3kT4z!*x-UL=k!$H7O8oo*=*8g>fT@QS$ zNxZS$TZDl{x631An**%By}g+cP1mJiCQMO4*GGOm!t`JJ8xbp|h&%^>R~3H!$WFa) zBP6^SyWzu!tq57o9lq8Yp6EU?AEydWw(edWq6+t)8q7q8E)Rlp$Qw#LH&Synbn$U` z=XytG?#R_H5uFMT4|n8F9liN&uJ`h6E_GfbA!;j*iRQ~SB zquiD#`9c&bJbST?15nEkGS{`|QU2!9-j%siXRlU;Cng?*{f}>N8$U7A01_wfpj%M|CYo$Rcv%AecU{&aX(<7@@yRku`%ZZOaA>H8Gjh>j} zCmKJuScG<>(`~21{=r@ZbmrRcT%k}-Zv0_K^l)rClRJ94k774-?X80xwaML?JVXH- zpx~t@{Lex6VRWo@#0%T{_J}x8?#|UqO~ekrVD1+S;GYF!EUn*1x`*V=&yqR6YiOwF znALw?t}IX#AXyNBKSLL=_U{;or*Lz1WXyX|f_berIfEFhTF5!Q%(gW;D=sre)oxT+i_FL=D5~}o_ExGBweRD^;Co2;9 z>CS_Rs@%iT8{e(SO?9?GGUi+FwWVv`twge}INUdUcpaQPg7WhDV`YsPdL_jw@?#U@ zHR-uSM~3h!Ja_KEnBi1Jq`8{nT>6((F+UUZ` zQ|R(`INi~e%(qQXgb(jc&9nTAx%jzt6up}}apQ5KI*5i(9Qm}o7?8~j5(?8*rP0LR z;i^RPwM0t^d-u`wgLAKA)|zne@l;t+G|_)}S9GcGZfV&HbXymlnK(UjAb;Zcq3FWx z?zh5yH^%Ze#s`vBEpvNLo=tpMm2VrmUpcpX;1W5_QUAcT=-To7`HoxHi^J)uqcus! z^v3$FuD^=^3)8HLv_?AdB3-rAXC~@`7Dy*J$^4>TOZ{pl%BE%?cG6o7jo&~`Ea<5e zC#IL7)cloeUHRKn`|~#rU!J{*^_N#gqeK1g|4>%G<$pZ51~gTKSI-UaOebb1-JO1Z z@8F}^o=v6YQS0?yB9GhClliIrZC|saTefnp{d#9rZFxoh@x-l}!I{CL>hRHtN5#=t z-?bIx?1ZK(o=ne9J(*tdUHKarK)98;%R|S?X}gNUBh&loCVyl2*6Y!+vqx8iqk~-^ zEKj{9dMbb6!o;Wb9 zGlMaalB8q2|Di1=w$aXEjrnHHnToI z)zJkvE9&n#HqTJccmwb6SXL{@$>$uNl%|VCk_o;@1>qsBpZ=wi2hBy0SPc~4MKs&> z{lG+IN=F3*LaDmx>*PKK2GaGK>*|W3>uSnQIhw8+isbmZ{%irk8;#D^=0>}5=Td_k z{!f$dtw|)4i7cu$Uf8aQiyhL55)>V*Dn;6872V(t1qgH}Y7o`}DoK-(iGsaoCNu2}awv0v5Co-o7_F~W3!RP~cx4p_s{>V4 z;@A%tQpC|LL^XVhfNQd$A(lqrPK`oIh+*nTQ)Iv3fm2RW@7+j0ALZ#jU#EIdmJh9bw4-QKZK}ViO9F5oZPK}Gnh>T zYY-qZWJLNA6!FWmLnix$@QBe%K zX*4=uHayWGE<6ZO;=oIx*bu^aj9%n>`C%LrjiYJ{zkqs^bHlVFKF3pCiFO2_DM%U9 zfUbzC>Go3!#38muK`77hL_7{ra1J_oBG*Hyk!o%xIbz2}=EmVz7xomnwpXf4Nl0y8 zO{6AxyL8i92p&yGeS&2w5+YCPW-2AabSVdEJwV5WKgjeOB8rC+6?&_2fYXvV@!;DM zp$2Ip+9f0hX+aboqjD1y^-)~TLLq#9jgb1{NHmWa=MCuc4R_{LRw2U?h~1Zi*&%fG z9M#q1OhF;)YY0XHu@Y8%RE}l8pzt@*9FHeS6@|(I3)w(`=__$mk9LgPA#N=rSq}8m z-$E`!w-;A}=->pZE%IzG@_@HV6+Z>ywsgr9k#CccL&A*^xoCkjcpRFIsT-vb7|%hz z2bUm7ZKbOz(kpJ9^0vKliaH`)&Ct7?kC|VdnkokndZ0*ga6C@eFn)kACNbZmM1tl! zo}c{;V}XxjuEwS!?Snt6$dGkSv1Cg|8qq){o`y$AR9I$8F;k|c$%s^L79meb2YYeN zlI0@#m7%IY9=E`?6c0w83l&JX@m1G|Gh_~Tat&DozipYB$3w82pwelt)*2~fMN|Z} zEklu2!`z%ipeXajpEjDA-0s-rL`rfs$I8CX z%>XJj(`74#N{p@aUPXPSn*}etjy0QTKm{l=s_irdYo=s_E2AJqaAV5Z{w*;U(!7Gh zCE6_7U3%!aymfA&!Un&ZiTvW&bIP@lpy!4%i!R6|{w4Mjb&!~hC`8P~($%wdZt4P8 zyj6m!lt3&0njdJ)O^(AX7@!IYTWiazmE9@YQ)*Aq9aL?RXw`JNKpLb08k1-g(d}ev0H2IP-JfYg8mL9D(7#hA~JERpySLXs5|h`p9w~ zLkZccDcD#TPnwyMfLRNvE)@JEn{xwuCNh~fXj6PQu9@*%qH8dTBxLJSu!xY!!=fZq)uFOZ0evAwDQ`}0pgna= zgu}BRW!4iTsHp%-C6oExx0!D<|9<^YMg_5o1wg9^g&|o&`o{V$h9$1VFc{N5Z#;Nc zwaE=AYZ3IpwU{<+3}&f$6)pQSgTJjO2_!OQg{nw?P{_Cu_bq@RAW>`*sf^i@&97C6 z5s=$Qp$(a>f>M#0tlg2)Z3fJe_~HU~@FOl>HdbANNfs(ei1<+78%uTA65C&)r3fgP z>f#!bfNNYm%!hX#Rq-+DRiYdM@(M5uRsl)T`vR+=D28}aNL80D0n{0z1QET#Rj59s z*+4tNU?h~lD zIE8K?>4cfI6e?j;io6EBP-bmd@KG!s!GblFn^6_FRz<4ZlpxztDl6gDnsj;%JRI28 zuP6VMY0hLmz?6n=IEB87arJvbi%SuJLo*GW9GM;@O~c zFg0lq4T=kyS@?pwX(+L^0aHmKb&jjBnG1aJMKyRlsv+rca}NQ&pWd(T(#EHRbD=~T_#zA-m4)h1psg_)vWmaJxm%7s-Sgn~vA z%T`k<2yJW-Qp~X^5&FOpgc=YpHgjJ&wz|-=W8U|xAp4VV5|WG{K;7ok zVKQ)4U9nTyxN6mwIGgOnf+FPzrZ4brTzMe9N_lP5MX@-eT2U1<%Z#Qtuv`g5=oY$g z-TgXhj37WNJKIeYrtCsp6nliIIVQ2>`Jy)15(QpfY=v{dfTMU?>{wwlvAHgsC$T)} z7N8HRDHiD(S?ADUx(|+L1QgsBA{ds4U>Vi2Vi%5!cM!F3HIzc5tB>+_PJaVn!z&c@ zkTK#Lt`e6)ryM%2fPG-V1BLc#-m#4-lWoFyJF(%mD1lRyDu^@Ta`93u9p;5t3fE-- zb4&m(YfG@5ScE~QuY+_ub2@58<+w13$%U3B_(xNcybQ<_)55U>f=f5aTeJe4y@)5G z8Hln!<@a(x+X*2_b?{6w!P8;Vaa2!H_6;e)Mj{>3IvX;-Yy9wS-PrPW=Hm^Szg@rc zaVGO;L;9TLou;F19_7FndoE`(O6-Tll&;!kHNwZyTmVR8$GVT});wSH=%@z)h1Bs{ zZomd4P$#e@>oos=>^T*8u10eeqKaXL9(|Et+F(INv4p(hAF z)<=c|(;U_-Y*U#1k+N3B`iKYJwIq7@?RLEmR37HV?KWqR*%zs(L;7F$TdwJos=V4q%P= zT)^nzydJ!3JW_pB0cJBBe-9(d_)7jLli85Fa`-IGtx(htH-_xuID(nkBx_XOXu$B% zQJZ60!lgxg115)9fQ={wv85z*&OGgj$lrpw&aEX2Xb{XwJOP2W6`frPrd~W;!TuFt z6)G@;gg05W`zcnq4u@P2F(IAf!3edoLZ(U5>w+%fNek(X!bDn&VlGss0#wKFd6BU6 zaa>8j#lSFQ3yxz+0I4Uy5Ux{P<&9tgMzv5ESm=zvD@GxDEM;&xQb%R=+RpcRcrv64 zej}m2s#;GsHf|)25u6!8f=k^cO_BiX%yd(tH7*q7kiN1x}$#o2ec(elwG7 zoy3Lsh^U|)%r(b=M8}XsUa{{$IHK+14RkdeW*}rBlt?7&1#q{aZgm;4!Hu}HY%M69 zjG%yU%qt%O?8aF zZpgqkVt&;jko zHLv@r860=IxT-4L#F#f?NrhjYFzV6%EKdTX3rN86Swb|a9uig91RIpfdxI0dG@{SG zGMgo$4Z;aAB?+bIup0;P_PGt#~-V4-Ki=pmIb87mI$bimlg;+ zK%5apT!`b8v|iYgu1;PB;a?mjz)4j6nnx$?&ARo}5VhNiAZB=i)B#>2&#Cwcyw_xp ztV=w{VP?Ac2tPfeP-t985%zU(dXW1{p5Rf%7F-?|ZBqa!c70-D5nD<`DGQdPQz{gI zB)utyiIurv8H)qVp6iOfv9^NPDQqmUEstj~_Dstq>jA?f5I`iLi)09V6b2o^6?zTb zv1YYsrwz(NE@+*rQ_@qs8lngZ_C zMZQMXi4G7;V_D)MB!Hn5gc4$z34;(GkfV|$rgaD$175ZyTIiv0 z>paJbxdFH!rGlfFY`yp>`NeVz9O2U{bVUS4ar%;Q>;<0KEogJ#SP*UDyk#sH?P>o6CStQ~F0|?;R zRz~eMgNd*Qx|?c%coQ{RqEBP2Ybk+sJ`v6f zGP3hBm#_g-mP|R*hw;xR_K-EuDBEG;vk9!$;E0fPgvxjB=ygliCC;5#3i%_X2$3&^mjruG!M~YC){Th4 z_}RDVHZXPAS6Wpy&dFi`Mb%OUk|DaTAd~m~_jKYD$G<5W_&QWFI^c5&h;L^$(7h|A zP>D|Z5=Z9{jm3>`nm7oR$PC26PcYDX=sv4wqde;1KN6Ott`Hl#fXfX6--k;0Ljq;%uvl z?<2&aruf7J$BfbWOyoDE zlm%~w(XSySNN8xtWIi!7pAhC@xn#@+tR08PWH^m1SQZo%D;p~|4_#Tvx&pa?(n*LJ zK2BHyuZCU7;VNz^k_u!Fz$iy3@yT>)fwto_)6YnSc^O5@8iY*BiG+A{tbl}ykvy6V zY(awAg{>L~svzblK5Ih<;6^2fP_R48KiDjyyYQ6pQ4I_NA69;w2a}B|bTaU<5s3K` zdW8%%U2sN$E>3)2kX>H{oG_MvLYJ!Wp7vn2=MQveiPQ82QD)hj%UOWNtRv1_fD)Km zY>6~9KEdEr)72ER0H4`l*^BRyfH$BIFh8YT0dIW5ODEWX0)xBR;uIJp?^jp^94I{w zW(l2X#V0cvVTc4H9*t%ezT^?BFi-y!q{3>?@_E&+D;#b*3(pggH46(=;2KFlA*cC- zGXfPv9rK$ftAzj!EitA}7@o0_r2lO{o%YroL=)ouu|7pZoPlOp1e5w5F{fS4sD*om z-$aV)2mAs;mk0_$_bf?9J*A_dLA-(dqCcR9vQ{L>nNH{;oL^y`kpW9Aj4LTAUq|AK zM!+`nwqVm$Y-XJG;bO(J2p6!!v;bS_qVgv)W?T`Cs4!T zC>Zdh*FeAG!e#?xG_nH%DBu-92OehP2L*e9_Dn}kD)wjPAb0cNo+>=l@EMudJBx!w zocL!6O2S0N3D>IG$SbQVHt+&KeLx6w1;?sk4)N1P!5!KPVLahTBd^*+*a5A;)+C~Y zCq$8?!b@So(pM*7vVib~EOI%>RqznvQ_Ik_Lio{y%{(k~+YmKg(Si9H(^z$WdiM`i=9dDaDaC$PO0quIp8>|G9dNju~`R1<0C(8U#ay z(~A)T&kFRK$DmWV&9@{KxIx&?&j@JE!qpf%>$w@4ZzpRvoHI z&`?~#H0UUf@M&ON%6y^>%Di&ZT2Pgul6iq#goe7Y^jckk9)r1IA^oL6!={ns!q*?@ z0}xQu$UCL**(XH0Oc*7L8Y5?l1o$+rl0%T)1pIIFD^xn&j`@jfR{ey!BwN;4AV>Zc_|2f z!MUDey0F*En(+PeYO$Vh~5QyM_Px%O9MTq z2@DS&=*9Fg0l&mytY3T%_Q%(^v>0*u^+kI+&lIC5G2h?bUNqO*)>gWrye`*!r7!Av zFcdxNya0kJ%U>J2U0#Ed!4IRykMD&&2M_Kn%}w6ws0>dXI8vKPqD}Fd-06oG!pB{m zPlu!P!>4zBUNU!}@6l@z6I{ucdpvb0d~~sU$;K=>=DD=0Z+V#KEB&Go9H~5hd-(1A z-pgalm-a2sDj@Xm!2SKRN5V&a2kOGk;Ug=OiHaZUK18+N3e@R^hx_;cIHXI9_ZZTJ z4OzObC53&Jqt;v&Tvcb$lE1ms|IjCAna4 zW^i3qS)yfSx(+MG2t<_6XxWdq*xQ#F1k0$VmUr|0m%C=V{n=-h-QD-Oo*N@!+tsVPDz_xca%Ya;ZjK(@o{q-ujpVP~=%>~ohTOy5V^5|( z{2I53+VzQ|aA@x^U@NT-hsXEiu1w!9FRDqbX|$qSXUBeDn+zX!9$5*%V_h(>3HY4; zuV7L=C!GHjp1*q`Ja&HoG@Z$Jp2tTP0BR=e>KX}8wcQHu-W@Acx-wNi4 z1(sXxaJsRi!tckqbZPk?w|;j0KXDZN$DvtTN66#eIM@nl=o`eYT;=W{T=nIC$5hl>8feO>rE z@N$M+{^-Dk;_^Qxie4?tAD-xr+K%4)GVC6i2rnG(T$kT>a&)#kY`u1DSN`y!3t{)E zeJk?gcaGOZhp!z?M57P7KAPKo?xtHxpL`1^uAJKy_V@37G9BGIJc!ek4@*mQ43d>KQ(#oyYSNJXzdUAiHY9)iGi#1v68BdC}j(eA3wNbOKHBp zyPZ(^>AEt=yN(*vr&UvBT4MQikjQ^wrCNAubgEFKwtV-Ncc@tH$#hw`|6t!XOpA)N zFL!cn4(iX!s>6Lf9l74N3uSJ4=D1p2u_3&3p!Lag{`jrSpQgeGV^izG&im)0v$w7! zioKm)HZr89PJz_+EiBkulq(+bOKYakdO>@?v};^~FM8Y=>MWXhT(+)^%BQM<$27Jz-#&eHW^mVrUAxj1 zrFh0@t%+`rwbs@>na*9hJMGnk$9JEenXD+StZ13JQu8VvV?)b;qS*&ehr{mCbGdWV zlS%SMSUtalsr!rlu7#!3w*eMeEx(?~`Zzjw@L(o)xBpVqeQpFAzir^T5~*l>v?ClF z?KPLmtR-8$v}$U3aTN|9-wP)^?A$$3R6@92QJGkoDA^kAzS3G#YS!dN&zz6W@3|Ho zy+2h~^!kdqJ)?u+Sl5B7@KFD?=+K_Qm5Frmw~6rLp0Rx2#GM3vFrbviLD^9i@;>jp zS6T9>WN|TSiPnZEt`8(DwK8;@oI5;m3RObk@$r%TkxO^^8$mTIi)IF+y_2`{2OmvG zR}SvmnD03~x-vL+l7Od5=GZdcC*tQ(*UgKWe8=uR&lK>4lcU!P zwL2IrlRrOphbwlX&cQy6MO#ar{xA3KmypCWp)WRge=;od>rqjm)+oGk|2mZ!<&PhJ z6fBQ1HlS-~`^kNZ+K(QkQlhBm{?&Df+5OSzv37825&lhVP0XH6*w0fJdQ5*PQI21k53HFb=C)V_qX*Z0wjx>EQd(K`W-?ipKQexB*9+7E1V>BN{~X%=K~4DhEM!O5;itoE zR)1WU&K+;PK6`UhQ+~7q-eW?qN6Vt{#-7R9+v%^%)8!T6!MhLa+{5!HOY_&eulY}h zk#<;@KhoO{uqUQ3B*MN!56a3)lF{VhoB2omXN3Fzoa)`NGEqIVeUzS2iX7sjNCb)p{dQu`ze* z&{TD9>cNF%Z8X(()z9@^yfSm^`wSvEQ)TP&$4(Dazm-3I>mVHAJ6BH4bVZksjOMSk z?Weu0$#>lvOaJ}go$~zQBNxNT!O3+iC{}~|0;s6L=+ga;Tu;~G%5^ohh_;1Ym-jy% zelq<7@AeB7%hCdZA6G0(iwl;tUe1hOT70m)ILmh*IgyF3Oiv*L5SJn3Pqd$ZI}_dO zY1Lv*7v87%z{`t5yvv28mnmEjKA{jBKPxQw%qN4MkBt9fVGv6)_D%mEiX0S6P7EJG zu|~9e|KK~(zS|F@;mZe5osn-FMh#`O|L{;;lM!|g?v1~Ewm?kFZTQl9jpa9-OE_{9 z-G`!y3x_NCOFH$*%0KTYaqJ`3{)TPxz>tWi@!V_URj$IAISOf>6cbp zEI)mCrWHk<&lO+H9XdF$ymSO8zYY6O?x~lYtM6`GOj3}w7tRn3R zc0}BSK2K6zxgyb+jyn6=R)&MuI>XM9iIp%M2hNPWT~QT|o!eUi%lkw$-FftXJvf-C zt9m+&>V^My!KluUO{nJ$5VX@9T}g#h8X z$0I8~%^mNUekWaBMa8D-2W8RTzRN*)@yyA}T7@nv?M%w`B03JmoqM&u}2o<-y#e?(}HXvB7*;9I%$I%T^=5vxv;Km#oWFNo#Du- zYrDczU47xrw!4Yk!12-iRNLf}>B@C0ia?q@hnoLTRI;)bOXMVXbZc*Sd4jm1xGdi@ z-iZYRCX$suVQirt8ZNN+62o|DHXN-5Y5k1gPbRv1pf$SR)32$pRme+Uk}J>M8JvFa zt$2^%mGJ*OXt~e)J!c`bydRz(7|V~33^vBy2|6r@LyZN+z@K|s{J$%DsRZ`3fut7$ z^}>MAa)NqkcAsGNfAWh~ODh0)&wezZBsS?MqF>Lh@Yig-Ku^mdVQJ9()) { + let result = rsonpath_syntax::parse(&query.string).expect("expected Ok"); - assert_eq!(expected, result); + assert_eq!(query.parsed, result); } #[test] - fn round_trip((_, query) in any_valid_query()) { - let input = query.to_string(); + fn round_trip(query in proptest::arbitrary::any::()) { + let input = query.parsed.to_string(); let result = rsonpath_syntax::parse(&input).expect("expected Ok"); - assert_eq!(query, result); + assert_eq!(query.parsed, result); } } } - use rsonpath_syntax::{ - builder::SliceBuilder, num::JsonInt, str::JsonString, JsonPathQuery, LogicalExpr, Segment, Selector, Selectors, - }; - - /* Approach: we generate the query string bit by bit, each time attaching what the expected - * typed element is. At the end we have the input string all ready, and the expected - * parser result can be easily obtained by a 1-1 translation. - */ - #[derive(Debug, Clone)] - enum PropSegment { - // .* - ShortChildWildcard, - // .name - ShortChildName(JsonString), - // ..* - ShortDescendantWildcard, - // ..name - ShortDescendantName(JsonString), - // [] - BracketedChild(Vec), - // ..[] - BracketedDescendant(Vec), - } - - #[derive(Debug, Clone)] - enum PropSelector { - Wildcard, - Name(JsonString), - Index(JsonInt), - Slice(Option, Option, Option), - Filter(LogicalExpr), - } - - fn any_valid_query() -> impl Strategy { - return prop::collection::vec(any_segment(None), 0..10) - .prop_map(map_prop_segments) - .prop_recursive(3, 10, 5, |query_strategy| { - prop::collection::vec(any_segment(Some(query_strategy)), 0..10).prop_map(map_prop_segments) - }); - - fn map_prop_segments(segments: Vec<(String, PropSegment)>) -> (String, JsonPathQuery) { - let mut s = "$".to_string(); - let mut v = vec![]; - - for (segment_s, segment) in segments { - s.push_str(&segment_s); - match segment { - PropSegment::ShortChildWildcard => v.push(Segment::Child(Selectors::one(Selector::Wildcard))), - PropSegment::ShortChildName(n) => v.push(Segment::Child(Selectors::one(Selector::Name(n)))), - PropSegment::ShortDescendantWildcard => { - v.push(Segment::Descendant(Selectors::one(Selector::Wildcard))) - } - PropSegment::ShortDescendantName(n) => { - v.push(Segment::Descendant(Selectors::one(Selector::Name(n)))) - } - PropSegment::BracketedChild(ss) => v.push(Segment::Child(Selectors::many( - ss.into_iter().map(map_prop_selector).collect(), - ))), - PropSegment::BracketedDescendant(ss) => v.push(Segment::Descendant(Selectors::many( - ss.into_iter().map(map_prop_selector).collect(), - ))), - } - } - - (s, JsonPathQuery::from_iter(v)) - } - fn map_prop_selector(s: PropSelector) -> Selector { - match s { - PropSelector::Wildcard => Selector::Wildcard, - PropSelector::Name(n) => Selector::Name(n), - PropSelector::Index(i) => Selector::Index(i.into()), - PropSelector::Slice(start, end, step) => Selector::Slice({ - let mut builder = SliceBuilder::new(); - if let Some(start) = start { - builder.with_start(start); - } - if let Some(step) = step { - builder.with_step(step); - } - if let Some(end) = end { - builder.with_end(end); - } - builder.into() - }), - PropSelector::Filter(logical) => Selector::Filter(logical), - } - } - } - - fn any_segment( - recursive_query_strategy: Option>, - ) -> impl Strategy { - return prop_oneof![ - strategy::Just((".*".to_string(), PropSegment::ShortChildWildcard)), - strategy::Just(("..*".to_string(), PropSegment::ShortDescendantWildcard)), - any_short_name().prop_map(|name| (format!(".{name}"), PropSegment::ShortChildName(JsonString::new(&name)))), - any_short_name().prop_map(|name| ( - format!("..{name}"), - PropSegment::ShortDescendantName(JsonString::new(&name)) - )), - prop::collection::vec(any_selector(recursive_query_strategy.clone()), 1..5).prop_map(|reprs| { - let mut s = "[".to_string(); - let v = collect_reprs(reprs, &mut s); - s.push(']'); - (s, PropSegment::BracketedChild(v)) - }), - prop::collection::vec(any_selector(recursive_query_strategy), 1..5).prop_map(|reprs| { - let mut s = "..[".to_string(); - let v = collect_reprs(reprs, &mut s); - s.push(']'); - (s, PropSegment::BracketedDescendant(v)) - }), - ]; - - fn collect_reprs(reprs: Vec<(String, PropSelector)>, s: &mut String) -> Vec { - let mut result = Vec::with_capacity(reprs.len()); - let mut first = true; - for (repr_s, prop_selector) in reprs { - if !first { - s.push(','); - } - first = false; - s.push_str(&repr_s); - result.push(prop_selector); - } - result - } - } - - fn any_selector( - recursive_query_strategy: Option>, - ) -> impl Strategy { - prop_oneof![ - strategy::Just(("*".to_string(), PropSelector::Wildcard)), - strings::any_json_string().prop_map(|(raw, s)| (raw, PropSelector::Name(s))), - any_json_int().prop_map(|(raw, i)| (raw, PropSelector::Index(i))), - any_slice().prop_map(|(raw, a, b, c)| (raw, PropSelector::Slice(a, b, c))), - filters::any_logical_expr(recursive_query_strategy) - .prop_map(|(raw, expr)| (format!("?{raw}"), PropSelector::Filter(expr))) - ] - } - - fn any_json_int() -> impl Strategy { - (-((1_i64 << 53) + 1)..((1_i64 << 53) - 1)).prop_map(|i| (i.to_string(), JsonInt::try_from(i).unwrap())) - } - - fn any_slice() -> impl Strategy, Option, Option)> { - ( - option::of(any_json_int()), - option::of(any_json_int()), - option::of(any_json_int()), - ) - .prop_map(|(a, b, c)| { - let mut s = String::new(); - let a = a.map(|(a_s, a_i)| { - s.push_str(&a_s); - a_i - }); - s.push(':'); - let b = b.map(|(b_s, b_i)| { - s.push_str(&b_s); - b_i - }); - s.push(':'); - let c = c.map(|(c_s, c_i)| { - s.push_str(&c_s); - c_i - }); - (s, a, b, c) - }) - } - - fn any_short_name() -> impl Strategy { - r"([A-Za-z]|_|[^\u0000-\u007F])([A-Za-z0-9]|_|[^\u0000-\u007F])*" - } + #[cfg(feature = "serde")] + mod serde { + use super::*; + use pretty_assertions::assert_eq; + use rsonpath_syntax::JsonPathQuery; - mod strings { - use proptest::{prelude::*, sample::SizeRange}; - use rsonpath_syntax::str::JsonString; + /// This is a proptest regression test. + /// It relies on serde_json using the `float_roundtrip` feature. + /// See: https://github.com/serde-rs/json/issues/1170 + #[test] + fn float_roundtrip() { + let string = "$[?$&&!($&&!$[?$&&($||$&&$..[?(405831638439668000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000==null||null==null)||null==null]||$)])]"; + let query = rsonpath_syntax::parse(string).unwrap(); - #[derive(Debug, PartialEq, Eq, Clone, Copy)] - enum JsonStringToken { - EncodeNormally(char), - ForceUnicodeEscape(char), - } + let json_str = serde_json::to_string(&query).unwrap(); + let query_deser = serde_json::from_str::(&json_str).unwrap(); - #[derive(Debug, PartialEq, Eq, Clone, Copy)] - enum JsonStringTokenEncodingMode { - SingleQuoted, - DoubleQuoted, + assert_eq!(query, query_deser); } - impl JsonStringToken { - fn raw(self) -> char { - match self { - Self::EncodeNormally(x) | Self::ForceUnicodeEscape(x) => x, + proptest! { + #[test] + fn query_cbor_roundtrips(ArbitraryJsonPathQuery { parsed, .. } in prop::arbitrary::any::()) { + use std::io; + struct ReadBuf<'a> { + buf: &'a [u8], + idx: usize, } - } - - fn encode(self, mode: JsonStringTokenEncodingMode) -> String { - return match self { - Self::EncodeNormally('\u{0008}') => r"\b".to_owned(), - Self::EncodeNormally('\t') => r"\t".to_owned(), - Self::EncodeNormally('\n') => r"\n".to_owned(), - Self::EncodeNormally('\u{000C}') => r"\f".to_owned(), - Self::EncodeNormally('\r') => r"\r".to_owned(), - Self::EncodeNormally('"') => match mode { - JsonStringTokenEncodingMode::DoubleQuoted => r#"\""#.to_owned(), - JsonStringTokenEncodingMode::SingleQuoted => r#"""#.to_owned(), - }, - Self::EncodeNormally('\'') => match mode { - JsonStringTokenEncodingMode::DoubleQuoted => r#"'"#.to_owned(), - JsonStringTokenEncodingMode::SingleQuoted => r#"\'"#.to_owned(), - }, - Self::EncodeNormally('/') => r"\/".to_owned(), - Self::EncodeNormally('\\') => r"\\".to_owned(), - Self::EncodeNormally(c @ ..='\u{001F}') | Self::ForceUnicodeEscape(c) => encode_unicode_escape(c), - Self::EncodeNormally(c) => c.to_string(), - }; - - fn encode_unicode_escape(c: char) -> String { - let mut buf = [0; 2]; - let enc = c.encode_utf16(&mut buf); - let mut res = String::new(); - for x in enc { - res += &format!("\\u{x:0>4x}"); + impl<'a> io::Read for &mut ReadBuf<'a> { + fn read(&mut self, buf: &mut [u8]) -> Result { + let len = std::cmp::min(self.buf.len() - self.idx, buf.len()); + buf.copy_from_slice(&self.buf[self.idx..self.idx + len]); + self.idx += len; + Ok(len) } - res } - } - } - pub(super) fn any_json_string() -> impl Strategy { - prop_oneof![ - Just(JsonStringTokenEncodingMode::SingleQuoted), - Just(JsonStringTokenEncodingMode::DoubleQuoted) - ] - .prop_flat_map(|mode| { - prop::collection::vec( - (prop::char::any(), prop::bool::ANY).prop_map(|(c, b)| { - if b { - JsonStringToken::EncodeNormally(c) - } else { - JsonStringToken::ForceUnicodeEscape(c) - } - }), - SizeRange::default(), - ) - .prop_map(move |v| { - let q = match mode { - JsonStringTokenEncodingMode::SingleQuoted => '\'', - JsonStringTokenEncodingMode::DoubleQuoted => '"', - }; - let mut s = String::new(); - let mut l = String::new(); - for x in v { - s += &x.encode(mode); - l.push(x.raw()); - } - (format!("{q}{s}{q}"), JsonString::new(&l)) - }) - }) - } - } + let mut buf = vec![]; + ciborium::into_writer(&parsed, &mut buf)?; - mod filters { - use proptest::{num, prelude::*, strategy}; - use rsonpath_syntax::{ - num::{JsonFloat, JsonNumber}, - str::JsonString, - Comparable, ComparisonExpr, ComparisonOp, JsonPathQuery, Literal, LogicalExpr, SingularJsonPathQuery, - SingularSegment, TestExpr, - }; - - pub(super) fn any_logical_expr( - test_query_strategy: Option>, - ) -> impl Strategy { - any_atomic_logical_expr(test_query_strategy).prop_recursive(8, 32, 2, |inner| { - prop_oneof![ - (inner.clone(), proptest::bool::ANY).prop_map(|((s, f), force_paren)| ( - match f { - LogicalExpr::Test(_) if !force_paren => format!("!{s}"), - _ => format!("!({s})"), - }, - LogicalExpr::Not(Box::new(f)) - )), - (inner.clone(), inner.clone(), proptest::bool::ANY, proptest::bool::ANY).prop_map( - |((lhs_s, lhs_e), (rhs_s, rhs_e), force_left_paren, force_right_paren)| { - let put_left_paren = force_left_paren || matches!(lhs_e, LogicalExpr::Or(_, _)); - let put_right_paren = - force_right_paren || matches!(rhs_e, LogicalExpr::Or(_, _) | LogicalExpr::And(_, _)); - let s = match (put_left_paren, put_right_paren) { - (true, true) => format!("({lhs_s})&&({rhs_s})"), - (true, false) => format!("({lhs_s})&&{rhs_s}"), - (false, true) => format!("{lhs_s}&&({rhs_s})"), - (false, false) => format!("{lhs_s}&&{rhs_s}"), - }; - (s, LogicalExpr::And(Box::new(lhs_e), Box::new(rhs_e))) - } - ), - (inner.clone(), inner.clone(), proptest::bool::ANY, proptest::bool::ANY).prop_map( - |((lhs_s, lhs_e), (rhs_s, rhs_e), force_left_paren, force_right_paren)| { - let put_left_paren = force_left_paren || matches!(lhs_e, LogicalExpr::Or(_, _)); - let put_right_paren = force_right_paren; - let s = match (put_left_paren, put_right_paren) { - (true, true) => format!("({lhs_s})||({rhs_s})"), - (true, false) => format!("({lhs_s})||{rhs_s}"), - (false, true) => format!("{lhs_s}||({rhs_s})"), - (false, false) => format!("{lhs_s}||{rhs_s}"), - }; - (s, LogicalExpr::Or(Box::new(lhs_e), Box::new(rhs_e))) - } - ) - ] - }) - } + let mut read = ReadBuf { buf: &buf, idx: 0 }; + let query_deser = ciborium::from_reader(&mut read)?; - fn any_atomic_logical_expr( - test_query_strategy: Option>, - ) -> impl Strategy { - if let Some(test_query_strategy) = test_query_strategy { - prop_oneof![ - any_test(test_query_strategy).prop_map(|(s, t)| (s, LogicalExpr::Test(t))), - any_comparison().prop_map(|(s, c)| (s, LogicalExpr::Comparison(c))), - ] - .boxed() - } else { - any_comparison() - .prop_map(|(s, c)| (s, LogicalExpr::Comparison(c))) - .boxed() + assert_eq!(parsed, query_deser); } - } - - fn any_test( - test_query_strategy: BoxedStrategy<(String, JsonPathQuery)>, - ) -> impl Strategy { - (proptest::bool::ANY, test_query_strategy).prop_map(|(relative, (mut s, q))| { - if relative { - assert_eq!(s.as_bytes()[0], b'$'); - s.replace_range(0..1, "@"); - (s, TestExpr::Relative(q)) - } else { - (s, TestExpr::Absolute(q)) - } - }) - } - - fn any_comparison() -> impl Strategy { - (any_comparable(), any_comparison_op(), any_comparable()).prop_map( - |((lhs_s, lhs_e), (op_s, op_e), (rhs_s, rhs_e))| { - ( - format!("{lhs_s}{op_s}{rhs_s}"), - ComparisonExpr::from_parts(lhs_e, op_e, rhs_e), - ) - }, - ) - } - - fn any_comparable() -> impl Strategy { - prop_oneof![ - any_literal().prop_map(|(s, l)| (s, Comparable::Literal(l))), - (proptest::bool::ANY, any_singular_query()).prop_map(|(relative, (mut s, q))| { - if relative { - assert_eq!(s.as_bytes()[0], b'$'); - s.replace_range(0..1, "@"); - (s, Comparable::RelativeSingularQuery(q)) - } else { - (s, Comparable::AbsoluteSingularQuery(q)) - } - }) - ] - } - prop_compose! { - fn any_singular_query()(segments in prop::collection::vec(any_singular_segment(), 0..10)) -> (String, SingularJsonPathQuery) { - let mut s = "$".to_string(); - let mut v = vec![]; - - for (segment_s, segment) in segments { - s.push_str(&segment_s); - v.push(segment); - } + #[test] + fn query_json_roundtrips(ArbitraryJsonPathQuery { parsed, .. } in prop::arbitrary::any::()) { + let json_str = serde_json::to_string(&parsed)?; + let query_deser = serde_json::from_str::(&json_str)?; - (s, SingularJsonPathQuery::from_iter(v)) + assert_eq!(parsed, query_deser); } - } - - fn any_singular_segment() -> impl Strategy { - prop_oneof![ - super::any_json_int().prop_map(|(s, i)| (format!("[{s}]"), SingularSegment::Index(i.into()))), - super::any_short_name().prop_map(|n| (format!(".{n}"), SingularSegment::Name(JsonString::new(&n)))), - super::strings::any_json_string().prop_map(|(s, n)| (format!("[{s}]"), SingularSegment::Name(n))), - ] - } - fn any_literal() -> impl Strategy { - prop_oneof![ - strategy::Just(("null".to_string(), Literal::Null)), - proptest::bool::ANY.prop_map(|b| (b.to_string(), Literal::Bool(b))), - any_json_number().prop_map(|(s, n)| (s, Literal::Number(n))), - super::strings::any_json_string().prop_map(|(raw, s)| (raw, Literal::String(s))) - ] - } - - fn any_json_number() -> impl Strategy { - prop_oneof![ - super::any_json_int().prop_map(|(s, i)| (s, JsonNumber::Int(i))), - any_json_float().prop_map(|(s, f)| (s, JsonNumber::Float(f))), - ] - .prop_map(|(x, n)| (x, n.normalize())) - } - - fn any_json_float() -> impl Strategy { - // We first generate the target f64 value we want and then pick one of its possible string reprs. - // Because an "int float" is also interesting we generate those half the time. - // If there is no exponent, there is only one possible representation. - // If we include an exponent we can move the floating point however far we want one way or the other. - return prop_oneof![ - any_float().prop_map(|f| (f.to_string(), JsonFloat::try_from(f).unwrap())), - any_float() - .prop_flat_map(|f| arbitrary_exp_repr(f).prop_map(move |s| (s, JsonFloat::try_from(f).unwrap()))), - ]; - - fn any_float() -> impl Strategy { - prop_oneof![num::f64::NORMAL, num::f64::NORMAL.prop_map(f64::trunc)] - } + #[test] + fn query_message_pack_roundtrips(ArbitraryJsonPathQuery { parsed, .. } in prop::arbitrary::any::()) { + let buf = rmp_serde::to_vec(&parsed)?; + let query_deser = rmp_serde::from_slice(&buf)?; - fn arbitrary_exp_repr(f: f64) -> impl Strategy { - let s = f.to_string(); - let fp_pos: isize = s.find('.').unwrap_or(s.len()).try_into().unwrap(); - let num_digits = if fp_pos == s.len() as isize { - s.len() - } else { - s.len() - 1 - } - if f.is_sign_negative() { - // Subtract the minus char. - 1 - } else { - 0 - }; - (-1024..=1024_isize, proptest::bool::ANY, proptest::bool::ANY).prop_map( - move |(exp, force_sign, uppercase_e)| { - let new_pos = fp_pos - exp; - let mut res = String::new(); - if f.is_sign_negative() { - res.push('-'); - } - let mut orig_digits = s.chars().filter(|c| *c != '.'); - - // There are three cases: - // 1. the new point is before all existing digits; - // in this case we need to append 0.000... at the front - // 2. the new point position falls within the existing string; - // this is straightforward, we just emplace it there - // 3. the new point is after all existing digits; - // in this case we need to append 0000... at the end - // After this operation we need to manually trim the zeroes. - if new_pos <= 0 { - // Case 1. - res.push_str("0."); - for _ in 0..(-new_pos) { - res.push('0'); - } - for orig_digit in orig_digits { - res.push(orig_digit); - } - } else if new_pos < num_digits as isize { - // Case 2. - let mut pos = 0; - let mut pushed_non_zero = false; - loop { - if pos == new_pos { - if !pushed_non_zero { - res.push('0'); - } - pushed_non_zero = true; - res.push('.'); - } else { - let Some(orig_digit) = orig_digits.next() else { break }; - if orig_digit == '0' { - if pushed_non_zero { - res.push(orig_digit); - } - } else { - pushed_non_zero = true; - res.push(orig_digit); - } - } - pos += 1; - } - } else if f == 0.0 { - // Case 3. special case. - // Note that -0.0 is handled here as well, as it is equal to 0.0 and the sign is appended above. - res.push('0'); - } else { - // Case 3. - // First skip zeroes. There has to be at least one non-zero since we checked - // f == 0.0 above. - let skip_zeroes = orig_digits.skip_while(|x| *x == '0'); - for orig_digit in skip_zeroes { - res.push(orig_digit); - } - for _ in 0..(new_pos - num_digits as isize) { - res.push('0'); - } - } - - res.push(if uppercase_e { 'E' } else { 'e' }); - - if exp > 0 { - if force_sign { - res.push('+'); - } - res.push_str(&exp.to_string()); - } else { - res.push_str(&exp.to_string()); - } - - res - }, - ) + assert_eq!(parsed, query_deser); } } - - fn any_comparison_op() -> impl Strategy { - prop_oneof![ - strategy::Just(("==".to_string(), ComparisonOp::EqualTo)), - strategy::Just(("!=".to_string(), ComparisonOp::NotEqualTo)), - strategy::Just(("<".to_string(), ComparisonOp::LessThan)), - strategy::Just((">".to_string(), ComparisonOp::GreaterThan)), - strategy::Just(("<=".to_string(), ComparisonOp::LesserOrEqualTo)), - strategy::Just((">=".to_string(), ComparisonOp::GreaterOrEqualTo)), - ] - } } } diff --git a/crates/rsonpath-syntax/tests/query_serialization_snapshots.rs b/crates/rsonpath-syntax/tests/query_serialization_snapshots.rs new file mode 100644 index 00000000..24cb4448 --- /dev/null +++ b/crates/rsonpath-syntax/tests/query_serialization_snapshots.rs @@ -0,0 +1,54 @@ +#[cfg(feature = "serde")] +mod ron { + use insta::assert_ron_snapshot; + use rsonpath_syntax::parse; + use std::error::Error; + + #[test] + fn empty_query() -> Result<(), Box> { + assert_ron_snapshot!(&parse("$")?); + Ok(()) + } + + #[test] + fn readme_query() -> Result<(), Box> { + assert_ron_snapshot!(&parse("$.jsonpath[*]")?); + Ok(()) + } + + #[test] + fn jsonpath_example_query() -> Result<(), Box> { + assert_ron_snapshot!(&parse("$..phoneNumbers[*].number")?); + Ok(()) + } + + #[test] + fn real_life_query() -> Result<(), Box> { + assert_ron_snapshot!(&parse("$.personal.details.contact.information.phones.home")?); + Ok(()) + } + + #[test] + fn slice() -> Result<(), Box> { + assert_ron_snapshot!(&parse("$..entries[3:5:7]")?); + Ok(()) + } + + #[test] + fn multiple_selectors() -> Result<(), Box> { + assert_ron_snapshot!(&parse("$..entries['abc', 4, 7:10:13]")?); + Ok(()) + } + + #[test] + fn filter() -> Result<(), Box> { + assert_ron_snapshot!(&parse("$..user[?@.id == 'value']..entities..url")?); + Ok(()) + } + + #[test] + fn nested_filter() -> Result<(), Box> { + assert_ron_snapshot!(&parse(r#"$.a[?@.b == "abc" && $[*][?@.c > 3.13]]"#)?); + Ok(()) + } +} diff --git a/crates/rsonpath-syntax/tests/snapshots/query_serialization_snapshots__ron__empty_query.snap b/crates/rsonpath-syntax/tests/snapshots/query_serialization_snapshots__ron__empty_query.snap new file mode 100644 index 00000000..e5c574c5 --- /dev/null +++ b/crates/rsonpath-syntax/tests/snapshots/query_serialization_snapshots__ron__empty_query.snap @@ -0,0 +1,8 @@ +--- +source: crates/rsonpath-syntax/tests/query_serialization_snapshots.rs +expression: "&query" +snapshot_kind: text +--- +JsonPathQuery( + segments: [], +) diff --git a/crates/rsonpath-syntax/tests/snapshots/query_serialization_snapshots__ron__filter.snap b/crates/rsonpath-syntax/tests/snapshots/query_serialization_snapshots__ron__filter.snap new file mode 100644 index 00000000..957c6fe7 --- /dev/null +++ b/crates/rsonpath-syntax/tests/snapshots/query_serialization_snapshots__ron__filter.snap @@ -0,0 +1,47 @@ +--- +source: crates/rsonpath-syntax/tests/query_serialization_snapshots.rs +expression: "&parse(\"$..user[?@.id == 'value']..entities..url\")?" +snapshot_kind: text +--- +JsonPathQuery( + segments: [ + Descendant(Selectors( + inner: [ + Name(JsonString( + quoted: "\"user\"", + )), + ], + )), + Child(Selectors( + inner: [ + Filter(Comparison(ComparisonExpr( + lhs: RelativeSingularQuery(SingularJsonPathQuery( + segments: [ + Name(JsonString( + quoted: "\"id\"", + )), + ], + )), + op: EqualTo, + rhs: Literal(String(JsonString( + quoted: "\"value\"", + ))), + ))), + ], + )), + Descendant(Selectors( + inner: [ + Name(JsonString( + quoted: "\"entities\"", + )), + ], + )), + Descendant(Selectors( + inner: [ + Name(JsonString( + quoted: "\"url\"", + )), + ], + )), + ], +) diff --git a/crates/rsonpath-syntax/tests/snapshots/query_serialization_snapshots__ron__jsonpath_example_query.snap b/crates/rsonpath-syntax/tests/snapshots/query_serialization_snapshots__ron__jsonpath_example_query.snap new file mode 100644 index 00000000..ed3a1b11 --- /dev/null +++ b/crates/rsonpath-syntax/tests/snapshots/query_serialization_snapshots__ron__jsonpath_example_query.snap @@ -0,0 +1,28 @@ +--- +source: crates/rsonpath-syntax/tests/query_serialization_snapshots.rs +expression: "&parse(\"$..phoneNumbers[*].number\")?" +snapshot_kind: text +--- +JsonPathQuery( + segments: [ + Descendant(Selectors( + inner: [ + Name(JsonString( + quoted: "\"phoneNumbers\"", + )), + ], + )), + Child(Selectors( + inner: [ + Wildcard, + ], + )), + Child(Selectors( + inner: [ + Name(JsonString( + quoted: "\"number\"", + )), + ], + )), + ], +) diff --git a/crates/rsonpath-syntax/tests/snapshots/query_serialization_snapshots__ron__multiple_selectors.snap b/crates/rsonpath-syntax/tests/snapshots/query_serialization_snapshots__ron__multiple_selectors.snap new file mode 100644 index 00000000..3e30e5a9 --- /dev/null +++ b/crates/rsonpath-syntax/tests/snapshots/query_serialization_snapshots__ron__multiple_selectors.snap @@ -0,0 +1,29 @@ +--- +source: crates/rsonpath-syntax/tests/query_serialization_snapshots.rs +expression: "&parse(\"$..entries['abc', 4, 7:10:13]\")?" +snapshot_kind: text +--- +JsonPathQuery( + segments: [ + Descendant(Selectors( + inner: [ + Name(JsonString( + quoted: "\"entries\"", + )), + ], + )), + Child(Selectors( + inner: [ + Name(JsonString( + quoted: "\"abc\"", + )), + Index(FromStart(JsonUInt(4))), + Slice(Slice( + start: FromStart(JsonUInt(7)), + end: Some(FromStart(JsonUInt(10))), + step: Forward(JsonUInt(13)), + )), + ], + )), + ], +) diff --git a/crates/rsonpath-syntax/tests/snapshots/query_serialization_snapshots__ron__nested_filter.snap b/crates/rsonpath-syntax/tests/snapshots/query_serialization_snapshots__ron__nested_filter.snap new file mode 100644 index 00000000..b58c31a2 --- /dev/null +++ b/crates/rsonpath-syntax/tests/snapshots/query_serialization_snapshots__ron__nested_filter.snap @@ -0,0 +1,56 @@ +--- +source: crates/rsonpath-syntax/tests/query_serialization_snapshots.rs +expression: "&parse(r#\"$.a[?@.b == \"abc\" && $[*][?@.c > 3.13]]\"#)?" +snapshot_kind: text +--- +JsonPathQuery( + segments: [ + Child(Selectors( + inner: [ + Name(JsonString( + quoted: "\"a\"", + )), + ], + )), + Child(Selectors( + inner: [ + Filter(And(Comparison(ComparisonExpr( + lhs: RelativeSingularQuery(SingularJsonPathQuery( + segments: [ + Name(JsonString( + quoted: "\"b\"", + )), + ], + )), + op: EqualTo, + rhs: Literal(String(JsonString( + quoted: "\"abc\"", + ))), + )), Test(Absolute(JsonPathQuery( + segments: [ + Child(Selectors( + inner: [ + Wildcard, + ], + )), + Child(Selectors( + inner: [ + Filter(Comparison(ComparisonExpr( + lhs: RelativeSingularQuery(SingularJsonPathQuery( + segments: [ + Name(JsonString( + quoted: "\"c\"", + )), + ], + )), + op: GreaterThan, + rhs: Literal(Number(Float(JsonFloat(3.13)))), + ))), + ], + )), + ], + ))))), + ], + )), + ], +) diff --git a/crates/rsonpath-syntax/tests/snapshots/query_serialization_snapshots__ron__readme_query.snap b/crates/rsonpath-syntax/tests/snapshots/query_serialization_snapshots__ron__readme_query.snap new file mode 100644 index 00000000..181d1067 --- /dev/null +++ b/crates/rsonpath-syntax/tests/snapshots/query_serialization_snapshots__ron__readme_query.snap @@ -0,0 +1,21 @@ +--- +source: crates/rsonpath-syntax/tests/query_serialization_snapshots.rs +expression: "&parse(\"$.jsonpath[*]\")?" +snapshot_kind: text +--- +JsonPathQuery( + segments: [ + Child(Selectors( + inner: [ + Name(JsonString( + quoted: "\"jsonpath\"", + )), + ], + )), + Child(Selectors( + inner: [ + Wildcard, + ], + )), + ], +) diff --git a/crates/rsonpath-syntax/tests/snapshots/query_serialization_snapshots__ron__real_life_query.snap b/crates/rsonpath-syntax/tests/snapshots/query_serialization_snapshots__ron__real_life_query.snap new file mode 100644 index 00000000..6fb33382 --- /dev/null +++ b/crates/rsonpath-syntax/tests/snapshots/query_serialization_snapshots__ron__real_life_query.snap @@ -0,0 +1,51 @@ +--- +source: crates/rsonpath-syntax/tests/query_serialization_snapshots.rs +expression: "&parse(\"$.personal.details.contact.information.phones.home\")?" +snapshot_kind: text +--- +JsonPathQuery( + segments: [ + Child(Selectors( + inner: [ + Name(JsonString( + quoted: "\"personal\"", + )), + ], + )), + Child(Selectors( + inner: [ + Name(JsonString( + quoted: "\"details\"", + )), + ], + )), + Child(Selectors( + inner: [ + Name(JsonString( + quoted: "\"contact\"", + )), + ], + )), + Child(Selectors( + inner: [ + Name(JsonString( + quoted: "\"information\"", + )), + ], + )), + Child(Selectors( + inner: [ + Name(JsonString( + quoted: "\"phones\"", + )), + ], + )), + Child(Selectors( + inner: [ + Name(JsonString( + quoted: "\"home\"", + )), + ], + )), + ], +) diff --git a/crates/rsonpath-syntax/tests/snapshots/query_serialization_snapshots__ron__slice.snap b/crates/rsonpath-syntax/tests/snapshots/query_serialization_snapshots__ron__slice.snap new file mode 100644 index 00000000..f99b61de --- /dev/null +++ b/crates/rsonpath-syntax/tests/snapshots/query_serialization_snapshots__ron__slice.snap @@ -0,0 +1,25 @@ +--- +source: crates/rsonpath-syntax/tests/query_serialization_snapshots.rs +expression: "&parse(\"$..entries[3:5:7]\")?" +snapshot_kind: text +--- +JsonPathQuery( + segments: [ + Descendant(Selectors( + inner: [ + Name(JsonString( + quoted: "\"entries\"", + )), + ], + )), + Child(Selectors( + inner: [ + Slice(Slice( + start: FromStart(JsonUInt(3)), + end: Some(FromStart(JsonUInt(5))), + step: Forward(JsonUInt(7)), + )), + ], + )), + ], +) diff --git a/crates/rsonpath-test/Cargo.toml b/crates/rsonpath-test/Cargo.toml index 1d234cdc..081b11bd 100644 --- a/crates/rsonpath-test/Cargo.toml +++ b/crates/rsonpath-test/Cargo.toml @@ -14,8 +14,8 @@ rust-version = "1.70.0" publish = false [dependencies] -serde = { version = "1.0.216", features = ["derive"] } -serde_json = "1.0.133" +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } [dev-dependencies] pretty_assertions = { workspace = true } diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock index 76ce6ddc..10e2719b 100644 --- a/fuzz/Cargo.lock +++ b/fuzz/Cargo.lock @@ -207,7 +207,6 @@ dependencies = [ name = "rsonpath-lib" version = "0.9.3" dependencies = [ - "arbitrary", "cfg-if", "log", "memmap2", @@ -248,18 +247,18 @@ checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "serde" -version = "1.0.216" +version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e" +checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.216" +version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e" +checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" dependencies = [ "proc-macro2", "quote", diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 22a7390b..e3a62012 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -10,12 +10,11 @@ cargo-fuzz = true [dependencies] arbitrary = { version = "1.4.*", features = ["derive"] } libfuzzer-sys = { version = "0.4.8" } -serde = "1.0.216" +serde = "1.0.217" serde_json = "1.0.134" [dependencies.rsonpath-lib] path = "../crates/rsonpath-lib" -features = ["arbitrary"] [dependencies.rsonpath-syntax] path = "../crates/rsonpath-syntax"