From feefcc94cd60fef2a270276d759b129f4f1b132e Mon Sep 17 00:00:00 2001 From: Dmitry Dygalo Date: Thu, 12 Sep 2024 09:08:15 +0200 Subject: [PATCH] refactor: Rework test suite Signed-off-by: Dmitry Dygalo --- .github/dependabot.yml | 12 - .github/workflows/{build.yml => ci.yml} | 91 +- .github/workflows/codspeed.yml | 7 +- .github/workflows/python-release.yml | 67 +- .github/workflows/rust-release.yml | 14 +- .gitignore | 8 +- .gitmodules | 4 +- .pre-commit-config.yaml | 35 - CHANGELOG.md | 9 + CONTRIBUTING.md | 93 + Cargo.toml | 41 + README.md | 290 +- bench_helpers/Cargo.toml | 12 - bench_helpers/src/lib.rs | 112 - bindings/python/rust-toolchain | 1 - bindings/python/rustfmt.toml | 2 - bindings/python/tox.ini | 13 - crates/benchmark-suite/Cargo.toml | 36 + crates/benchmark-suite/README.md | 76 + crates/benchmark-suite/benches/boon.rs | 50 + crates/benchmark-suite/benches/jsonschema.rs | 52 + .../benches/jsonschema_valid.rs | 42 + crates/benchmark-suite/benches/valico.rs | 50 + crates/benchmark-suite/src/lib.rs | 1 + crates/benchmark/Cargo.toml | 16 + crates/benchmark/README.md | 19 + .../benchmark}/data/canada.json | 0 .../benchmark}/data/citm_catalog.json | 0 .../benchmark}/data/citm_catalog_schema.json | 0 .../benchmark}/data/fast_invalid.json | 0 .../benchmark}/data/fast_schema.json | 0 .../benchmark}/data/fast_valid.json | 0 .../benchmark}/data/geojson.json | 0 .../benchmark}/data/keywords.json | 0 .../benchmark}/data/kubernetes.json | 0 .../benchmark}/data/openapi.json | 0 .../benchmark}/data/swagger.json | 0 .../benchmark}/data/zuora.json | 0 crates/benchmark/src/lib.rs | 147 + crates/jsonschema-cli/Cargo.toml | 24 + crates/jsonschema-cli/README.md | 63 + crates/jsonschema-cli/src/main.rs | 91 + .../jsonschema-py}/.gitignore | 0 .../jsonschema-py}/CHANGELOG.md | 4 + .../jsonschema-py}/Cargo.toml | 27 +- .../python => crates/jsonschema-py}/README.md | 2 +- .../jsonschema-py}/benches/bench.py | 2 +- .../jsonschema-py}/benches/conftest.py | 0 .../python => crates/jsonschema-py}/build.rs | 0 .../jsonschema-py}/pyproject.toml | 61 +- .../python/jsonschema_rs/__init__.py | 0 .../python/jsonschema_rs/__init__.pyi | 0 .../python/jsonschema_rs/py.typed | 0 crates/jsonschema-py/rust-toolchain.toml | 4 + .../jsonschema-py}/src/ffi.rs | 0 .../jsonschema-py}/src/lib.rs | 16 - .../jsonschema-py}/src/ser.rs | 0 .../jsonschema-py}/src/types.rs | 0 .../tests-py/test_jsonschema.py | 0 .../jsonschema-py}/tests-py/test_suite.py | 16 +- crates/jsonschema-py/tox.ini | 16 + crates/jsonschema-py/uv.lock | 290 ++ .../jsonschema-testsuite-codegen/Cargo.toml | 21 + .../src/config.rs | 95 + .../src/generator.rs | 114 + .../src/idents.rs | 20 + .../jsonschema-testsuite-codegen/src/lib.rs | 71 + .../src/loader.rs | 73 + .../jsonschema-testsuite-codegen/src/mocks.rs | 54 + .../jsonschema-testsuite-internal/Cargo.toml | 13 + .../jsonschema-testsuite-internal/src/lib.rs | 38 + crates/jsonschema-testsuite/Cargo.toml | 11 + crates/jsonschema-testsuite/README.md | 47 + crates/jsonschema-testsuite/src/lib.rs | 2 + crates/jsonschema/Cargo.toml | 69 + crates/jsonschema/benches/jsonschema.rs | 52 + crates/jsonschema/benches/keywords.rs | 50 + .../draft2019-09/meta/applicator.json | 0 .../draft2019-09/meta/content.json | 0 .../meta_schemas/draft2019-09/meta/core.json | 0 .../draft2019-09/meta/format.json | 0 .../draft2019-09/meta/meta-data.json | 0 .../draft2019-09/meta/validation.json | 0 .../meta_schemas/draft2019-09/schema.json | 0 .../draft2020-12/meta/applicator.json | 0 .../draft2020-12/meta/content.json | 0 .../meta_schemas/draft2020-12/meta/core.json | 0 .../draft2020-12/meta/format-annotation.json | 0 .../draft2020-12/meta/meta-data.json | 0 .../draft2020-12/meta/unevaluated.json | 0 .../draft2020-12/meta/validation.json | 0 .../meta_schemas/draft2020-12/schema.json | 0 .../jsonschema}/meta_schemas/draft4.json | 0 .../jsonschema}/meta_schemas/draft6.json | 0 .../jsonschema}/meta_schemas/draft7.json | 0 .../jsonschema}/src/compilation/context.rs | 0 .../jsonschema}/src/compilation/mod.rs | 6 +- .../jsonschema}/src/compilation/options.rs | 276 +- .../jsonschema}/src/content_encoding.rs | 0 .../jsonschema}/src/content_media_type.rs | 0 .../jsonschema}/src/error.rs | 2 +- .../src/keywords/additional_items.rs | 0 .../src/keywords/additional_properties.rs | 0 .../jsonschema}/src/keywords/all_of.rs | 0 .../jsonschema}/src/keywords/any_of.rs | 0 .../jsonschema}/src/keywords/boolean.rs | 0 .../jsonschema}/src/keywords/const_.rs | 0 .../jsonschema}/src/keywords/contains.rs | 31 - .../jsonschema}/src/keywords/content.rs | 0 .../jsonschema}/src/keywords/custom.rs | 0 .../jsonschema}/src/keywords/dependencies.rs | 0 .../jsonschema}/src/keywords/enum_.rs | 0 .../src/keywords/exclusive_maximum.rs | 0 .../src/keywords/exclusive_minimum.rs | 0 .../jsonschema}/src/keywords/format.rs | 18 +- .../jsonschema}/src/keywords/helpers.rs | 0 .../jsonschema}/src/keywords/if_.rs | 0 .../jsonschema}/src/keywords/items.rs | 1 - .../src/keywords/legacy/maximum_draft_4.rs | 0 .../src/keywords/legacy/minimum_draft_4.rs | 0 .../jsonschema}/src/keywords/legacy/mod.rs | 0 .../src/keywords/legacy/type_draft_4.rs | 0 .../jsonschema}/src/keywords/max_items.rs | 5 - .../jsonschema}/src/keywords/max_length.rs | 5 - .../src/keywords/max_properties.rs | 5 - .../jsonschema}/src/keywords/maximum.rs | 0 .../jsonschema}/src/keywords/min_items.rs | 5 - .../jsonschema}/src/keywords/min_length.rs | 5 - .../src/keywords/min_properties.rs | 5 - .../jsonschema}/src/keywords/minimum.rs | 0 .../jsonschema}/src/keywords/mod.rs | 1 - .../jsonschema}/src/keywords/multiple_of.rs | 0 .../jsonschema}/src/keywords/not.rs | 0 .../jsonschema}/src/keywords/one_of.rs | 0 .../jsonschema}/src/keywords/pattern.rs | 0 .../src/keywords/pattern_properties.rs | 0 .../jsonschema}/src/keywords/prefix_items.rs | 2 +- .../jsonschema}/src/keywords/properties.rs | 0 .../src/keywords/property_names.rs | 0 .../jsonschema}/src/keywords/ref_.rs | 107 +- .../jsonschema}/src/keywords/required.rs | 0 .../jsonschema}/src/keywords/type_.rs | 0 .../src/keywords/unevaluated_properties.rs | 41 +- .../jsonschema}/src/keywords/unique_items.rs | 0 crates/jsonschema/src/lib.rs | 562 ++++ .../jsonschema}/src/output.rs | 0 .../jsonschema}/src/paths.rs | 0 .../jsonschema}/src/primitive_type.rs | 0 .../jsonschema}/src/properties.rs | 0 .../jsonschema}/src/resolver.rs | 0 .../jsonschema}/src/schema_node.rs | 0 .../jsonschema}/src/schemas.rs | 124 +- .../jsonschema}/src/validator.rs | 0 .../tests/draft7_instance_paths.json | 2257 +++++++++++++ .../jsonschema}/tests/output.rs | 0 crates/jsonschema/tests/suite | 1 + crates/jsonschema/tests/suite.rs | 255 ++ jsonschema-test-suite/CHANGELOG.md | 14 - jsonschema-test-suite/Cargo.toml | 31 - jsonschema-test-suite/LICENSE | 21 - jsonschema-test-suite/README.md | 75 - jsonschema-test-suite/proc_macro/.gitignore | 2 - jsonschema-test-suite/proc_macro/Cargo.toml | 31 - jsonschema-test-suite/proc_macro/README.md | 8 - .../proc_macro/src/attribute_config.rs | 48 - jsonschema-test-suite/proc_macro/src/lib.rs | 138 - .../proc_macro/src/mockito_mocks.rs | 59 - .../proc_macro/src/test_case.rs | 153 - jsonschema-test-suite/rustfmt.toml | 2 - jsonschema-test-suite/src/lib.rs | 101 - jsonschema-test-suite/test_case/.gitignore | 2 - jsonschema-test-suite/test_case/Cargo.toml | 20 - jsonschema-test-suite/test_case/README.md | 9 - jsonschema-test-suite/test_case/src/lib.rs | 54 - .../tests/introduce-failure.rs | 19 - jsonschema-test-suite/tests/no-op.rs | 4 - jsonschema/Cargo.toml | 104 - jsonschema/benches/boon.rs | 53 - jsonschema/benches/jsonschema.rs | 166 - jsonschema/benches/jsonschema_valid.rs | 100 - jsonschema/benches/valico.rs | 98 - jsonschema/rustfmt.toml | 2 - jsonschema/src/lib.rs | 273 -- jsonschema/src/main.rs | 93 - jsonschema/tests/draft7_instance_paths.json | 2866 ----------------- jsonschema/tests/suite | 1 - jsonschema/tests/test_suite.rs | 212 -- perf-helpers/.gitignore | 3 - perf-helpers/Cargo.toml | 14 - perf-helpers/run | 31 - perf-helpers/rustfmt.toml | 2 - perf-helpers/src/main.rs | 24 - bench_helpers/rustfmt.toml => rustfmt.toml | 0 193 files changed, 5327 insertions(+), 5866 deletions(-) rename .github/workflows/{build.yml => ci.yml} (61%) delete mode 100644 .pre-commit-config.yaml create mode 100644 CONTRIBUTING.md create mode 100644 Cargo.toml delete mode 100644 bench_helpers/Cargo.toml delete mode 100644 bench_helpers/src/lib.rs delete mode 100644 bindings/python/rust-toolchain delete mode 100644 bindings/python/rustfmt.toml delete mode 100644 bindings/python/tox.ini create mode 100644 crates/benchmark-suite/Cargo.toml create mode 100644 crates/benchmark-suite/README.md create mode 100644 crates/benchmark-suite/benches/boon.rs create mode 100644 crates/benchmark-suite/benches/jsonschema.rs create mode 100644 crates/benchmark-suite/benches/jsonschema_valid.rs create mode 100644 crates/benchmark-suite/benches/valico.rs create mode 100644 crates/benchmark-suite/src/lib.rs create mode 100644 crates/benchmark/Cargo.toml create mode 100644 crates/benchmark/README.md rename {jsonschema/benches => crates/benchmark}/data/canada.json (100%) rename {jsonschema/benches => crates/benchmark}/data/citm_catalog.json (100%) rename {jsonschema/benches => crates/benchmark}/data/citm_catalog_schema.json (100%) rename {jsonschema/benches => crates/benchmark}/data/fast_invalid.json (100%) rename {jsonschema/benches => crates/benchmark}/data/fast_schema.json (100%) rename {jsonschema/benches => crates/benchmark}/data/fast_valid.json (100%) rename {jsonschema/benches => crates/benchmark}/data/geojson.json (100%) rename {jsonschema/benches => crates/benchmark}/data/keywords.json (100%) rename {jsonschema/benches => crates/benchmark}/data/kubernetes.json (100%) rename {jsonschema/benches => crates/benchmark}/data/openapi.json (100%) rename {jsonschema/benches => crates/benchmark}/data/swagger.json (100%) rename {jsonschema/benches => crates/benchmark}/data/zuora.json (100%) create mode 100644 crates/benchmark/src/lib.rs create mode 100644 crates/jsonschema-cli/Cargo.toml create mode 100644 crates/jsonschema-cli/README.md create mode 100644 crates/jsonschema-cli/src/main.rs rename {bindings/python => crates/jsonschema-py}/.gitignore (100%) rename {bindings/python => crates/jsonschema-py}/CHANGELOG.md (99%) rename {bindings/python => crates/jsonschema-py}/Cargo.toml (53%) rename {bindings/python => crates/jsonschema-py}/README.md (99%) rename {bindings/python => crates/jsonschema-py}/benches/bench.py (98%) rename {bindings/python => crates/jsonschema-py}/benches/conftest.py (100%) rename {bindings/python => crates/jsonschema-py}/build.rs (100%) rename {bindings/python => crates/jsonschema-py}/pyproject.toml (58%) rename {bindings/python => crates/jsonschema-py}/python/jsonschema_rs/__init__.py (100%) rename {bindings/python => crates/jsonschema-py}/python/jsonschema_rs/__init__.pyi (100%) rename {bindings/python => crates/jsonschema-py}/python/jsonschema_rs/py.typed (100%) create mode 100644 crates/jsonschema-py/rust-toolchain.toml rename {bindings/python => crates/jsonschema-py}/src/ffi.rs (100%) rename {bindings/python => crates/jsonschema-py}/src/lib.rs (97%) rename {bindings/python => crates/jsonschema-py}/src/ser.rs (100%) rename {bindings/python => crates/jsonschema-py}/src/types.rs (100%) rename {bindings/python => crates/jsonschema-py}/tests-py/test_jsonschema.py (100%) rename {bindings/python => crates/jsonschema-py}/tests-py/test_suite.py (86%) create mode 100644 crates/jsonschema-py/tox.ini create mode 100644 crates/jsonschema-py/uv.lock create mode 100644 crates/jsonschema-testsuite-codegen/Cargo.toml create mode 100644 crates/jsonschema-testsuite-codegen/src/config.rs create mode 100644 crates/jsonschema-testsuite-codegen/src/generator.rs create mode 100644 crates/jsonschema-testsuite-codegen/src/idents.rs create mode 100644 crates/jsonschema-testsuite-codegen/src/lib.rs create mode 100644 crates/jsonschema-testsuite-codegen/src/loader.rs create mode 100644 crates/jsonschema-testsuite-codegen/src/mocks.rs create mode 100644 crates/jsonschema-testsuite-internal/Cargo.toml create mode 100644 crates/jsonschema-testsuite-internal/src/lib.rs create mode 100644 crates/jsonschema-testsuite/Cargo.toml create mode 100644 crates/jsonschema-testsuite/README.md create mode 100644 crates/jsonschema-testsuite/src/lib.rs create mode 100644 crates/jsonschema/Cargo.toml create mode 100644 crates/jsonschema/benches/jsonschema.rs create mode 100644 crates/jsonschema/benches/keywords.rs rename {jsonschema => crates/jsonschema}/meta_schemas/draft2019-09/meta/applicator.json (100%) rename {jsonschema => crates/jsonschema}/meta_schemas/draft2019-09/meta/content.json (100%) rename {jsonschema => crates/jsonschema}/meta_schemas/draft2019-09/meta/core.json (100%) rename {jsonschema => crates/jsonschema}/meta_schemas/draft2019-09/meta/format.json (100%) rename {jsonschema => crates/jsonschema}/meta_schemas/draft2019-09/meta/meta-data.json (100%) rename {jsonschema => crates/jsonschema}/meta_schemas/draft2019-09/meta/validation.json (100%) rename {jsonschema => crates/jsonschema}/meta_schemas/draft2019-09/schema.json (100%) rename {jsonschema => crates/jsonschema}/meta_schemas/draft2020-12/meta/applicator.json (100%) rename {jsonschema => crates/jsonschema}/meta_schemas/draft2020-12/meta/content.json (100%) rename {jsonschema => crates/jsonschema}/meta_schemas/draft2020-12/meta/core.json (100%) rename {jsonschema => crates/jsonschema}/meta_schemas/draft2020-12/meta/format-annotation.json (100%) rename {jsonschema => crates/jsonschema}/meta_schemas/draft2020-12/meta/meta-data.json (100%) rename {jsonschema => crates/jsonschema}/meta_schemas/draft2020-12/meta/unevaluated.json (100%) rename {jsonschema => crates/jsonschema}/meta_schemas/draft2020-12/meta/validation.json (100%) rename {jsonschema => crates/jsonschema}/meta_schemas/draft2020-12/schema.json (100%) rename {jsonschema => crates/jsonschema}/meta_schemas/draft4.json (100%) rename {jsonschema => crates/jsonschema}/meta_schemas/draft6.json (100%) rename {jsonschema => crates/jsonschema}/meta_schemas/draft7.json (100%) rename {jsonschema => crates/jsonschema}/src/compilation/context.rs (100%) rename {jsonschema => crates/jsonschema}/src/compilation/mod.rs (99%) rename {jsonschema => crates/jsonschema}/src/compilation/options.rs (81%) rename {jsonschema => crates/jsonschema}/src/content_encoding.rs (100%) rename {jsonschema => crates/jsonschema}/src/content_media_type.rs (100%) rename {jsonschema => crates/jsonschema}/src/error.rs (99%) rename {jsonschema => crates/jsonschema}/src/keywords/additional_items.rs (100%) rename {jsonschema => crates/jsonschema}/src/keywords/additional_properties.rs (100%) rename {jsonschema => crates/jsonschema}/src/keywords/all_of.rs (100%) rename {jsonschema => crates/jsonschema}/src/keywords/any_of.rs (100%) rename {jsonschema => crates/jsonschema}/src/keywords/boolean.rs (100%) rename {jsonschema => crates/jsonschema}/src/keywords/const_.rs (100%) rename {jsonschema => crates/jsonschema}/src/keywords/contains.rs (92%) rename {jsonschema => crates/jsonschema}/src/keywords/content.rs (100%) rename {jsonschema => crates/jsonschema}/src/keywords/custom.rs (100%) rename {jsonschema => crates/jsonschema}/src/keywords/dependencies.rs (100%) rename {jsonschema => crates/jsonschema}/src/keywords/enum_.rs (100%) rename {jsonschema => crates/jsonschema}/src/keywords/exclusive_maximum.rs (100%) rename {jsonschema => crates/jsonschema}/src/keywords/exclusive_minimum.rs (100%) rename {jsonschema => crates/jsonschema}/src/keywords/format.rs (97%) rename {jsonschema => crates/jsonschema}/src/keywords/helpers.rs (100%) rename {jsonschema => crates/jsonschema}/src/keywords/if_.rs (100%) rename {jsonschema => crates/jsonschema}/src/keywords/items.rs (99%) rename {jsonschema => crates/jsonschema}/src/keywords/legacy/maximum_draft_4.rs (100%) rename {jsonschema => crates/jsonschema}/src/keywords/legacy/minimum_draft_4.rs (100%) rename {jsonschema => crates/jsonschema}/src/keywords/legacy/mod.rs (100%) rename {jsonschema => crates/jsonschema}/src/keywords/legacy/type_draft_4.rs (100%) rename {jsonschema => crates/jsonschema}/src/keywords/max_items.rs (96%) rename {jsonschema => crates/jsonschema}/src/keywords/max_length.rs (96%) rename {jsonschema => crates/jsonschema}/src/keywords/max_properties.rs (95%) rename {jsonschema => crates/jsonschema}/src/keywords/maximum.rs (100%) rename {jsonschema => crates/jsonschema}/src/keywords/min_items.rs (96%) rename {jsonschema => crates/jsonschema}/src/keywords/min_length.rs (96%) rename {jsonschema => crates/jsonschema}/src/keywords/min_properties.rs (95%) rename {jsonschema => crates/jsonschema}/src/keywords/minimum.rs (100%) rename {jsonschema => crates/jsonschema}/src/keywords/mod.rs (99%) rename {jsonschema => crates/jsonschema}/src/keywords/multiple_of.rs (100%) rename {jsonschema => crates/jsonschema}/src/keywords/not.rs (100%) rename {jsonschema => crates/jsonschema}/src/keywords/one_of.rs (100%) rename {jsonschema => crates/jsonschema}/src/keywords/pattern.rs (100%) rename {jsonschema => crates/jsonschema}/src/keywords/pattern_properties.rs (100%) rename {jsonschema => crates/jsonschema}/src/keywords/prefix_items.rs (99%) rename {jsonschema => crates/jsonschema}/src/keywords/properties.rs (100%) rename {jsonschema => crates/jsonschema}/src/keywords/property_names.rs (100%) rename {jsonschema => crates/jsonschema}/src/keywords/ref_.rs (69%) rename {jsonschema => crates/jsonschema}/src/keywords/required.rs (100%) rename {jsonschema => crates/jsonschema}/src/keywords/type_.rs (100%) rename {jsonschema => crates/jsonschema}/src/keywords/unevaluated_properties.rs (98%) rename {jsonschema => crates/jsonschema}/src/keywords/unique_items.rs (100%) create mode 100644 crates/jsonschema/src/lib.rs rename {jsonschema => crates/jsonschema}/src/output.rs (100%) rename {jsonschema => crates/jsonschema}/src/paths.rs (100%) rename {jsonschema => crates/jsonschema}/src/primitive_type.rs (100%) rename {jsonschema => crates/jsonschema}/src/properties.rs (100%) rename {jsonschema => crates/jsonschema}/src/resolver.rs (100%) rename {jsonschema => crates/jsonschema}/src/schema_node.rs (100%) rename {jsonschema => crates/jsonschema}/src/schemas.rs (53%) rename {jsonschema => crates/jsonschema}/src/validator.rs (100%) create mode 100644 crates/jsonschema/tests/draft7_instance_paths.json rename {jsonschema => crates/jsonschema}/tests/output.rs (100%) create mode 160000 crates/jsonschema/tests/suite create mode 100644 crates/jsonschema/tests/suite.rs delete mode 100644 jsonschema-test-suite/CHANGELOG.md delete mode 100644 jsonschema-test-suite/Cargo.toml delete mode 100644 jsonschema-test-suite/LICENSE delete mode 100644 jsonschema-test-suite/README.md delete mode 100644 jsonschema-test-suite/proc_macro/.gitignore delete mode 100644 jsonschema-test-suite/proc_macro/Cargo.toml delete mode 100644 jsonschema-test-suite/proc_macro/README.md delete mode 100644 jsonschema-test-suite/proc_macro/src/attribute_config.rs delete mode 100644 jsonschema-test-suite/proc_macro/src/lib.rs delete mode 100644 jsonschema-test-suite/proc_macro/src/mockito_mocks.rs delete mode 100644 jsonschema-test-suite/proc_macro/src/test_case.rs delete mode 100644 jsonschema-test-suite/rustfmt.toml delete mode 100644 jsonschema-test-suite/src/lib.rs delete mode 100644 jsonschema-test-suite/test_case/.gitignore delete mode 100644 jsonschema-test-suite/test_case/Cargo.toml delete mode 100644 jsonschema-test-suite/test_case/README.md delete mode 100644 jsonschema-test-suite/test_case/src/lib.rs delete mode 100644 jsonschema-test-suite/tests/introduce-failure.rs delete mode 100644 jsonschema-test-suite/tests/no-op.rs delete mode 100644 jsonschema/Cargo.toml delete mode 100644 jsonschema/benches/boon.rs delete mode 100644 jsonschema/benches/jsonschema.rs delete mode 100644 jsonschema/benches/jsonschema_valid.rs delete mode 100644 jsonschema/benches/valico.rs delete mode 100644 jsonschema/rustfmt.toml delete mode 100644 jsonschema/src/lib.rs delete mode 100644 jsonschema/src/main.rs delete mode 100644 jsonschema/tests/draft7_instance_paths.json delete mode 160000 jsonschema/tests/suite delete mode 100644 jsonschema/tests/test_suite.rs delete mode 100644 perf-helpers/.gitignore delete mode 100644 perf-helpers/Cargo.toml delete mode 100755 perf-helpers/run delete mode 100644 perf-helpers/rustfmt.toml delete mode 100644 perf-helpers/src/main.rs rename bench_helpers/rustfmt.toml => rustfmt.toml (100%) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 81bc24ca..d50600a4 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -8,15 +8,3 @@ updates: directory: "/jsonschema" schedule: interval: "daily" - - package-ecosystem: "cargo" - directory: "/bindings/python" - schedule: - interval: "daily" - - package-ecosystem: "cargo" - directory: "/bench_helpers" - schedule: - interval: "daily" - - package-ecosystem: "cargo" - directory: "/perf-helpers" - schedule: - interval: "daily" diff --git a/.github/workflows/build.yml b/.github/workflows/ci.yml similarity index 61% rename from .github/workflows/build.yml rename to .github/workflows/ci.yml index 7a657b93..a593903b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/ci.yml @@ -17,30 +17,27 @@ jobs: fetch-depth: 0 - uses: aevea/commitsar@v0.20.2 - pre-commit: - name: Generic pre-commit checks + msrv: + name: MSRV runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 - with: - fetch-depth: 1 - - uses: actions/setup-python@v5 + - uses: dtolnay/rust-toolchain@stable with: - python-version: 3.11 + toolchain: "1.65" + + - uses: Swatinem/rust-cache@v2 - - run: pip install pre-commit - - run: pre-commit run --all-files - working-directory: ./bindings/python + - run: cargo build test-stable: strategy: fail-fast: false matrix: os: [ubuntu-22.04, macos-12, windows-2022] - draft: [draft201909, draft202012] - name: Test ${{ matrix.draft }} (stable) on ${{ matrix.os}} + name: Test on ${{ matrix.os }} runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 @@ -51,12 +48,10 @@ jobs: - uses: Swatinem/rust-cache@v2 with: - workspaces: jsonschema cache-all-crates: "true" - key: ${{ matrix.os }}-${{ matrix.draft }} + key: ${{ matrix.os }} - - run: cargo test --no-fail-fast --features ${{ matrix.draft }} - working-directory: ./jsonschema + - run: cargo test --no-fail-fast build-wasm32: strategy: @@ -77,11 +72,9 @@ jobs: - uses: Swatinem/rust-cache@v2 with: - workspaces: jsonschema cache-all-crates: "true" - - run: cargo build --target ${{ matrix.target }} --no-default-features --features=cli - working-directory: ./jsonschema + - run: cargo build --target ${{ matrix.target }} --no-default-features -p jsonschema coverage: name: Run test coverage @@ -95,7 +88,6 @@ jobs: - uses: Swatinem/rust-cache@v2 with: - workspaces: jsonschema cache-all-crates: "true" - name: Install cargo-llvm-cov @@ -103,11 +95,9 @@ jobs: - name: Run tests run: cargo llvm-cov --no-report --all-features - working-directory: ./jsonschema - name: Generate coverage reports run: cargo llvm-cov report --lcov --output-path lcov.info - working-directory: ./jsonschema - name: Upload coverage to Codecov uses: codecov/codecov-action@v4 @@ -117,12 +107,26 @@ jobs: files: lcov.info fail_ci_if_error: true + lints-python: + name: Python lints + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + + - uses: hynek/setup-cached-uv@v2 + + - run: uvx ruff check python tests-py + working-directory: crates/jsonschema-py + + - run: uvx mypy python + working-directory: crates/jsonschema-py + test-python: strategy: fail-fast: false matrix: os: [ubuntu-22.04, macos-12, windows-2022] - python-version: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12'] + python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] name: Python ${{ matrix.python-version }} on ${{ matrix.os }} runs-on: ${{ matrix.os }} @@ -130,19 +134,15 @@ jobs: - uses: actions/checkout@v4 with: submodules: true - - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - architecture: x64 - - - run: python -m pip install tox - working-directory: ./bindings/python - uses: dtolnay/rust-toolchain@stable - - name: Run ${{ matrix.python }} tox job - run: tox -e py - working-directory: ./bindings/python + - uses: hynek/setup-cached-uv@v2 + + - run: uv python install ${{ matrix.python-version }} + + - run: uvx --with="tox" --with="tox-gh-actions" --with="tox-uv" tox -e py + working-directory: crates/jsonschema-py fmt: name: Rustfmt @@ -155,19 +155,6 @@ jobs: components: rustfmt - run: cargo fmt --all -- --check - working-directory: ./jsonschema - - - run: cargo fmt --all -- --check - working-directory: ./bindings/python - - - run: cargo fmt --all -- --check - working-directory: ./bench_helpers - - - run: cargo fmt --all -- --check - working-directory: ./perf-helpers - - - run: cargo fmt --all -- --check - working-directory: ./jsonschema-test-suite clippy: name: Clippy @@ -182,16 +169,8 @@ jobs: components: clippy - uses: Swatinem/rust-cache@v2 - with: - workspaces: | - jsonschema - bindings/python - run: cargo clippy --all-targets --all-features -- -D warnings - working-directory: ./jsonschema - - - run: cargo clippy --all-targets --all-features -- -D warnings - working-directory: ./bindings/python features: name: Check features @@ -204,11 +183,7 @@ jobs: - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 - with: - workspaces: jsonschema - cache-all-crates: "true" - uses: taiki-e/install-action@cargo-hack - - run: cargo hack check --feature-powerset --lib - working-directory: ./jsonschema + - run: cargo hack check --feature-powerset diff --git a/.github/workflows/codspeed.yml b/.github/workflows/codspeed.yml index 8f38d229..6b2af23c 100644 --- a/.github/workflows/codspeed.yml +++ b/.github/workflows/codspeed.yml @@ -16,16 +16,13 @@ jobs: - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 - with: - workspaces: jsonschema - run: cargo install cargo-codspeed - - run: cargo codspeed build - working-directory: ./jsonschema + - run: cargo codspeed build -p jsonschema jsonschema - uses: CodSpeedHQ/action@v3 with: - run: cargo codspeed run jsonschema + run: cargo codspeed run -p jsonschema jsonschema token: ${{ secrets.CODSPEED_TOKEN }} working-directory: ./jsonschema diff --git a/.github/workflows/python-release.yml b/.github/workflows/python-release.yml index 95e58083..efa513bb 100644 --- a/.github/workflows/python-release.yml +++ b/.github/workflows/python-release.yml @@ -21,24 +21,22 @@ jobs: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: "3.7" - architecture: x64 - - name: Install Rust toolchain - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - profile: minimal - default: true - - name: Build sdist - uses: messense/maturin-action@v1 - with: - command: sdist - args: -m bindings/python/Cargo.toml --out dist + + - uses: dtolnay/rust-toolchain@stable + + - uses: Swatinem/rust-cache@v2 + + - uses: hynek/setup-cached-uv@v2 + + - run: uv python install "3.12" + + - run: uv build --sdist --out-dir dist + working-directory: crates/jsonschema-py + - name: Install sdist run: | - pip install dist/${{ env.PACKAGE_NAME }}-*.tar.gz --force-reinstall + uv pip install dist/${{ env.PACKAGE_NAME }}-*.tar.gz --force-reinstall + - name: Upload sdist uses: actions/upload-artifact@v4 with: @@ -49,24 +47,19 @@ jobs: runs-on: macos-12 strategy: matrix: - python-version: [ '3.7', '3.8', '3.9', '3.10', '3.11', '3.12' ] + python-version: [ '3.8', '3.9', '3.10', '3.11', '3.12' ] steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} architecture: x64 - - name: Install Rust toolchain - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - profile: minimal - default: true + - uses: dtolnay/rust-toolchain@stable - name: Build wheels - x86_64 uses: messense/maturin-action@v1 with: target: x86_64 - args: --release -m bindings/python/Cargo.toml --out dist --interpreter ${{ matrix.python-version }} + args: --release -m crates/jsonschema-py/Cargo.toml --out dist --interpreter ${{ matrix.python-version }} - name: Install built wheel - x86_64 run: | pip install dist/${{ env.PACKAGE_NAME }}-*.whl --force-reinstall @@ -80,23 +73,18 @@ jobs: runs-on: macos-12 strategy: matrix: - python-version: [ '3.7', '3.8', '3.9', '3.10', '3.11', '3.12' ] + python-version: [ '3.8', '3.9', '3.10', '3.11', '3.12' ] steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} architecture: x64 - - name: Install Rust toolchain - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - profile: minimal - default: true + - uses: dtolnay/rust-toolchain@stable - name: Build wheels - universal2 uses: messense/maturin-action@v1 with: - args: --release -m bindings/python/Cargo.toml --target universal2-apple-darwin --out dist --interpreter ${{ matrix.python-version }} + args: --release -m crates/jsonschema-py/Cargo.toml --target universal2-apple-darwin --out dist --interpreter ${{ matrix.python-version }} - name: Install built wheel - universal2 run: | pip install dist/${{ env.PACKAGE_NAME }}-*universal2.whl --force-reinstall @@ -110,7 +98,7 @@ jobs: runs-on: windows-2022 strategy: matrix: - python-version: [ '3.7', '3.8', '3.9', '3.10', '3.11', '3.12' ] + python-version: [ '3.8', '3.9', '3.10', '3.11', '3.12' ] target: [ x64, x86 ] steps: - uses: actions/checkout@v4 @@ -118,17 +106,12 @@ jobs: with: python-version: ${{ matrix.python-version }} architecture: ${{ matrix.target }} - - name: Install Rust toolchain - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - profile: minimal - default: true + - uses: dtolnay/rust-toolchain@stable - name: Build wheels uses: messense/maturin-action@v1 with: target: ${{ matrix.target }} - args: --release -m bindings/python/Cargo.toml --out dist --interpreter ${{ matrix.python-version }} + args: --release -m crates/jsonschema-py/Cargo.toml --out dist --interpreter ${{ matrix.python-version }} - name: Install built wheel shell: bash run: | @@ -143,7 +126,7 @@ jobs: runs-on: ubuntu-22.04 strategy: matrix: - python-version: [ '3.7', '3.8', '3.9', '3.10', '3.11', '3.12' ] + python-version: [ '3.8', '3.9', '3.10', '3.11', '3.12' ] target: [ x86_64, i686, aarch64 ] steps: - uses: actions/checkout@v4 @@ -155,7 +138,7 @@ jobs: with: target: ${{ matrix.target }} manylinux: auto - args: --release -m bindings/python/Cargo.toml --out dist --interpreter ${{ matrix.python-version }} + args: --release -m crates/jsonschema-py/Cargo.toml --out dist --interpreter ${{ matrix.python-version }} - name: Install built wheel on native architecture if: matrix.target == 'x86_64' run: | diff --git a/.github/workflows/rust-release.yml b/.github/workflows/rust-release.yml index 2f308984..798eaa7b 100644 --- a/.github/workflows/rust-release.yml +++ b/.github/workflows/rust-release.yml @@ -7,16 +7,14 @@ on: jobs: rust-release: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 - - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: stable - override: true + + - uses: dtolnay/rust-toolchain@stable + - run: cargo login ${CRATES_IO_TOKEN} env: CRATES_IO_TOKEN: ${{ secrets.CRATES_IO_TOKEN }} - - run: cargo publish - working-directory: ./jsonschema + + - run: cargo publish --manifest-path crates/Cargo.toml diff --git a/.gitignore b/.gitignore index 5e5e5050..7f523497 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,8 @@ # Rust -/jsonschema/target -/bindings/*/target -/bench_helpers/target -/jsonschema-test-suite/target +/crates/*/target +/target/ .hypothesis .benchmarks -/jsonschema/Cargo.lock -/jsonschema-test-suite/Cargo.lock # IDEs /.idea diff --git a/.gitmodules b/.gitmodules index b5278d8b..07fef621 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ -[submodule "jsonschema/tests/suite"] - path = jsonschema/tests/suite +[submodule "testsuite"] + path = crates/jsonschema/tests/suite url = https://github.com/json-schema-org/JSON-Schema-Test-Suite.git diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml deleted file mode 100644 index a03cd787..00000000 --- a/.pre-commit-config.yaml +++ /dev/null @@ -1,35 +0,0 @@ -default_language_version: - python: python3.11 - -repos: - - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.6.0 - hooks: - - id: check-yaml - - id: end-of-file-fixer - - id: trailing-whitespace - exclude: ^.*\.(md|rst)$ - - id: debug-statements - - id: mixed-line-ending - args: [--fix=lf] - - id: check-merge-conflict - - - repo: https://github.com/jorisroovers/gitlint - rev: v0.19.1 - hooks: - - id: gitlint - - - repo: https://github.com/adrienverge/yamllint - rev: v1.35.1 - hooks: - - id: yamllint - - - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.3.7 - hooks: - - id: ruff-format - - - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.3.7 - hooks: - - id: ruff diff --git a/CHANGELOG.md b/CHANGELOG.md index 219b32bd..8108998e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ ## [Unreleased] +### Added + +- `jsonschema::compile` shortcut. + +### Deprecated + +- `cli` feature in favor of a separate `jsonschema-cli` crate. +- `draft201909` and `draft202012` features. The relevant functionality is now enabled by default. + ## [0.18.3] - 2024-09-12 ### Fixed diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..ab0096ba --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,93 @@ +# Contributing to jsonschema + +Thank you for your interest in contributing to jsonschema! We welcome contributions from everyone in the form of suggestions, bug reports, pull requests, and feedback. This document provides guidance if you're thinking of helping out. + +## Submitting Bug Reports and Feature Requests + +When reporting a bug or asking for help, please include enough details so that others can reproduce the behavior you're seeing. + +To open an issue, [follow this link](https://github.com/Stranger6667/jsonschema-rs/issues/new) and fill out the appropriate template. + +When making a feature request, please make it clear what problem you intend to solve with the feature and provide some ideas on how to implement it. + +## Making Changes + +### Rust Toolchain + +jsonschema targets Rust 1.65 as its Minimum Supported Rust Version (MSRV). Please ensure your contributions are compatible with this version. + +You can use [rustup](https://rustup.rs/) to manage your installed toolchains. To set up the correct version for the jsonschema project: + +```console +$ rustup override set 1.65 +``` + +### Running the Tests + +The tests in jsonschema depend on the [JSON Schema Test Suite](https://github.com/json-schema-org/JSON-Schema-Test-Suite). Before running the tests, you need to download the suite: + +1. Initialize and update the git submodules: + + ```console + $ git submodule init + $ git submodule update + ``` + + This will clone the JSON Schema Test Suite to `crates/jsonschema/tests/suite/`. + +2. Run the tests: + + ```console + $ cargo test --all-features + ``` + +Make sure all tests pass before submitting your pull request. If you've added new functionality, please include appropriate tests. + +### Formatting and Linting + +Format your code using: + +```console +$ cargo fmt --all +``` + +And lint it using: + +```console +$ cargo clippy --all-targets --all-features -- -D warnings +``` + +### Adding New Functionality + +For small changes (e.g., bug fixes), feel free to submit a PR directly. + +For larger changes (e.g., new functionality or configuration options), please create an [issue](https://github.com/Stranger6667/jsonschema-rs/issues) first to discuss your proposed change. + +### Improving Documentation + +Contributions to documentation are always welcome. If you find any part of the documentation unclear or incomplete, please open an issue or submit a pull request. + +### Implementing Missing Keywords + +If you're looking to contribute code, implementing missing keywords for newer JSON Schema drafts is a great place to start. Check the [compliance badges](https://github.com/Stranger6667/jsonschema-rs#supported-drafts) to see which drafts might need work. + +### Fixing Test Cases + +Another way to contribute is by fixing failing test cases from the [JSON Schema Test Suite](https://github.com/json-schema-org/JSON-Schema-Test-Suite). You can check the current status on the [Bowtie Report](https://bowtie.report/#/implementations/rust-jsonschema). + +## Pull Requests + +1. Ensure your code passes all tests and lint checks. +2. Update the documentation as necessary. +3. Add or update tests as appropriate. +4. If you're adding new functionality, please include a description in the README. +5. If your change affects users, add an entry to the CHANGELOG. + +## Getting Help + +If you need help with contributing to jsonschema, you can: + +1. Open a [GitHub Discussion](https://github.com/Stranger6667/jsonschema-rs/discussions). +2. Ask in the pull request or issue if you've already opened one. + +Thank you for contributing to jsonschema! diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 00000000..5f027b9e --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,41 @@ +[workspace] +members = ["crates/*"] +resolver = "2" + +[workspace.package] +rust-version = "1.65" +edition = "2021" +authors = ["Dmitry Dygalo "] +repository = "https://github.com/Stranger6667/jsonschema-rs" +license = "MIT" + +[profile.release] +lto = "fat" +codegen-units = 1 + +[workspace.dependencies] +serde_json = "1" + +[workspace.lints.rust] +unsafe_code = "warn" +unreachable_pub = "warn" + +[workspace.lints.clippy] +pedantic = { level = "warn", priority = -2 } +# Allowed pedantic lints +module_name_repetitions = "allow" +similar_names = "allow" +too_many_lines = "allow" +too_many_arguments = "allow" +# Disallowed restriction lints +print_stdout = "warn" +print_stderr = "warn" +dbg_macro = "warn" +empty_drop = "warn" +empty_structs_with_brackets = "warn" +exit = "warn" +get_unwrap = "warn" +rc_buffer = "warn" +rc_mutex = "warn" +rest_pat_in_fully_bound_structs = "warn" + diff --git a/README.md b/README.md index 2e8d77ad..559b396d 100644 --- a/README.md +++ b/README.md @@ -1,270 +1,110 @@ # jsonschema -[![ci](https://github.com/Stranger6667/jsonschema-rs/workflows/ci/badge.svg)](https://github.com/Stranger6667/jsonschema-rs/actions) -[![codecov](https://codecov.io/gh/Stranger6667/jsonschema-rs/branch/master/graph/badge.svg)](https://codecov.io/gh/Stranger6667/jsonschema-rs) -[![Crates.io](https://img.shields.io/crates/v/jsonschema.svg)](https://crates.io/crates/jsonschema) -[![docs.rs](https://docs.rs/jsonschema/badge.svg)](https://docs.rs/jsonschema/) -[![gitter](https://img.shields.io/gitter/room/Stranger6667/jsonschema-rs.svg)](https://gitter.im/Stranger6667/jsonschema-rs) +[crates.io](https://crates.io/crates/jsonschema) +[docs.rs](https://docs.rs/jsonschema) +[build status](https://github.com/Stranger6667/jsonschema-rs/actions?query=branch%3Amain) +[codecov.io](https://app.codecov.io/github/Stranger6667/jsonschema-rs) +Supported Dialects -A JSON Schema validator implementation. It compiles schema into a validation tree to have validation as fast as possible. - -Supported drafts: - -- Draft 7 (except optional `idn-hostname.json` test case) -- Draft 6 -- Draft 4 (except optional `bignum.json` test case) - -Partially supported drafts (some keywords are not implemented): -- Draft 2019-09 (requires the `draft201909` feature enabled) -- Draft 2020-12 (requires the `draft202012` feature enabled) - -```toml -# Cargo.toml -jsonschema = "0.18" -``` - -To validate documents against some schema and get validation errors (if any): - -```rust -use jsonschema::JSONSchema; -use serde_json::json; - -fn main() { - let schema = json!({"maxLength": 5}); - let instance = json!("foo"); - let compiled = JSONSchema::compile(&schema) - .expect("A valid schema"); - let result = compiled.validate(&instance); - if let Err(errors) = result { - for error in errors { - println!("Validation error: {}", error); - println!( - "Instance path: {}", error.instance_path - ); - } - } -} -``` - -Each error has an `instance_path` attribute that indicates the path to the erroneous part within the validated instance. -It could be transformed to JSON Pointer via `.to_string()` or to `Vec` via `.into_vec()`. - -If you only need to know whether document is valid or not (which is faster): - -```rust -use jsonschema::is_valid; -use serde_json::json; - -fn main() { - let schema = json!({"maxLength": 5}); - let instance = json!("foo"); - assert!(is_valid(&schema, &instance)); -} -``` - -Or use a compiled schema (preferred): +A high-performance JSON Schema validator for Rust. ```rust -use jsonschema::JSONSchema; use serde_json::json; -fn main() { +fn main() -> Result<(), Box> { let schema = json!({"maxLength": 5}); let instance = json!("foo"); - // Draft is detected automatically - // with fallback to Draft7 - let compiled = JSONSchema::compile(&schema) - .expect("A valid schema"); - assert!(compiled.is_valid(&instance)); -} -``` -## Output styles + // One-off validation + assert!(jsonschema::is_valid(&schema, &instance)); -`jsonschema` supports `basic` & `flag` output styles from Draft 2019-09, so you can serialize the validation results with `serde`: - -```rust -use jsonschema::{Output, BasicOutput, JSONSchema}; -use serde_json::json; + // Build & reuse (faster) + let validator = jsonschema::compile(&schema) + .expect("Invalid schema"); -fn main() { - let schema_json = json!({ - "title": "string value", - "type": "string" - }); - let instance = json!("some string"); - let schema = JSONSchema::compile(&schema_json) - .expect("A valid schema"); - - let output: BasicOutput = schema.apply(&instance).basic(); - let output_json = serde_json::to_value(output) - .expect("Failed to serialize output"); - - assert_eq!( - output_json, - json!({ - "valid": true, - "annotations": [ - { - "keywordLocation": "", - "instanceLocation": "", - "annotations": { - "title": "string value" - } - } - ] - }) - ); -} -``` - -## Custom keywords - -`jsonschema` allows you to implement custom validation logic by defining custom keywords. -To use your own keyword, you need to implement the `Keyword` trait and add it to the `JSONSchema` instance via the `with_keyword` method: - -```rust -use jsonschema::{ - paths::{JSONPointer, JsonPointerNode}, - ErrorIterator, JSONSchema, Keyword, ValidationError, -}; -use serde_json::{json, Map, Value}; -use std::iter::once; - -struct MyCustomValidator; - -impl Keyword for MyCustomValidator { - fn validate<'instance>( - &self, - instance: &'instance Value, - instance_path: &JsonPointerNode, - ) -> ErrorIterator<'instance> { - // ... validate instance ... - if !instance.is_object() { - let error = ValidationError::custom( - JSONPointer::default(), - instance_path.into(), - instance, - "Boom!", - ); - Box::new(once(error)) - } else { - Box::new(None.into_iter()) + // Iterate over errors + if let Err(errors) = validator.validate(&instance) { + for error in errors { + eprintln!("Error: {}", error); + eprintln!("Location: {}", error.instance_path); } } - fn is_valid(&self, instance: &Value) -> bool { - // ... determine if instance is valid ... - true - } -} -// You can create a factory function, or use a closure to create new validator instances. -fn custom_validator_factory<'a>( - // Parent object where your keyword is defined - parent: &'a Map, - // Your keyword value - value: &'a Value, - // JSON Pointer to your keyword within the schema - path: JSONPointer, -) -> Result, ValidationError<'a>> { - // You may return validation error if the keyword is misused for some reason - Ok(Box::new(MyCustomValidator)) -} + // Boolean result + assert!(validator.is_valid(&instance)); -fn main() { - let schema = json!({"my-type": "my-schema"}); - let instance = json!({"a": "b"}); - let compiled = JSONSchema::options() - // Register your keyword via a factory function - .with_keyword("my-type", custom_validator_factory) - // Or use a closure - .with_keyword("my-type-with-closure", |_, _, _| Ok(Box::new(MyCustomValidator))) - .compile(&schema) - .expect("A valid schema"); - assert!(compiled.is_valid(instance)); + Ok(()) } ``` -## Reference resolving and TLS +You also can use it from the command line via the [jsonschema-cli](crates/jsonschema-cli) crate. -By default, `jsonschema` resolves HTTP references via `reqwest` without TLS support. -If you'd like to resolve HTTPS, you need to enable TLS support in `reqwest`: - -```toml -reqwest = { version = "*", features = [ "rustls-tls" ] } +```console +$ jsonschema-cli schema.json -i instance.json ``` -Otherwise, you might get validation errors like `invalid URL, scheme is not http`. +See more usage examples in the [documentation](https://docs.rs/jsonschema). -## Status +## Highlights -This library is functional and ready for use, but its API is still evolving to the 1.0 API. +- 🚀 High-performance validation +- 📚 Support for popular JSON Schema drafts +- 🔧 Custom keywords and format validators +- 🌐 Remote reference fetching (network/file) +- 🎨 `Basic` output style as per JSON Schema spec +- 🔗 Bindings for [Python](crates/jsonschema-py), [Ruby](https://github.com/driv3r/rusty_json_schema), and [JavaScript](https://github.com/ahungrynoob/jsonschema) +- 💻 Command Line Interface -## Bindings +### Supported drafts -- Python - See the `./bindings/python` directory -- Ruby - a [crate](https://github.com/driv3r/rusty_json_schema) by @driv3r -- NodeJS - a [package](https://github.com/ahungrynoob/jsonschema) by @ahungrynoob +Compliance levels vary across drafts, with newer versions having some unimplemented keywords. -## Running tests +- ![Draft 2020-12](https://img.shields.io/endpoint?url=https%3A%2F%2Fbowtie.report%2Fbadges%2Frust-jsonschema%2Fcompliance%2Fdraft2020-12.json) +- ![Draft 2019-09](https://img.shields.io/endpoint?url=https%3A%2F%2Fbowtie.report%2Fbadges%2Frust-jsonschema%2Fcompliance%2Fdraft2019-09.json) +- ![Draft 7](https://img.shields.io/endpoint?url=https%3A%2F%2Fbowtie.report%2Fbadges%2Frust-jsonschema%2Fcompliance%2Fdraft7.json) +- ![Draft 6](https://img.shields.io/endpoint?url=https%3A%2F%2Fbowtie.report%2Fbadges%2Frust-jsonschema%2Fcompliance%2Fdraft6.json) +- ![Draft 4](https://img.shields.io/endpoint?url=https%3A%2F%2Fbowtie.report%2Fbadges%2Frust-jsonschema%2Fcompliance%2Fdraft4.json) -The tests in [jsonschema/](jsonschema/) depend on the [JSON Schema Test Suite](https://github.com/json-schema-org/JSON-Schema-Test-Suite). Before calling `cargo test`, download the suite: +You can check the current status on the [Bowtie Report](https://bowtie.report/#/implementations/rust-jsonschema). -```bash -$ git submodule init -$ git submodule update -``` -These commands clone the suite to [jsonschema/tests/suite/](jsonschema/tests/suite/). +## Notable Users -Now, enter the `jsonschema` directory and run `cargo test`. - -```bash -$ cd jsonschema -$ cargo test -``` +- Tauri: [Config validation](https://github.com/tauri-apps/tauri/blob/c901d9fdf932bf7c3c77e9d3097fabb1fe0712af/crates/tauri-cli/src/helpers/config.rs#L173) +- Apollo Router: [Config file validation](https://github.com/apollographql/router/blob/855cf6cc0757ca6176970ddf3ae8c98c87c632d1/apollo-router/src/configuration/schema.rs#L120) +- QSV: [CSV record validation with custom keyword & format validator](https://github.com/jqnatividad/qsv/blob/4774cb779c7a5939e10ce32f91ac8447158c276c/src/cmd/validate.rs#L506) ## Performance -There is a comparison with other JSON Schema validators written in Rust - `jsonschema_valid==0.5.2`, `valico==4.0.0`, and `boon==0.5.0`. +`jsonschema`` outperforms other Rust JSON Schema validators in most scenarios: -Test machine i8700K (12 cores), 32GB RAM. +- Up to **20-470x** faster than `valico` and `jsonschema_valid` for complex schemas +- Generally **2-3x** faster than `boon` -Input values and schemas: +For detailed benchmarks, see our [full performance comparison](crates/benchmark-suite). -- [Zuora](https://github.com/APIs-guru/openapi-directory/blob/master/APIs/zuora.com/2021-04-23/openapi.yaml) OpenAPI schema (`zuora.json`). Validated against [OpenAPI 3.0 JSON Schema](https://github.com/OAI/OpenAPI-Specification/blob/main/schemas/v3.0/schema.json) (`openapi.json`). -- [Kubernetes](https://raw.githubusercontent.com/APIs-guru/openapi-directory/master/APIs/kubernetes.io/v1.10.0/swagger.yaml) Swagger schema (`kubernetes.json`). Validated against [Swagger JSON Schema](https://github.com/OAI/OpenAPI-Specification/blob/main/schemas/v2.0/schema.json) (`swagger.json`). -- Canadian border in GeoJSON format (`canada.json`). Schema is taken from the [GeoJSON website](https://geojson.org/schema/FeatureCollection.json) (`geojson.json`). -- Concert data catalog (`citm_catalog.json`). Schema is inferred via [infers-jsonschema](https://github.com/Stranger6667/infers-jsonschema) & manually adjusted (`citm_catalog_schema.json`). -- `Fast` is taken from [fastjsonschema benchmarks](https://github.com/horejsek/python-fastjsonschema/blob/master/performance.py#L15) (`fast_schema.json`, `fast_valid.json` and `fast_invalid.json`). +## Minimum Supported Rust Version (MSRV) -| Case | Schema size | Instance size | -| -------------- | ----------- | ------------- | -| OpenAPI | 18 KB | 4.5 MB | -| Swagger | 25 KB | 3.0 MB | -| Canada | 4.8 KB | 2.1 MB | -| CITM catalog | 2.3 KB | 501 KB | -| Fast (valid) | 595 B | 55 B | -| Fast (invalid) | 595 B | 60 B | +This crate requires Rust 1.65 or later. -Here is the average time for each contender to validate. Ratios are given against compiled `JSONSchema` using its `validate` method. The `is_valid` method is faster, but gives only a boolean return value: +## Support -| Case | jsonschema_valid | valico | boon | jsonschema (validate) | jsonschema (is_valid) | -| -------------- | ----------------------- | ----------------------- | ------------------------ | ---------------------- | ---------------------- | -| OpenAPI | - (1) | - (1) | 11.71 ms (**x3.34**) | 3.500 ms | 3.147 ms (**x0.89**) | -| Swagger | - (2) | 180.65 ms (**x32.12**) | 16.01 ms (**x2.84**) | 5.623 ms | 3.634 ms (**x0.64**) | -| Canada | 40.363 ms (**x33.13**) | 427.40 ms (**x350.90**) | 25.50 ms (**x20.93**) | 1.218 ms | 1.217 ms (**x0.99**) | -| CITM catalog | 5.357 ms (**x2.51**) | 39.215 ms (**x18.44**) | 1.58 ms (**x0.74**) | 2.126 ms | 569.23 us (**x0.26**) | -| Fast (valid) | 2.27 us (**x4.87**) | 6.55 us (**x14.05**) | 542.2 us (**x1.16**) | 465.89 ns | 113.94 ns (**x0.24**) | -| Fast (invalid) | 412.21 ns (**x0.46**) | 6.69 us (**x7.61**) | 787.12 us (**x0.89**) | 878.23 ns | 4.21ns (**x0.004**) | +If you have questions, need help, or want to suggest improvements, please use [GitHub Discussions](https://github.com/Stranger6667/jsonschema-rs/discussions). -Notes: +## Sponsorship -1. `jsonschema_valid` and `valico` do not handle valid path instances matching the `^\\/` regex. +If you find `jsonschema` useful, please consider [sponsoring its development](https://github.com/sponsors/Stranger6667). -2. `jsonschema_valid` fails to resolve local references (e.g. `#/definitions/definitions`). +## Contributing -You can find benchmark code in `benches/jsonschema.rs`, Rust version is `1.78`. +We welcome contributions! Here's how you can help: -## Support +- Share your use cases +- Implement missing keywords +- Fix failing test cases from the [JSON Schema test suite](https://bowtie.report/#/implementations/rust-jsonschema) + +See [CONTRIBUTING.md](CONTRIBUTING.md) for more details. + +## License + +Licensed under [MIT License](LICENSE). -If you have anything to discuss regarding this library, please, join our [gitter](https://gitter.im/Stranger6667/jsonschema-rs)! diff --git a/bench_helpers/Cargo.toml b/bench_helpers/Cargo.toml deleted file mode 100644 index 866ff1f6..00000000 --- a/bench_helpers/Cargo.toml +++ /dev/null @@ -1,12 +0,0 @@ -[package] -name = "bench_helpers" -version = "0.1.0" -authors = ["Dmitry Dygalo "] -edition = "2021" -license = "MIT" - -[dependencies] -serde = { version = "1", features = ["derive"] } -serde_json = "1" -criterion = { version = "0.5.1", features = [], default-features = false } -codspeed-criterion-compat = "2.6.0" diff --git a/bench_helpers/src/lib.rs b/bench_helpers/src/lib.rs deleted file mode 100644 index 0ff62b6b..00000000 --- a/bench_helpers/src/lib.rs +++ /dev/null @@ -1,112 +0,0 @@ -use codspeed_criterion_compat::Criterion; -use serde::Deserialize; -use serde_json::{from_reader, Value}; -use std::{ - fs::{read_to_string, File}, - io::BufReader, -}; - -#[derive(Debug, Deserialize)] -struct Benchmark<'a> { - name: &'a str, - schema: Value, - valid: Option>, - invalid: Vec, -} - -pub fn read_json(filepath: &str) -> Value { - let file = File::open(filepath).expect("Failed to open file"); - let reader = BufReader::new(file); - from_reader(reader).expect("Invalid JSON") -} - -fn strip_characters(original: &str) -> String { - original - .chars() - .filter(|&c| !"{}:\" ,[]".contains(c)) - .collect() -} - -pub fn bench_openapi(bench: &mut dyn FnMut(&str, Value, Value)) { - let schema = read_json("benches/data/openapi.json"); - let instance = read_json("benches/data/zuora.json"); - bench("openapi", schema, instance) -} - -pub fn bench_swagger(bench: &mut dyn FnMut(&str, Value, Value)) { - let schema = read_json("benches/data/swagger.json"); - let instance = read_json("benches/data/kubernetes.json"); - bench("swagger", schema, instance) -} - -pub fn bench_geojson(bench: &mut dyn FnMut(&str, Value, Value)) { - let schema = read_json("benches/data/geojson.json"); - let instance = read_json("benches/data/canada.json"); - bench("geojson", schema, instance) -} - -pub fn bench_citm(bench: &mut dyn FnMut(&str, Value, Value)) { - let schema = read_json("benches/data/citm_catalog_schema.json"); - let instance = read_json("benches/data/citm_catalog.json"); - bench("CITM", schema, instance) -} - -pub fn bench_fast(bench: &mut dyn FnMut(&str, Value, Value, Value)) { - let schema = read_json("benches/data/fast_schema.json"); - let valid = read_json("benches/data/fast_valid.json"); - let invalid = read_json("benches/data/fast_invalid.json"); - bench("fast", schema, valid, invalid) -} - -pub fn bench_keywords( - c: &mut Criterion, - is_skipped: &dyn Fn(&str) -> bool, - is_valid: &dyn Fn(&Value, &Value) -> bool, - bench_compile: &mut dyn FnMut(&mut Criterion, &str, &Value), - bench_valid: fn(&mut Criterion, &str, &Value, &Value), - bench_invalid: fn(&mut Criterion, &str, &Value, &Value), -) { - let content = read_to_string("benches/data/keywords.json").expect("Can't read file"); - let data: Vec = serde_json::from_str(&content).expect("Deserialization error"); - for benchmark in data { - if is_skipped(benchmark.name) { - eprintln!("Skip {}", benchmark.name); - continue; - } - // Schema compilation - let suffix = strip_characters(&benchmark.schema.to_string()); - bench_compile( - c, - &format!("{} {}", benchmark.name, suffix), - &benchmark.schema, - ); - // Valid cases - if let Some(valid_cases) = benchmark.valid { - for instance in valid_cases { - if !is_valid(&benchmark.schema, &instance) { - eprintln!("This instance should be VALID: {}", benchmark.name); - } - let suffix = strip_characters(&instance.to_string()); - bench_valid( - c, - &format!("{} {}", benchmark.name, suffix), - &benchmark.schema, - &instance, - ) - } - } - // Invalid cases - for instance in benchmark.invalid { - if is_valid(&benchmark.schema, &instance) { - eprintln!("This instance should be INVALID: {}", benchmark.name); - } - let suffix = strip_characters(&instance.to_string()); - bench_invalid( - c, - &format!("{} {}", benchmark.name, suffix), - &benchmark.schema, - &instance, - ) - } - } -} diff --git a/bindings/python/rust-toolchain b/bindings/python/rust-toolchain deleted file mode 100644 index 2bf5ad04..00000000 --- a/bindings/python/rust-toolchain +++ /dev/null @@ -1 +0,0 @@ -stable diff --git a/bindings/python/rustfmt.toml b/bindings/python/rustfmt.toml deleted file mode 100644 index bf772232..00000000 --- a/bindings/python/rustfmt.toml +++ /dev/null @@ -1,2 +0,0 @@ -imports_granularity = "Crate" -edition = "2021" diff --git a/bindings/python/tox.ini b/bindings/python/tox.ini deleted file mode 100644 index 78a750f4..00000000 --- a/bindings/python/tox.ini +++ /dev/null @@ -1,13 +0,0 @@ -[tox] -skipsdist = True -envlist = py{37,38,39,310,311,312} - -[testenv] -deps = - flask - pytest - pytest-benchmark - hypothesis -commands = - pip install -e . - python -m pytest tests-py {posargs:} diff --git a/crates/benchmark-suite/Cargo.toml b/crates/benchmark-suite/Cargo.toml new file mode 100644 index 00000000..2504e214 --- /dev/null +++ b/crates/benchmark-suite/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "benchmark-suite" +version = "0.1.0" +readme = "README.md" +rust-version.workspace = true +edition.workspace = true +authors.workspace = true +repository.workspace = true +license.workspace = true +publish = false + +[dev-dependencies] +benchmark = { path = "../benchmark/" } +boon = "0.6.0" +criterion = { version = "0.5", default-features = false } +jsonschema = { path = "../jsonschema/" } +jsonschema-valid = "0.5.2" +serde_json.workspace = true +valico = "4.0.0" + +[[bench]] +harness = false +name = "jsonschema" + +[[bench]] +harness = false +name = "valico" + +[[bench]] +harness = false +name = "jsonschema_valid" + +[[bench]] +harness = false +name = "boon" + diff --git a/crates/benchmark-suite/README.md b/crates/benchmark-suite/README.md new file mode 100644 index 00000000..38a0f0dc --- /dev/null +++ b/crates/benchmark-suite/README.md @@ -0,0 +1,76 @@ +# Benchmark Suite + +A benchmarking suite for comparing different Rust JSON Schema implementations. + +## Implementations + +- `jsonschema` (latest version in this repo) +- [valico](https://crates.io/crates/valico) (v4.0.0) +- [jsonschema-valid](https://crates.io/crates/jsonschema-valid) (v0.5.2) +- [boon](https://crates.io/crates/boon) (v0.6.0) + +## Usage + +To run the benchmarks: + +```console +$ cargo bench +``` + +## Overview + +| Benchmark | Description | Schema Size | Instance Size | +|----------|------------------------------------------------|-------------|---------------| +| OpenAPI | Zuora API validated against OpenAPI 3.0 schema | 18 KB | 4.5 MB | +| Swagger | Kubernetes API (v1.10.0) with Swagger schema | 25 KB | 3.0 MB | +| GeoJSON | Canadian border in GeoJSON format | 4.8 KB | 2.1 MB | +| CITM | Concert data catalog with inferred schema | 2.3 KB | 501 KB | +| Fast | From fastjsonschema benchmarks (valid/invalid) | 595 B | 55 B / 60 B | + +Sources: +- OpenAPI: [Zuora](https://github.com/APIs-guru/openapi-directory/blob/master/APIs/zuora.com/2021-04-23/openapi.yaml), [Schema](https://github.com/OAI/OpenAPI-Specification/blob/main/schemas/v3.0/schema.json) +- Swagger: [Kubernetes](https://raw.githubusercontent.com/APIs-guru/openapi-directory/master/APIs/kubernetes.io/v1.10.0/swagger.yaml), [Schema](https://github.com/OAI/OpenAPI-Specification/blob/main/schemas/v2.0/schema.json) +- GeoJSON: [Schema](https://geojson.org/schema/FeatureCollection.json) +- CITM: Schema inferred via [infers-jsonschema](https://github.com/Stranger6667/infers-jsonschema) +- Fast: [fastjsonschema benchmarks](https://github.com/horejsek/python-fastjsonschema/blob/master/performance.py#L15) + +## Results + +### Comparison with Other Libraries + +| Benchmark | jsonschema_valid | valico | boon | jsonschema (validate) | +|---------------|------------------|---------------|---------------|------------------------| +| OpenAPI | - | - | 12.23 ms (**x2.25**) | 5.43 ms | +| Swagger | - | 201.98 ms (**x28.98**) | 18.24 ms (**x2.62**) | 6.97 ms | +| GeoJSON | 35.75 ms (**x30.56**) | 559.52 ms (**x478.22**) | 29.01 ms (**x24.79**) | 1.17 ms | +| CITM Catalog | 5.51 ms (**x2.45**) | 46.31 ms (**x20.58**) | 2.07 ms (**x0.92**) | 2.25 ms | +| Fast (Valid) | 2.10 µs (**x4.27**) | 6.61 µs (**x13.44**) | 597.00 ns (**x1.21**) | 491.82 ns | +| Fast (Invalid)| 368.53 ns (**x0.56**) | 6.77 µs (**x10.22**) | 748.22 ns (**x1.13**) | 662.52 ns | + +### jsonschema Performance: `validate` vs `is_valid` + +| Benchmark | validate | is_valid | Speedup | +|---------------|------------|------------|---------| +| OpenAPI | 5.43 ms | 4.70 ms | 1.16x | +| Swagger | 6.97 ms | 5.35 ms | 1.30x | +| GeoJSON | 1.17 ms | 1.16 ms | 1.00x | +| CITM Catalog | 2.2510 ms | 630.82 µs | 3.57x | +| Fast (Valid) | 491.82 ns | 111.09 ns | 4.43x | +| Fast (Invalid)| 662.52 ns | 7.1289 ns | 92.93x | + +Notes: + +1. `jsonschema_valid` and `valico` do not handle valid path instances matching the `^\\/` regex. + +2. `jsonschema_valid` fails to resolve local references (e.g. `#/definitions/definitions`). + +You can find benchmark code in [benches/](benches/), Rust version is `1.81`. + +## Purpose + +The `benchmark-suite` crate provides a standardized way to measure and compare the performance of various JSON Schema validation libraries in Rust. It helps in identifying performance bottlenecks and guiding optimization efforts for the `jsonschema` crate. + +## Contributing + +Contributions to improve, expand, or optimize the benchmark suite are welcome. This includes adding new benchmarks, ensuring fair representation of real-world use cases, and optimizing the configuration and usage of benchmarked libraries. Such efforts are highly appreciated as they ensure accurate and meaningful performance comparisons. + diff --git a/crates/benchmark-suite/benches/boon.rs b/crates/benchmark-suite/benches/boon.rs new file mode 100644 index 00000000..90ce3d5c --- /dev/null +++ b/crates/benchmark-suite/benches/boon.rs @@ -0,0 +1,50 @@ +use benchmark::Benchmark; +use boon::{Compiler, Schemas}; +use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; +use serde_json::Value; + +fn bench_compile(c: &mut Criterion, name: &str, schema: &Value) { + let mut compiler = Compiler::new(); + compiler + .add_resource("schema.json", schema.clone()) + .expect("Failed to add resource"); + c.bench_function(&format!("boon/{}/compile", name), |b| { + b.iter(|| { + compiler + .compile("schema.json", &mut Schemas::new()) + .expect("Failed to compiled"); + }) + }); +} + +fn bench_validate(c: &mut Criterion, name: &str, schema: &Value, instance: &Value) { + let mut schemas = Schemas::new(); + let mut compiler = Compiler::new(); + compiler + .add_resource("schema.json", schema.clone()) + .expect("Failed to add resource"); + let id = compiler + .compile("schema.json", &mut schemas) + .expect("Failed to compile"); + + c.bench_with_input( + BenchmarkId::new(name, "validate"), + instance, + |b, instance| b.iter(|| schemas.validate(instance, id).is_ok()), + ); +} + +fn run_benchmarks(c: &mut Criterion) { + for benchmark in Benchmark::iter() { + benchmark.run(&mut |name, schema, instances| { + bench_compile(c, name, schema); + for instance in instances { + let name = format!("boon/{}/{}", name, instance.name); + bench_validate(c, &name, schema, &instance.data); + } + }); + } +} + +criterion_group!(boon, run_benchmarks); +criterion_main!(boon); diff --git a/crates/benchmark-suite/benches/jsonschema.rs b/crates/benchmark-suite/benches/jsonschema.rs new file mode 100644 index 00000000..6778efba --- /dev/null +++ b/crates/benchmark-suite/benches/jsonschema.rs @@ -0,0 +1,52 @@ +use benchmark::Benchmark; +use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; +use jsonschema::JSONSchema; +use serde_json::Value; + +fn bench_compile(c: &mut Criterion, name: &str, schema: &Value) { + c.bench_function(&format!("jsonschema/{}/compile", name), |b| { + b.iter(|| JSONSchema::compile(schema).expect("Valid schema")) + }); +} + +fn bench_is_valid(c: &mut Criterion, name: &str, schema: &Value, instance: &Value) { + let compiled = JSONSchema::compile(schema).expect("Valid schema"); + c.bench_with_input( + BenchmarkId::new(name, "is_valid"), + instance, + |b, instance| { + b.iter(|| { + let _ = compiled.is_valid(instance); + }) + }, + ); +} + +fn bench_validate(c: &mut Criterion, name: &str, schema: &Value, instance: &Value) { + let compiled = JSONSchema::compile(schema).expect("Valid schema"); + c.bench_with_input( + BenchmarkId::new(name, "validate"), + instance, + |b, instance| { + b.iter(|| { + let _ = compiled.validate(instance); + }) + }, + ); +} + +fn run_benchmarks(c: &mut Criterion) { + for benchmark in Benchmark::iter() { + benchmark.run(&mut |name, schema, instances| { + bench_compile(c, name, schema); + for instance in instances { + let name = format!("jsonschema/{}/{}", name, instance.name); + bench_is_valid(c, &name, schema, &instance.data); + bench_validate(c, &name, schema, &instance.data); + } + }); + } +} + +criterion_group!(jsonschema, run_benchmarks); +criterion_main!(jsonschema); diff --git a/crates/benchmark-suite/benches/jsonschema_valid.rs b/crates/benchmark-suite/benches/jsonschema_valid.rs new file mode 100644 index 00000000..414dff01 --- /dev/null +++ b/crates/benchmark-suite/benches/jsonschema_valid.rs @@ -0,0 +1,42 @@ +use benchmark::Benchmark; +use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; +use jsonschema_valid::{schemas, Config}; +use serde_json::Value; + +fn bench_compile(c: &mut Criterion, name: &str, schema: &Value) { + c.bench_function(&format!("jsonschema_valid/{}/compile", name), |b| { + b.iter(|| Config::from_schema(schema, Some(schemas::Draft::Draft7)).expect("Valid schema")) + }); +} + +fn bench_validate(c: &mut Criterion, name: &str, schema: &Value, instance: &Value) { + let cfg = Config::from_schema(schema, Some(schemas::Draft::Draft7)).expect("Valid schema"); + c.bench_with_input( + BenchmarkId::new(name, "validate"), + instance, + |b, instance| { + b.iter(|| { + let _ = jsonschema_valid::validate(&cfg, instance); + }) + }, + ); +} + +static UNSUPPORTED_BENCHMARKS: &[&str] = &["Open API", "Swagger"]; + +fn run_benchmarks(c: &mut Criterion) { + for benchmark in Benchmark::iter() { + benchmark.run(&mut |name, schema, instances| { + if !UNSUPPORTED_BENCHMARKS.contains(&name) { + bench_compile(c, name, schema); + for instance in instances { + let name = format!("jsonschema_valid/{}/{}", name, instance.name); + bench_validate(c, &name, schema, &instance.data); + } + } + }); + } +} + +criterion_group!(jsonschema_valid, run_benchmarks); +criterion_main!(jsonschema_valid); diff --git a/crates/benchmark-suite/benches/valico.rs b/crates/benchmark-suite/benches/valico.rs new file mode 100644 index 00000000..f191048e --- /dev/null +++ b/crates/benchmark-suite/benches/valico.rs @@ -0,0 +1,50 @@ +use benchmark::Benchmark; +use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; +use serde_json::Value; +use valico::json_schema; + +fn bench_compile(c: &mut Criterion, name: &str, schema: &Value) { + c.bench_function(&format!("valico/{}/compile", name), |b| { + b.iter(|| { + let mut scope = json_schema::Scope::new(); + scope + .compile_and_return(schema.clone(), false) + .expect("Valid schema"); + }) + }); +} + +fn bench_validate(c: &mut Criterion, name: &str, schema: &Value, instance: &Value) { + let mut scope = json_schema::Scope::new(); + let compiled = scope + .compile_and_return(schema.clone(), false) + .expect("Valid schema"); + c.bench_with_input( + BenchmarkId::new(name, "validate"), + instance, + |b, instance| { + b.iter(|| { + compiled.validate(instance).is_valid(); + }) + }, + ); +} + +static UNSUPPORTED_BENCHMARKS: &[&str] = &["Open API"]; + +fn run_benchmarks(c: &mut Criterion) { + for benchmark in Benchmark::iter() { + benchmark.run(&mut |name, schema, instances| { + if !UNSUPPORTED_BENCHMARKS.contains(&name) { + bench_compile(c, name, schema); + for instance in instances { + let name = format!("valico/{}/{}", name, instance.name); + bench_validate(c, &name, schema, &instance.data); + } + } + }); + } +} + +criterion_group!(valico, run_benchmarks); +criterion_main!(valico); diff --git a/crates/benchmark-suite/src/lib.rs b/crates/benchmark-suite/src/lib.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/crates/benchmark-suite/src/lib.rs @@ -0,0 +1 @@ + diff --git a/crates/benchmark/Cargo.toml b/crates/benchmark/Cargo.toml new file mode 100644 index 00000000..d10ac895 --- /dev/null +++ b/crates/benchmark/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "benchmark" +version = "0.1.0" +rust-version = "1.80" +readme = "README.md" +license.workspace = true +repository.workspace = true +edition.workspace = true +authors.workspace = true +publish = false + +[dependencies] +serde = { version = "1", features = ["derive"] } +serde_json.workspace = true +criterion = { version = "0.5.1", default-features = false } +strum = { version = "0.26.3", features = ["derive"] } diff --git a/crates/benchmark/README.md b/crates/benchmark/README.md new file mode 100644 index 00000000..ab80eb1f --- /dev/null +++ b/crates/benchmark/README.md @@ -0,0 +1,19 @@ +# benchmark + +A helper crate for running JSON Schema validation benchmarks across multiple libraries. + +## Features + +- Predefined set of JSON schemas and instances for benchmarking +- Easy-to-use API for running benchmarks uniformly across different libraries + +## Usage + +```rust +use benchmark::Benchmark; + +for benchmark in Benchmark::iter() { + benchmark.run(&mut |schema_name, instance_name, schema, instance| { + // Your benchmarking code here + }); +} diff --git a/jsonschema/benches/data/canada.json b/crates/benchmark/data/canada.json similarity index 100% rename from jsonschema/benches/data/canada.json rename to crates/benchmark/data/canada.json diff --git a/jsonschema/benches/data/citm_catalog.json b/crates/benchmark/data/citm_catalog.json similarity index 100% rename from jsonschema/benches/data/citm_catalog.json rename to crates/benchmark/data/citm_catalog.json diff --git a/jsonschema/benches/data/citm_catalog_schema.json b/crates/benchmark/data/citm_catalog_schema.json similarity index 100% rename from jsonschema/benches/data/citm_catalog_schema.json rename to crates/benchmark/data/citm_catalog_schema.json diff --git a/jsonschema/benches/data/fast_invalid.json b/crates/benchmark/data/fast_invalid.json similarity index 100% rename from jsonschema/benches/data/fast_invalid.json rename to crates/benchmark/data/fast_invalid.json diff --git a/jsonschema/benches/data/fast_schema.json b/crates/benchmark/data/fast_schema.json similarity index 100% rename from jsonschema/benches/data/fast_schema.json rename to crates/benchmark/data/fast_schema.json diff --git a/jsonschema/benches/data/fast_valid.json b/crates/benchmark/data/fast_valid.json similarity index 100% rename from jsonschema/benches/data/fast_valid.json rename to crates/benchmark/data/fast_valid.json diff --git a/jsonschema/benches/data/geojson.json b/crates/benchmark/data/geojson.json similarity index 100% rename from jsonschema/benches/data/geojson.json rename to crates/benchmark/data/geojson.json diff --git a/jsonschema/benches/data/keywords.json b/crates/benchmark/data/keywords.json similarity index 100% rename from jsonschema/benches/data/keywords.json rename to crates/benchmark/data/keywords.json diff --git a/jsonschema/benches/data/kubernetes.json b/crates/benchmark/data/kubernetes.json similarity index 100% rename from jsonschema/benches/data/kubernetes.json rename to crates/benchmark/data/kubernetes.json diff --git a/jsonschema/benches/data/openapi.json b/crates/benchmark/data/openapi.json similarity index 100% rename from jsonschema/benches/data/openapi.json rename to crates/benchmark/data/openapi.json diff --git a/jsonschema/benches/data/swagger.json b/crates/benchmark/data/swagger.json similarity index 100% rename from jsonschema/benches/data/swagger.json rename to crates/benchmark/data/swagger.json diff --git a/jsonschema/benches/data/zuora.json b/crates/benchmark/data/zuora.json similarity index 100% rename from jsonschema/benches/data/zuora.json rename to crates/benchmark/data/zuora.json diff --git a/crates/benchmark/src/lib.rs b/crates/benchmark/src/lib.rs new file mode 100644 index 00000000..fc0eeef5 --- /dev/null +++ b/crates/benchmark/src/lib.rs @@ -0,0 +1,147 @@ +use serde_json::Value; +use std::sync::LazyLock; +use strum::{EnumIter, IntoEnumIterator}; + +static OPEN_API: &[u8] = include_bytes!("../data/openapi.json"); +static SWAGGER: &[u8] = include_bytes!("../data/swagger.json"); +static GEOJSON: &[u8] = include_bytes!("../data/geojson.json"); +static CITM_SCHEMA: &[u8] = include_bytes!("../data/citm_catalog_schema.json"); +static FAST_SCHEMA: &[u8] = include_bytes!("../data/fast_schema.json"); + +static ZUORA: &[u8] = include_bytes!("../data/zuora.json"); +static KUBERNETES: &[u8] = include_bytes!("../data/kubernetes.json"); +static CANADA: &[u8] = include_bytes!("../data/canada.json"); +static CITM: &[u8] = include_bytes!("../data/citm_catalog.json"); +static FAST_VALID: &[u8] = include_bytes!("../data/fast_valid.json"); +static FAST_INVALID: &[u8] = include_bytes!("../data/fast_invalid.json"); + +pub fn read_json(slice: &[u8]) -> Value { + serde_json::from_slice(slice).expect("Invalid JSON") +} + +#[derive(Debug, Clone, Copy, EnumIter)] +pub enum Benchmark { + OpenAPI, + Swagger, + GeoJSON, + CITM, + Fast, +} + +type BenchFunc<'a> = dyn FnMut(&str, &Value, &[BenchInstance]) + 'a; + +impl Benchmark { + pub fn iter() -> impl Iterator { + ::iter() + } + pub fn run(self, bench: &mut BenchFunc) { + BENCHMARK_SUITE.run(self, bench) + } +} + +struct BenchData { + name: &'static str, + schema: Value, + instances: Vec, +} + +#[derive(Debug)] +pub struct BenchInstance { + pub name: String, + pub data: Value, +} + +pub struct BenchmarkSuite { + benchmarks: [LazyLock; 5], +} + +impl BenchmarkSuite { + fn new() -> Self { + Self { + benchmarks: [ + LazyLock::new(|| BenchData { + name: "Open API", + schema: read_json(OPEN_API), + instances: vec![BenchInstance { + name: "Zuora".to_string(), + data: read_json(ZUORA), + }], + }), + LazyLock::new(|| BenchData { + name: "Swagger", + schema: read_json(SWAGGER), + instances: vec![BenchInstance { + name: "Kubernetes".to_string(), + data: read_json(KUBERNETES), + }], + }), + LazyLock::new(|| BenchData { + name: "GeoJSON", + schema: read_json(GEOJSON), + instances: vec![BenchInstance { + name: "Canada".to_string(), + data: read_json(CANADA), + }], + }), + LazyLock::new(|| BenchData { + name: "CITM", + schema: read_json(CITM_SCHEMA), + instances: vec![BenchInstance { + name: "Catalog".to_string(), + data: read_json(CITM), + }], + }), + LazyLock::new(|| BenchData { + name: "Fast", + schema: read_json(FAST_SCHEMA), + instances: vec![ + BenchInstance { + name: "Valid".to_string(), + data: read_json(FAST_VALID), + }, + BenchInstance { + name: "Invalid".to_string(), + data: read_json(FAST_INVALID), + }, + ], + }), + ], + } + } + + fn run(&self, bench_type: Benchmark, bench: &mut BenchFunc) { + let index = bench_type as usize; + let data = &self.benchmarks[index]; + bench(data.name, &data.schema, &data.instances); + } +} + +static BENCHMARK_SUITE: LazyLock = LazyLock::new(BenchmarkSuite::new); + +#[derive(serde::Deserialize)] +pub struct KeywordBenchmark { + pub name: String, + pub schema: Value, + pub valid: Vec, + pub invalid: Vec, +} + +static KEYWORDS: &[u8] = include_bytes!("../data/keywords.json"); +static KEYWORD_BENCHMARKS: LazyLock> = + LazyLock::new(|| serde_json::from_slice(KEYWORDS).expect("Invalid JSON")); + +pub fn run_keyword_benchmarks(bench: &mut BenchFunc) { + for kb in KEYWORD_BENCHMARKS.iter() { + for (prefix, values) in [("valid", &kb.valid), ("invalid", &kb.invalid)] { + let instances: Vec<_> = values + .iter() + .enumerate() + .map(|(idx, instance)| BenchInstance { + name: format!("{}/{}", prefix, idx), + data: instance.clone(), + }) + .collect(); + bench(&kb.name, &kb.schema, &instances); + } + } +} diff --git a/crates/jsonschema-cli/Cargo.toml b/crates/jsonschema-cli/Cargo.toml new file mode 100644 index 00000000..1b7a34d8 --- /dev/null +++ b/crates/jsonschema-cli/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "jsonschema-cli" +version = "0.18.3" +description = "A command line tool for JSON Schema validation." +keywords = ["jsonschema", "validation"] +categories = ["web-programming"] +readme = "README.md" +rust-version.workspace = true +edition.workspace = true +authors.workspace = true +repository.workspace = true +license.workspace = true + +[dependencies] +clap = { version = "4.5", features = ["derive"] } +jsonschema = { path = "../jsonschema/" } +serde_json.workspace = true + +[[bin]] +name = "jsonschema-cli" +path = "src/main.rs" + +[lints] +workspace = true diff --git a/crates/jsonschema-cli/README.md b/crates/jsonschema-cli/README.md new file mode 100644 index 00000000..9ea62ba0 --- /dev/null +++ b/crates/jsonschema-cli/README.md @@ -0,0 +1,63 @@ +# jsonschema-cli + +A fast and user-friendly command-line tool for JSON Schema validation, powered by the high-performance `jsonschema` Rust crate. + +## Installation + +``` +cargo install jsonschema-cli +``` + +## Usage + +``` +jsonschema [OPTIONS] +``` + +### Options: + +- `-i, --instance `: JSON instance(s) to validate (can be used multiple times) +- `-v, --version`: Show version information +- `--help`: Display help information + +### Examples: + +Validate a single instance: +``` +jsonschema schema.json -i instance.json +``` + +Validate multiple instances: +``` +jsonschema schema.json -i instance1.json -i instance2.json +``` + +## Features + +- Validate one or more JSON instances against a single schema +- Clear, concise output with detailed error reporting +- Fast validation using the `jsonschema` Rust crate + +## Output + +For each instance, the tool will output: + +- ` - VALID` if the instance is valid +- ` - INVALID` followed by a list of errors if invalid + +Example output: +``` +instance1.json - VALID +instance2.json - INVALID. Errors: +1. "name" is a required property +2. "age" must be a number +``` + +## Exit Codes + +- 0: All instances are valid (or no instances provided) +- 1: One or more instances are invalid, or there was an error + +## License + +This project is licensed under the MIT License. diff --git a/crates/jsonschema-cli/src/main.rs b/crates/jsonschema-cli/src/main.rs new file mode 100644 index 00000000..ed5a9507 --- /dev/null +++ b/crates/jsonschema-cli/src/main.rs @@ -0,0 +1,91 @@ +#![allow(clippy::print_stdout)] +use std::{ + fs::File, + io::BufReader, + path::{Path, PathBuf}, + process::ExitCode, +}; + +use clap::Parser; +use jsonschema::JSONSchema; + +#[derive(Parser)] +#[command(name = "jsonschema")] +struct Cli { + /// A path to a JSON instance (i.e. filename.json) to validate (may be specified multiple times). + #[arg(short = 'i', long = "instance")] + instances: Option>, + + /// The JSON Schema to validate with (i.e. schema.json). + #[arg(value_parser, required_unless_present("version"))] + schema: Option, + + /// Show program's version number and exit. + #[arg(short = 'v', long = "version")] + version: bool, +} + +fn read_json( + path: &Path, +) -> Result, Box> { + let file = File::open(path)?; + let reader = BufReader::new(file); + Ok(serde_json::from_reader(reader)) +} + +fn validate_instances( + instances: &[PathBuf], + schema_path: &Path, +) -> Result> { + let mut success = true; + + let schema_json = read_json(schema_path)??; + match JSONSchema::compile(&schema_json) { + Ok(schema) => { + for instance in instances { + let instance_json = read_json(instance)??; + let validation = schema.validate(&instance_json); + let filename = instance.to_string_lossy(); + match validation { + Ok(()) => println!("{filename} - VALID"), + Err(errors) => { + success = false; + + println!("{filename} - INVALID. Errors:"); + for (i, e) in errors.enumerate() { + println!("{}. {}", i + 1, e); + } + } + } + } + } + Err(error) => { + println!("Schema is invalid. Error: {error}"); + success = false; + } + } + Ok(success) +} + +fn main() -> ExitCode { + let config = Cli::parse(); + + if config.version { + println!(concat!("Version: ", env!("CARGO_PKG_VERSION"))); + return ExitCode::SUCCESS; + } + + if let Some(schema) = config.schema { + if let Some(instances) = config.instances { + return match validate_instances(&instances, &schema) { + Ok(true) => ExitCode::SUCCESS, + Ok(false) => ExitCode::FAILURE, + Err(error) => { + println!("Error: {error}"); + ExitCode::FAILURE + } + }; + } + } + ExitCode::SUCCESS +} diff --git a/bindings/python/.gitignore b/crates/jsonschema-py/.gitignore similarity index 100% rename from bindings/python/.gitignore rename to crates/jsonschema-py/.gitignore diff --git a/bindings/python/CHANGELOG.md b/crates/jsonschema-py/CHANGELOG.md similarity index 99% rename from bindings/python/CHANGELOG.md rename to crates/jsonschema-py/CHANGELOG.md index 462ea695..20c31c75 100644 --- a/bindings/python/CHANGELOG.md +++ b/crates/jsonschema-py/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Removed + +- Support for Python 3.7 + ## [0.18.3] - 2024-09-12 ### Fixed diff --git a/bindings/python/Cargo.toml b/crates/jsonschema-py/Cargo.toml similarity index 53% rename from bindings/python/Cargo.toml rename to crates/jsonschema-py/Cargo.toml index 7b8b0b53..61478e09 100644 --- a/bindings/python/Cargo.toml +++ b/crates/jsonschema-py/Cargo.toml @@ -1,15 +1,15 @@ [package] -name = "jsonschema-python" +name = "jsonschema-py" version = "0.18.3" -authors = ["Dmitry Dygalo "] -edition = "2021" -license = "MIT" -readme = "README.md" description = "JSON schema validaton library" -repository = "https://github.com/Stranger6667/jsonschema-rs" keywords = ["jsonschema", "validation"] categories = ["web-programming"] -rust-version = "1.56.1" +readme = "README.md" +license.workspace = true +repository.workspace = true +rust-version.workspace = true +edition.workspace = true +authors.workspace = true [lib] name = "jsonschema_rs" @@ -19,18 +19,9 @@ crate-type = ["cdylib"] built = { version = "0.7.1", features = ["cargo-lock", "chrono"] } pyo3-build-config = { version = "0.22.2", features = ["resolve-config"] } -[dependencies.jsonschema] -path = "../../jsonschema" -version = "*" -default-features = false -features = ["resolve-http", "resolve-file", "draft201909", "draft202012"] - [dependencies] -serde_json = "1.0.91" +jsonschema = { path = "../jsonschema/" } +serde_json.workspace = true serde = "1.0.152" pyo3 = { version = "0.22.2", features = ["extension-module"] } pyo3-built = "0.5" - -[profile.release] -codegen-units = 1 -lto = "fat" diff --git a/bindings/python/README.md b/crates/jsonschema-py/README.md similarity index 99% rename from bindings/python/README.md rename to crates/jsonschema-py/README.md index 15d9dcdc..826de34e 100644 --- a/bindings/python/README.md +++ b/crates/jsonschema-py/README.md @@ -149,7 +149,7 @@ Measured with stable Rust 1.56, CPython 3.9.7 / PyPy3 7.3.6 on Intel i8700K ## Python support -`jsonschema-rs` supports CPython 3.7, 3.8, 3.9, 3.10, 3.11, and 3.12. +`jsonschema-rs` supports CPython 3.8, 3.9, 3.10, 3.11, and 3.12. ## License diff --git a/bindings/python/benches/bench.py b/crates/jsonschema-py/benches/bench.py similarity index 98% rename from bindings/python/benches/bench.py rename to crates/jsonschema-py/benches/bench.py index ab4cbda6..3aebb735 100644 --- a/bindings/python/benches/bench.py +++ b/crates/jsonschema-py/benches/bench.py @@ -24,7 +24,7 @@ def load_json_str(filename): def load_from_benches(filename, loader=load_json): - return loader(f"../../jsonschema/benches/data/{filename}") + return loader(f"../benchmark/data/{filename}") OPENAPI = load_from_benches("openapi.json") diff --git a/bindings/python/benches/conftest.py b/crates/jsonschema-py/benches/conftest.py similarity index 100% rename from bindings/python/benches/conftest.py rename to crates/jsonschema-py/benches/conftest.py diff --git a/bindings/python/build.rs b/crates/jsonschema-py/build.rs similarity index 100% rename from bindings/python/build.rs rename to crates/jsonschema-py/build.rs diff --git a/bindings/python/pyproject.toml b/crates/jsonschema-py/pyproject.toml similarity index 58% rename from bindings/python/pyproject.toml rename to crates/jsonschema-py/pyproject.toml index 1eefbb58..55dc7f1a 100644 --- a/bindings/python/pyproject.toml +++ b/crates/jsonschema-py/pyproject.toml @@ -1,3 +1,7 @@ +[build-system] +requires = ["maturin>=1.0,<2.0"] +build-backend = "maturin" + [project] name = "jsonschema_rs" description = "Fast JSON Schema validation for Python implemented in Rust" @@ -17,7 +21,6 @@ classifiers = [ "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", @@ -26,19 +29,59 @@ classifiers = [ "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Rust", ] +requires-python = ">=3.8" dependencies = [] -requires-python = ">=3.7" + +[project.optional-dependencies] +tests = [ + "flask>=2.2.5", + "hypothesis>=6.79.4", + "pytest>=7.4.4", +] +bench = [ + "pytest-benchmark>=4.0.0", +] [project.urls] -Homepage = "https://github.com/Stranger6667/jsonschema-rs/tree/master/python" -Changelog = "https://github.com/Stranger6667/jsonschema-rs/blob/master/bindings/python/CHANGELOG.md" +Homepage = "https://github.com/Stranger6667/jsonschema-rs/tree/master/crates/jsonschema-py" +Changelog = "https://github.com/Stranger6667/jsonschema-rs/blob/master/crates/jsonschema-py/CHANGELOG.md" "Bug Tracker" = "https://github.com/Stranger6667/jsonschema-rs/issues" Source = "https://github.com/Stranger6667/jsonschema-rs" Funding = 'https://github.com/sponsors/Stranger6667' +[tool.maturin] +python-source = "python" +strip = true +include = [{ path = "rust-toolchain.toml", format = ["sdist", "wheel"] }] + [tool.ruff] line-length = 120 -target-version = "py37" +target-version = "py38" + +[tool.ruff.lint] +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes + "C", # flake8-comprehensions + "B", # flake8-bugbear + "D", # pydocstyle +] +ignore = [ + "E501", # Line too long + "B008", # Do not perform function calls in argument defaults + "C901", # Too complex + "D100", # Missing docstring in public module + "D101", # Missing docstring in public class + "D102", # Missing docstring in public method + "D103", # Missing docstring in public function + "D104", # Missing docstring in public package + "D105", # Missing docstring in magic method + "D107", # Missing docstring in `__init__` + "D203", # One blank line before class + "D213", # Multiline summary second line + "D401", # Imperative mood +] [tool.ruff.lint.isort] known-first-party = ["jsonschema_rs"] @@ -46,11 +89,3 @@ known-third-party = ["hypothesis", "pytest"] [tool.ruff.format] skip-magic-trailing-comma = false - -[tool.maturin] -python-source = "python" -strip = true - -[build-system] -requires = ["maturin>=1.1"] -build-backend = "maturin" diff --git a/bindings/python/python/jsonschema_rs/__init__.py b/crates/jsonschema-py/python/jsonschema_rs/__init__.py similarity index 100% rename from bindings/python/python/jsonschema_rs/__init__.py rename to crates/jsonschema-py/python/jsonschema_rs/__init__.py diff --git a/bindings/python/python/jsonschema_rs/__init__.pyi b/crates/jsonschema-py/python/jsonschema_rs/__init__.pyi similarity index 100% rename from bindings/python/python/jsonschema_rs/__init__.pyi rename to crates/jsonschema-py/python/jsonschema_rs/__init__.pyi diff --git a/bindings/python/python/jsonschema_rs/py.typed b/crates/jsonschema-py/python/jsonschema_rs/py.typed similarity index 100% rename from bindings/python/python/jsonschema_rs/py.typed rename to crates/jsonschema-py/python/jsonschema_rs/py.typed diff --git a/crates/jsonschema-py/rust-toolchain.toml b/crates/jsonschema-py/rust-toolchain.toml new file mode 100644 index 00000000..8106042e --- /dev/null +++ b/crates/jsonschema-py/rust-toolchain.toml @@ -0,0 +1,4 @@ +[toolchain] +channel = "1.81" + + diff --git a/bindings/python/src/ffi.rs b/crates/jsonschema-py/src/ffi.rs similarity index 100% rename from bindings/python/src/ffi.rs rename to crates/jsonschema-py/src/ffi.rs diff --git a/bindings/python/src/lib.rs b/crates/jsonschema-py/src/lib.rs similarity index 97% rename from bindings/python/src/lib.rs rename to crates/jsonschema-py/src/lib.rs index 5e3bed43..89c0d606 100644 --- a/bindings/python/src/lib.rs +++ b/crates/jsonschema-py/src/lib.rs @@ -1,19 +1,3 @@ -#![warn( - clippy::doc_markdown, - clippy::redundant_closure, - clippy::explicit_iter_loop, - clippy::match_same_arms, - clippy::needless_borrow, - clippy::print_stdout, - clippy::arithmetic_side_effects, - clippy::cast_possible_truncation, - clippy::map_unwrap_or, - clippy::unseparated_literal_suffix, - clippy::cargo, - clippy::unwrap_used, - rust_2018_compatibility, - rust_2018_idioms -)] #![allow(clippy::upper_case_acronyms)] use std::{ diff --git a/bindings/python/src/ser.rs b/crates/jsonschema-py/src/ser.rs similarity index 100% rename from bindings/python/src/ser.rs rename to crates/jsonschema-py/src/ser.rs diff --git a/bindings/python/src/types.rs b/crates/jsonschema-py/src/types.rs similarity index 100% rename from bindings/python/src/types.rs rename to crates/jsonschema-py/src/types.rs diff --git a/bindings/python/tests-py/test_jsonschema.py b/crates/jsonschema-py/tests-py/test_jsonschema.py similarity index 100% rename from bindings/python/tests-py/test_jsonschema.py rename to crates/jsonschema-py/tests-py/test_jsonschema.py diff --git a/bindings/python/tests-py/test_suite.py b/crates/jsonschema-py/tests-py/test_suite.py similarity index 86% rename from bindings/python/tests-py/test_suite.py rename to crates/jsonschema-py/tests-py/test_suite.py index e49310e5..00706cd5 100644 --- a/bindings/python/tests-py/test_suite.py +++ b/crates/jsonschema-py/tests-py/test_suite.py @@ -11,7 +11,7 @@ import jsonschema_rs -TEST_SUITE_PATH = "../../jsonschema/tests/suite" +TEST_SUITE_PATH = "../jsonschema/tests/suite" EXPONENTIAL_BASE = 2 JITTER = (0.0, 0.5) INITIAL_RETRY_DELAY = 0.05 @@ -51,9 +51,17 @@ def mock_server(): SUPPORTED_DRAFTS = (4, 6, 7) NOT_SUPPORTED_CASES = { - 4: ("bignum.json", "email.json"), - 6: ("bignum.json", "email.json"), - 7: ("bignum.json", "email.json", "idn-hostname.json"), + 4: ("bignum.json", "email.json", "ecmascript-regex.json", "refRemote.json"), + 6: ("bignum.json", "email.json", "ecmascript-regex.json", "refRemote.json"), + 7: ( + "bignum.json", + "email.json", + "idn-hostname.json", + "time.json", + "ecmascript-regex.json", + "refRemote.json", + "cross-draft.json", + ), } diff --git a/crates/jsonschema-py/tox.ini b/crates/jsonschema-py/tox.ini new file mode 100644 index 00000000..f493f714 --- /dev/null +++ b/crates/jsonschema-py/tox.ini @@ -0,0 +1,16 @@ +[tox] +isolated_build = true +envlist = py{38,39,310,311,312} + +[gh-actions] +python = + 3.8: py38 + 3.9: py39 + 3.10: py310 + 3.11: py311 + 3.12: py312 + +[testenv] +extras = tests +commands = + python -m pytest tests-py {posargs:} diff --git a/crates/jsonschema-py/uv.lock b/crates/jsonschema-py/uv.lock new file mode 100644 index 00000000..2d449819 --- /dev/null +++ b/crates/jsonschema-py/uv.lock @@ -0,0 +1,290 @@ +version = 1 +requires-python = ">=3.8" + +[[package]] +name = "attrs" +version = "24.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/0f/aafca9af9315aee06a89ffde799a10a582fe8de76c563ee80bbcdc08b3fb/attrs-24.2.0.tar.gz", hash = "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346", size = 792678 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/21/5b6702a7f963e95456c0de2d495f67bf5fd62840ac655dc451586d23d39a/attrs-24.2.0-py3-none-any.whl", hash = "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2", size = 63001 }, +] + +[[package]] +name = "click" +version = "8.1.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "platform_system == 'Windows'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/d3/f04c7bfcf5c1862a2a5b845c6b2b360488cf47af55dfa79c98f6a6bf98b5/click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de", size = 336121 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", size = 97941 }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "exceptiongroup" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453 }, +] + +[[package]] +name = "flask" +version = "2.2.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, + { name = "itsdangerous" }, + { name = "jinja2" }, + { name = "werkzeug" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5f/76/a4d2c4436dda4b0a12c71e075c508ea7988a1066b06a575f6afe4fecc023/Flask-2.2.5.tar.gz", hash = "sha256:edee9b0a7ff26621bd5a8c10ff484ae28737a2410d99b0bb9a6850c7fb977aa0", size = 697814 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/1a/8b6d48162861009d1e017a9740431c78d860809773b66cac220a11aa3310/Flask-2.2.5-py3-none-any.whl", hash = "sha256:58107ed83443e86067e41eff4631b058178191a355886f8e479e347fa1285fdf", size = 101817 }, +] + +[[package]] +name = "hypothesis" +version = "6.79.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "sortedcontainers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ef/f6/7e45292d294dc5a093335361f9b5f2783775b20ac2569e5f0bdd75abf8c7/hypothesis-6.79.4.tar.gz", hash = "sha256:e9a9ff3dc3f3eebbf214d6852882ac96ad72023f0e9770139fd3d3c1b87673e2", size = 355439 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/9e/fb74dda9518f58f7efb14d4381fc545c190f377bb78712825b40ee600905/hypothesis-6.79.4-py3-none-any.whl", hash = "sha256:5ce05bc70aa4f20114effaf3375dc8b51d09a04026a0cf89d4514fc0b69f6304", size = 417669 }, +] + +[[package]] +name = "importlib-metadata" +version = "6.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/82/f6e29c8d5c098b6be61460371c2c5591f4a335923639edec43b3830650a4/importlib_metadata-6.7.0.tar.gz", hash = "sha256:1aaf550d4f73e5d6783e7acb77aec43d49da8017410afae93822cc9cca98c4d4", size = 53569 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/94/64287b38c7de4c90683630338cf28f129decbba0a44f0c6db35a873c73c4/importlib_metadata-6.7.0-py3-none-any.whl", hash = "sha256:cb52082e659e97afc5dac71e79de97d8681de3aa07ff18578330904a9d18e5b5", size = 22934 }, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, +] + +[[package]] +name = "itsdangerous" +version = "2.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7f/a1/d3fb83e7a61fa0c0d3d08ad0a94ddbeff3731c05212617dff3a94e097f08/itsdangerous-2.1.2.tar.gz", hash = "sha256:5dbbc68b317e5e42f327f9021763545dc3fc3bfe22e6deb96aaf1fc38874156a", size = 56143 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/5f/447e04e828f47465eeab35b5d408b7ebaaaee207f48b7136c5a7267a30ae/itsdangerous-2.1.2-py3-none-any.whl", hash = "sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44", size = 15749 }, +] + +[[package]] +name = "jinja2" +version = "3.1.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ed/55/39036716d19cab0747a5020fc7e907f362fbf48c984b14e62127f7e68e5d/jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369", size = 240245 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/80/3a54838c3fb461f6fec263ebf3a3a41771bd05190238de3486aae8540c36/jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d", size = 133271 }, +] + +[[package]] +name = "jsonschema-rs" +version = "0.18.3" +source = { editable = "." } + +[package.optional-dependencies] +bench = [ + { name = "pytest-benchmark" }, +] +tests = [ + { name = "flask" }, + { name = "hypothesis" }, + { name = "pytest" }, +] + +[package.metadata] +requires-dist = [ + { name = "flask", marker = "extra == 'tests'", specifier = ">=2.2.5" }, + { name = "hypothesis", marker = "extra == 'tests'", specifier = ">=6.79.4" }, + { name = "pytest", marker = "extra == 'tests'", specifier = ">=7.4.4" }, + { name = "pytest-benchmark", marker = "extra == 'bench'", specifier = ">=4.0.0" }, +] + +[[package]] +name = "markupsafe" +version = "2.1.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/87/5b/aae44c6655f3801e81aa3eef09dbbf012431987ba564d7231722f68df02d/MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b", size = 19384 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/54/ad5eb37bf9d51800010a74e4665425831a9db4e7c4e0fde4352e391e808e/MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc", size = 18206 }, + { url = "https://files.pythonhosted.org/packages/6a/4a/a4d49415e600bacae038c67f9fecc1d5433b9d3c71a4de6f33537b89654c/MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5", size = 14079 }, + { url = "https://files.pythonhosted.org/packages/0a/7b/85681ae3c33c385b10ac0f8dd025c30af83c78cec1c37a6aa3b55e67f5ec/MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46", size = 26620 }, + { url = "https://files.pythonhosted.org/packages/7c/52/2b1b570f6b8b803cef5ac28fdf78c0da318916c7d2fe9402a84d591b394c/MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f", size = 25818 }, + { url = "https://files.pythonhosted.org/packages/29/fe/a36ba8c7ca55621620b2d7c585313efd10729e63ef81e4e61f52330da781/MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900", size = 25493 }, + { url = "https://files.pythonhosted.org/packages/60/ae/9c60231cdfda003434e8bd27282b1f4e197ad5a710c14bee8bea8a9ca4f0/MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff", size = 30630 }, + { url = "https://files.pythonhosted.org/packages/65/dc/1510be4d179869f5dafe071aecb3f1f41b45d37c02329dfba01ff59e5ac5/MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad", size = 29745 }, + { url = "https://files.pythonhosted.org/packages/30/39/8d845dd7d0b0613d86e0ef89549bfb5f61ed781f59af45fc96496e897f3a/MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd", size = 30021 }, + { url = "https://files.pythonhosted.org/packages/c7/5c/356a6f62e4f3c5fbf2602b4771376af22a3b16efa74eb8716fb4e328e01e/MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4", size = 16659 }, + { url = "https://files.pythonhosted.org/packages/69/48/acbf292615c65f0604a0c6fc402ce6d8c991276e16c80c46a8f758fbd30c/MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5", size = 17213 }, + { url = "https://files.pythonhosted.org/packages/11/e7/291e55127bb2ae67c64d66cef01432b5933859dfb7d6949daa721b89d0b3/MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f", size = 18219 }, + { url = "https://files.pythonhosted.org/packages/6b/cb/aed7a284c00dfa7c0682d14df85ad4955a350a21d2e3b06d8240497359bf/MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2", size = 14098 }, + { url = "https://files.pythonhosted.org/packages/1c/cf/35fe557e53709e93feb65575c93927942087e9b97213eabc3fe9d5b25a55/MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced", size = 29014 }, + { url = "https://files.pythonhosted.org/packages/97/18/c30da5e7a0e7f4603abfc6780574131221d9148f323752c2755d48abad30/MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5", size = 28220 }, + { url = "https://files.pythonhosted.org/packages/0c/40/2e73e7d532d030b1e41180807a80d564eda53babaf04d65e15c1cf897e40/MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c", size = 27756 }, + { url = "https://files.pythonhosted.org/packages/18/46/5dca760547e8c59c5311b332f70605d24c99d1303dd9a6e1fc3ed0d73561/MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f", size = 33988 }, + { url = "https://files.pythonhosted.org/packages/6d/c5/27febe918ac36397919cd4a67d5579cbbfa8da027fa1238af6285bb368ea/MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a", size = 32718 }, + { url = "https://files.pythonhosted.org/packages/f8/81/56e567126a2c2bc2684d6391332e357589a96a76cb9f8e5052d85cb0ead8/MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f", size = 33317 }, + { url = "https://files.pythonhosted.org/packages/00/0b/23f4b2470accb53285c613a3ab9ec19dc944eaf53592cb6d9e2af8aa24cc/MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906", size = 16670 }, + { url = "https://files.pythonhosted.org/packages/b7/a2/c78a06a9ec6d04b3445a949615c4c7ed86a0b2eb68e44e7541b9d57067cc/MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617", size = 17224 }, + { url = "https://files.pythonhosted.org/packages/53/bd/583bf3e4c8d6a321938c13f49d44024dbe5ed63e0a7ba127e454a66da974/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1", size = 18215 }, + { url = "https://files.pythonhosted.org/packages/48/d6/e7cd795fc710292c3af3a06d80868ce4b02bfbbf370b7cee11d282815a2a/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4", size = 14069 }, + { url = "https://files.pythonhosted.org/packages/51/b5/5d8ec796e2a08fc814a2c7d2584b55f889a55cf17dd1a90f2beb70744e5c/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee", size = 29452 }, + { url = "https://files.pythonhosted.org/packages/0a/0d/2454f072fae3b5a137c119abf15465d1771319dfe9e4acbb31722a0fff91/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5", size = 28462 }, + { url = "https://files.pythonhosted.org/packages/2d/75/fd6cb2e68780f72d47e6671840ca517bda5ef663d30ada7616b0462ad1e3/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b", size = 27869 }, + { url = "https://files.pythonhosted.org/packages/b0/81/147c477391c2750e8fc7705829f7351cf1cd3be64406edcf900dc633feb2/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a", size = 33906 }, + { url = "https://files.pythonhosted.org/packages/8b/ff/9a52b71839d7a256b563e85d11050e307121000dcebc97df120176b3ad93/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f", size = 32296 }, + { url = "https://files.pythonhosted.org/packages/88/07/2dc76aa51b481eb96a4c3198894f38b480490e834479611a4053fbf08623/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169", size = 33038 }, + { url = "https://files.pythonhosted.org/packages/96/0c/620c1fb3661858c0e37eb3cbffd8c6f732a67cd97296f725789679801b31/MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad", size = 16572 }, + { url = "https://files.pythonhosted.org/packages/3f/14/c3554d512d5f9100a95e737502f4a2323a1959f6d0d01e0d0997b35f7b10/MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb", size = 17127 }, + { url = "https://files.pythonhosted.org/packages/f8/ff/2c942a82c35a49df5de3a630ce0a8456ac2969691b230e530ac12314364c/MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a", size = 18192 }, + { url = "https://files.pythonhosted.org/packages/4f/14/6f294b9c4f969d0c801a4615e221c1e084722ea6114ab2114189c5b8cbe0/MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46", size = 14072 }, + { url = "https://files.pythonhosted.org/packages/81/d4/fd74714ed30a1dedd0b82427c02fa4deec64f173831ec716da11c51a50aa/MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532", size = 26928 }, + { url = "https://files.pythonhosted.org/packages/c7/bd/50319665ce81bb10e90d1cf76f9e1aa269ea6f7fa30ab4521f14d122a3df/MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab", size = 26106 }, + { url = "https://files.pythonhosted.org/packages/4c/6f/f2b0f675635b05f6afd5ea03c094557bdb8622fa8e673387444fe8d8e787/MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68", size = 25781 }, + { url = "https://files.pythonhosted.org/packages/51/e0/393467cf899b34a9d3678e78961c2c8cdf49fb902a959ba54ece01273fb1/MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0", size = 30518 }, + { url = "https://files.pythonhosted.org/packages/f6/02/5437e2ad33047290dafced9df741d9efc3e716b75583bbd73a9984f1b6f7/MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4", size = 29669 }, + { url = "https://files.pythonhosted.org/packages/0e/7d/968284145ffd9d726183ed6237c77938c021abacde4e073020f920e060b2/MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3", size = 29933 }, + { url = "https://files.pythonhosted.org/packages/bf/f3/ecb00fc8ab02b7beae8699f34db9357ae49d9f21d4d3de6f305f34fa949e/MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff", size = 16656 }, + { url = "https://files.pythonhosted.org/packages/92/21/357205f03514a49b293e214ac39de01fadd0970a6e05e4bf1ddd0ffd0881/MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029", size = 17206 }, + { url = "https://files.pythonhosted.org/packages/0f/31/780bb297db036ba7b7bbede5e1d7f1e14d704ad4beb3ce53fb495d22bc62/MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf", size = 18193 }, + { url = "https://files.pythonhosted.org/packages/6c/77/d77701bbef72892affe060cdacb7a2ed7fd68dae3b477a8642f15ad3b132/MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2", size = 14073 }, + { url = "https://files.pythonhosted.org/packages/d9/a7/1e558b4f78454c8a3a0199292d96159eb4d091f983bc35ef258314fe7269/MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8", size = 26486 }, + { url = "https://files.pythonhosted.org/packages/5f/5a/360da85076688755ea0cceb92472923086993e86b5613bbae9fbc14136b0/MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3", size = 25685 }, + { url = "https://files.pythonhosted.org/packages/6a/18/ae5a258e3401f9b8312f92b028c54d7026a97ec3ab20bfaddbdfa7d8cce8/MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465", size = 25338 }, + { url = "https://files.pythonhosted.org/packages/0b/cc/48206bd61c5b9d0129f4d75243b156929b04c94c09041321456fd06a876d/MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e", size = 30439 }, + { url = "https://files.pythonhosted.org/packages/d1/06/a41c112ab9ffdeeb5f77bc3e331fdadf97fa65e52e44ba31880f4e7f983c/MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea", size = 29531 }, + { url = "https://files.pythonhosted.org/packages/02/8c/ab9a463301a50dab04d5472e998acbd4080597abc048166ded5c7aa768c8/MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6", size = 29823 }, + { url = "https://files.pythonhosted.org/packages/bc/29/9bc18da763496b055d8e98ce476c8e718dcfd78157e17f555ce6dd7d0895/MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf", size = 16658 }, + { url = "https://files.pythonhosted.org/packages/f6/f8/4da07de16f10551ca1f640c92b5f316f9394088b183c6a57183df6de5ae4/MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5", size = 17211 }, +] + +[[package]] +name = "packaging" +version = "24.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/b5/b43a27ac7472e1818c4bafd44430e69605baefe1f34440593e0332ec8b4d/packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9", size = 147882 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/df/1fceb2f8900f8639e278b056416d49134fb8d84c5942ffaa01ad34782422/packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5", size = 53488 }, +] + +[[package]] +name = "pluggy" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/42/8f2833655a29c4e9cb52ee8a2be04ceac61bcff4a680fb338cbd3d1e322d/pluggy-1.2.0.tar.gz", hash = "sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3", size = 61613 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/32/4a79112b8b87b21450b066e102d6608907f4c885ed7b04c3fdb085d4d6ae/pluggy-1.2.0-py3-none-any.whl", hash = "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849", size = 17695 }, +] + +[[package]] +name = "py-cpuinfo" +version = "9.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/37/a8/d832f7293ebb21690860d2e01d8115e5ff6f2ae8bbdc953f0eb0fa4bd2c7/py-cpuinfo-9.0.0.tar.gz", hash = "sha256:3cdbbf3fac90dc6f118bfd64384f309edeadd902d7c8fb17f02ffa1fc3f49690", size = 104716 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/a9/023730ba63db1e494a271cb018dcd361bd2c917ba7004c3e49d5daf795a2/py_cpuinfo-9.0.0-py3-none-any.whl", hash = "sha256:859625bc251f64e21f077d099d4162689c762b5d6a4c3c97553d56241c9674d5", size = 22335 }, +] + +[[package]] +name = "pytest" +version = "7.4.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/80/1f/9d8e98e4133ffb16c90f3b405c43e38d3abb715bb5d7a63a5a684f7e46a3/pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280", size = 1357116 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/ff/f6e8b8f39e08547faece4bd80f89d5a8de68a38b2d179cc1c4490ffa3286/pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8", size = 325287 }, +] + +[[package]] +name = "pytest-benchmark" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "py-cpuinfo" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/08/e6b0067efa9a1f2a1eb3043ecd8a0c48bfeb60d3255006dcc829d72d5da2/pytest-benchmark-4.0.0.tar.gz", hash = "sha256:fb0785b83efe599a6a956361c0691ae1dbb5318018561af10f3e915caa0048d1", size = 334641 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/a1/3b70862b5b3f830f0422844f25a823d0470739d994466be9dbbbb414d85a/pytest_benchmark-4.0.0-py3-none-any.whl", hash = "sha256:fdb7db64e31c8b277dff9850d2a2556d8b60bcb0ea6524e36e28ffd7c87f71d6", size = 43951 }, +] + +[[package]] +name = "sortedcontainers" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575 }, +] + +[[package]] +name = "tomli" +version = "2.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c0/3f/d7af728f075fb08564c5949a9c95e44352e23dee646869fa104a3b2060a3/tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f", size = 15164 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/75/10a9ebee3fd790d20926a90a2547f0bf78f371b2f13aa822c759680ca7b9/tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", size = 12757 }, +] + +[[package]] +name = "werkzeug" +version = "2.2.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/3c/baaebf3235c87d61d6593467056d5a8fba7c75ac838b8d100a5e64eba7a0/Werkzeug-2.2.3.tar.gz", hash = "sha256:2e1ccc9417d4da358b9de6f174e3ac094391ea1d4fbef2d667865d819dfd0afe", size = 845884 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/f8/9da63c1617ae2a1dec2fbf6412f3a0cfe9d4ce029eccbda6e1e4258ca45f/Werkzeug-2.2.3-py3-none-any.whl", hash = "sha256:56433961bc1f12533306c624f3be5e744389ac61d722175d543e1751285da612", size = 233551 }, +] + +[[package]] +name = "zipp" +version = "3.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/00/27/f0ac6b846684cecce1ee93d32450c45ab607f65c2e0255f0092032d91f07/zipp-3.15.0.tar.gz", hash = "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b", size = 18454 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/fa/c9e82bbe1af6266adf08afb563905eb87cab83fde00a0a08963510621047/zipp-3.15.0-py3-none-any.whl", hash = "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556", size = 6758 }, +] diff --git a/crates/jsonschema-testsuite-codegen/Cargo.toml b/crates/jsonschema-testsuite-codegen/Cargo.toml new file mode 100644 index 00000000..478c713f --- /dev/null +++ b/crates/jsonschema-testsuite-codegen/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "jsonschema-testsuite-codegen" +version = "0.1.0" +rust-version.workspace = true +edition.workspace = true +authors.workspace = true +repository.workspace = true +license.workspace = true +publish = false + +[lib] +proc-macro = true + +[dependencies] +internal = { package = "jsonschema-testsuite-internal", path = "../jsonschema-testsuite-internal/" } +heck = "0.5" +proc-macro2 = "1" +quote = "1" +serde_json.workspace = true +syn = { version = "2", features = ["full"] } +walkdir = "2.5" diff --git a/crates/jsonschema-testsuite-codegen/src/config.rs b/crates/jsonschema-testsuite-codegen/src/config.rs new file mode 100644 index 00000000..bd4081d5 --- /dev/null +++ b/crates/jsonschema-testsuite-codegen/src/config.rs @@ -0,0 +1,95 @@ +use syn::{ + parse::{Parse, ParseStream}, + punctuated::Punctuated, + Expr, ExprArray, ExprLit, Lit, Meta, Token, +}; + +/// Configuration for the `suite` attribute. +pub(crate) struct SuiteConfig { + pub(crate) path: String, + pub(crate) drafts: Vec, + pub(crate) xfail: Vec, +} + +impl Parse for SuiteConfig { + fn parse(input: ParseStream) -> syn::Result { + let mut path = None; + let mut drafts = Vec::new(); + let mut xfail = Vec::new(); + + for meta in Punctuated::::parse_terminated(input)? { + match meta { + Meta::NameValue(nv) if nv.path.is_ident("path") => { + if let Expr::Lit(ExprLit { + lit: Lit::Str(lit), .. + }) = nv.value + { + path = Some(lit.value()); + } else { + return Err(syn::Error::new_spanned( + nv.value, + "Test suite path should be a string literal", + )); + } + } + Meta::NameValue(nv) if nv.path.is_ident("drafts") => { + if let Expr::Array(ExprArray { elems, .. }) = nv.value { + for elem in elems { + if let Expr::Lit(ExprLit { + lit: Lit::Str(lit), .. + }) = elem + { + drafts.push(lit.value()); + } else { + return Err(syn::Error::new_spanned( + elem, + "Drafts name should be a string literal", + )); + } + } + } else { + return Err(syn::Error::new_spanned( + nv.value, + "Drafts should be an array of string literals", + )); + } + } + Meta::NameValue(nv) if nv.path.is_ident("xfail") => { + if let Expr::Array(ExprArray { elems, .. }) = nv.value { + for elem in elems { + if let Expr::Lit(ExprLit { + lit: Lit::Str(lit), .. + }) = elem + { + xfail.push(lit.value()); + } else { + return Err(syn::Error::new_spanned( + elem, + "XFail item should be a string literal", + )); + } + } + } else { + return Err(syn::Error::new_spanned( + nv.value, + "XFail should be an array of string literals", + )); + } + } + _ => return Err(syn::Error::new_spanned(meta, "Unexpected attribute")), + } + } + let path = path.ok_or_else(|| { + syn::Error::new(input.span(), "Missing path to JSON Schema test suite") + })?; + if drafts.is_empty() { + return Err(syn::Error::new(input.span(), "Drafts are missing")); + } + + Ok(SuiteConfig { + path, + drafts, + xfail, + }) + } +} diff --git a/crates/jsonschema-testsuite-codegen/src/generator.rs b/crates/jsonschema-testsuite-codegen/src/generator.rs new file mode 100644 index 00000000..33f6583c --- /dev/null +++ b/crates/jsonschema-testsuite-codegen/src/generator.rs @@ -0,0 +1,114 @@ +use crate::{idents, loader}; +use heck::ToSnakeCase; +use std::collections::HashSet; + +use proc_macro2::TokenStream; +use quote::{format_ident, quote}; + +pub(crate) fn generate_modules( + tree: &loader::TestCaseTree, + functions: &mut HashSet, + xfail: &[String], + draft: &str, +) -> TokenStream { + generate_nested_structure(tree, functions, vec![draft.to_string()], xfail, draft) +} + +fn generate_nested_structure( + tree: &loader::TestCaseTree, + functions: &mut HashSet, + current_path: Vec, + xfail: &[String], + draft: &str, +) -> TokenStream { + let modules = tree.iter().map(|(name, node)| { + let module_name = idents::sanitize(name.to_snake_case()); + let module_ident = format_ident!("{}", module_name); + let mut new_path = current_path.clone(); + new_path.push(module_name.clone()); + + match node { + loader::TestCaseNode::Submodule(subtree) => { + let submodules = generate_nested_structure( + subtree, functions, new_path, xfail, draft + ); + quote! { + mod #module_ident { + use super::*; + + #submodules + } + } + } + loader::TestCaseNode::TestFile(cases) => { + let mut modules = HashSet::with_capacity(cases.len()); + let case_modules = cases.iter().map(|case| { + let base_module_name = idents::sanitize(case.description.to_snake_case()); + let module_name = idents::get_unique(&base_module_name, &mut modules); + let module_ident = format_ident!("{}", module_name); + let mut case_path = new_path.clone(); + case_path.push(module_name); + + let schema = serde_json::to_string(&case.schema).expect("Can't serialize JSON"); + let case_description = &case.description; + + let test_functions = case.tests.iter().map(|test| { + let base_test_name = idents::sanitize(test.description.to_snake_case()); + let test_name = idents::get_unique(&base_test_name, functions); + let test_ident = format_ident!("test_{}", test_name); + case_path.push(test_name.clone()); + + let full_test_path = case_path.join("::"); + let should_ignore = xfail.iter().any(|x| full_test_path.starts_with(x)); + let ignore_attr = if should_ignore { + quote! { #[ignore] } + } else { + quote! {} + }; + case_path.pop().expect("Empty path"); + + let test_description = &test.description; + let data = serde_json::to_string(&test.data).expect("Can't serialize JSON"); + let valid = test.valid; + + quote! { + #ignore_attr + #[test] + fn #test_ident() { + let test = testsuite::Test { + draft: #draft, + schema: serde_json::from_str(#schema).expect("Failed to load JSON"), + case: #case_description, + description: #test_description, + data: serde_json::from_str(#data).expect("Failed to load JSON"), + valid: #valid, + }; + inner_test(test); + } + } + }); + + quote! { + mod #module_ident { + use super::*; + + #(#test_functions)* + } + } + }); + + quote! { + mod #module_ident { + use super::*; + + #(#case_modules)* + } + } + } + } + }); + + quote! { + #(#modules)* + } +} diff --git a/crates/jsonschema-testsuite-codegen/src/idents.rs b/crates/jsonschema-testsuite-codegen/src/idents.rs new file mode 100644 index 00000000..aa52cfa2 --- /dev/null +++ b/crates/jsonschema-testsuite-codegen/src/idents.rs @@ -0,0 +1,20 @@ +use std::collections::HashSet; + +pub(crate) fn get_unique(base: &str, used: &mut HashSet) -> String { + let mut name = base.to_string(); + let mut counter = 1; + + while !used.insert(name.clone()) { + name = format!("{}_{}", base, counter); + counter += 1; + } + + name +} + +pub(crate) fn sanitize(s: String) -> String { + match s.as_str() { + "const" | "enum" | "ref" | "type" => format!("r#{s}"), + _ => s, + } +} diff --git a/crates/jsonschema-testsuite-codegen/src/lib.rs b/crates/jsonschema-testsuite-codegen/src/lib.rs new file mode 100644 index 00000000..94b90af3 --- /dev/null +++ b/crates/jsonschema-testsuite-codegen/src/lib.rs @@ -0,0 +1,71 @@ +use std::collections::HashSet; + +use proc_macro::TokenStream; + +use quote::{format_ident, quote}; +use syn::{parse_macro_input, ItemFn}; +mod config; +mod generator; +mod idents; +mod loader; +mod mocks; + +/// A procedural macro that generates tests from +/// [JSON-Schema-Test-Suite](https://github.com/json-schema-org/JSON-Schema-Test-Suite). +#[proc_macro_attribute] +pub fn suite(args: TokenStream, input: TokenStream) -> TokenStream { + let config = parse_macro_input!(args as config::SuiteConfig); + let test_func = parse_macro_input!(input as ItemFn); + let test_func_ident = &test_func.sig.ident; + + let mocks = match mocks::generate(&config.path) { + Ok(mocks) => mocks, + Err(e) => { + let err = e.to_string(); + return TokenStream::from(quote! { + compile_error!(#err); + }); + } + }; + + let mut output = quote! { + #test_func + + static MOCK: once_cell::sync::Lazy = once_cell::sync::Lazy::new(|| { + #mocks + }); + }; + // There are a lot of tests in the test suite + let mut functions = HashSet::with_capacity(7200); + + for draft in config.drafts { + let suite_tree = match loader::load_suite(&config.path, &draft) { + Ok(tree) => tree, + Err(e) => { + let err = e.to_string(); + return TokenStream::from(quote! { + compile_error!(#err); + }); + } + }; + let modules = + generator::generate_modules(&suite_tree, &mut functions, &config.xfail, &draft); + let draft = format_ident!("{}", &draft.replace("-", "_")); + output = quote! { + #output + + mod #draft { + use testsuite::Test; + use super::{#test_func_ident, MOCK}; + + #[inline] + fn inner_test(test: Test) { + let _ = &*MOCK; + #test_func_ident(test); + } + #modules + } + } + } + output.into() +} diff --git a/crates/jsonschema-testsuite-codegen/src/loader.rs b/crates/jsonschema-testsuite-codegen/src/loader.rs new file mode 100644 index 00000000..2aa45e2f --- /dev/null +++ b/crates/jsonschema-testsuite-codegen/src/loader.rs @@ -0,0 +1,73 @@ +use std::{collections::BTreeMap, fs::File, io::BufReader, path::Path}; + +use internal::Case; +use walkdir::WalkDir; + +pub(crate) type TestCaseTree = BTreeMap; + +#[derive(Debug)] +pub(crate) enum TestCaseNode { + Submodule(TestCaseTree), + TestFile(Vec), +} + +impl TestCaseNode { + fn submodule_mut(&mut self) -> Result<&mut TestCaseTree, Box> { + match self { + TestCaseNode::Submodule(tree) => Ok(tree), + TestCaseNode::TestFile(_) => Err("Expected a sub-module, found a test file".into()), + } + } +} + +pub(crate) fn load_suite( + suite_path: &str, + draft: &str, +) -> Result> { + let full_path = Path::new(suite_path).join("tests").join(draft); + if !full_path.exists() { + return Err(format!("Path does not exist: {}", full_path.display()).into()); + } + let mut root = TestCaseTree::new(); + + for entry in WalkDir::new(&full_path).into_iter().filter_map(Result::ok) { + let path = entry.path(); + if path.is_file() && path.extension().map_or(false, |ext| ext == "json") { + let relative_path = path.strip_prefix(&full_path)?; + let file = File::open(path)?; + let reader = BufReader::new(file); + let cases: Vec = serde_json::from_reader(reader)?; + + insert_into_module_tree(&mut root, relative_path, cases)?; + } + } + + Ok(root) +} + +fn insert_into_module_tree( + tree: &mut TestCaseTree, + path: &Path, + cases: Vec, +) -> Result<(), Box> { + let mut current = tree; + + // Navigate through the path components + for component in path.parent().unwrap_or(Path::new("")).components() { + let key = component.as_os_str().to_string_lossy().into_owned(); + current = current + .entry(key) + .or_insert_with(|| TestCaseNode::Submodule(TestCaseTree::new())) + .submodule_mut()?; + } + + // Insert the test file + let file_name = path + .file_stem() + .expect("Invalid filename") + .to_string_lossy() + .into_owned(); + current.insert(file_name, TestCaseNode::TestFile(cases)); + + Ok(()) +} diff --git a/crates/jsonschema-testsuite-codegen/src/mocks.rs b/crates/jsonschema-testsuite-codegen/src/mocks.rs new file mode 100644 index 00000000..2e323716 --- /dev/null +++ b/crates/jsonschema-testsuite-codegen/src/mocks.rs @@ -0,0 +1,54 @@ +use std::{ + fs::read_to_string, + path::{Path, MAIN_SEPARATOR}, +}; + +use proc_macro2::TokenStream as TokenStream2; +use quote::quote; + +pub(crate) fn generate(suite_path: &str) -> Result> { + let remotes = Path::new(suite_path).join("remotes"); + + let mut mock = quote! { + let mut server = mockito::Server::new_with_opts(mockito::ServerOpts { + port: 1234, + ..Default::default() + }); + }; + + if remotes.exists() && remotes.is_dir() { + for entry in walkdir::WalkDir::new(&remotes) + .into_iter() + .filter_map(|e| e.ok()) + .filter(|e| e.file_type().is_file()) + { + let path = entry.path(); + let relative_path = path.strip_prefix(&remotes).expect("Invalid path"); + let url_path = relative_path + .to_str() + .expect("Invalid filename") + .replace(MAIN_SEPARATOR, "/"); + let content = read_to_string(path).expect("Failed to read a file"); + + mock = quote! { + #mock + server.mock("GET", format!("/{}", #url_path).as_str()) + .with_status(200) + .with_header("content-type", "application/json") + .with_body(#content) + .create(); + } + } + } else { + return Err(format!( + "Path does not exist or is not a directory: {}. Run `git submodule init && git submodule update`", + remotes.display() + ) + .into()); + } + + Ok(quote! { + #mock + server + }) +} diff --git a/crates/jsonschema-testsuite-internal/Cargo.toml b/crates/jsonschema-testsuite-internal/Cargo.toml new file mode 100644 index 00000000..72999aca --- /dev/null +++ b/crates/jsonschema-testsuite-internal/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "jsonschema-testsuite-internal" +version = "0.1.0" +rust-version.workspace = true +edition.workspace = true +authors.workspace = true +repository.workspace = true +license.workspace = true +publish = false + +[dependencies] +serde_json.workspace = true +serde = { version = "1", features = ["derive"] } diff --git a/crates/jsonschema-testsuite-internal/src/lib.rs b/crates/jsonschema-testsuite-internal/src/lib.rs new file mode 100644 index 00000000..361cd0b0 --- /dev/null +++ b/crates/jsonschema-testsuite-internal/src/lib.rs @@ -0,0 +1,38 @@ +use serde_json::Value; + +/// An individual test case, containing multiple tests of a single schema's behavior. +#[derive(Debug, serde::Deserialize)] +pub struct Case { + /// The test case description. + pub description: String, + /// A valid JSON Schema. + pub schema: Value, + /// A set of related tests all using the same schema. + pub tests: Vec, +} + +/// A single test. +#[derive(Debug, serde::Deserialize)] +pub struct InnerTest { + /// The test description, briefly explaining which behavior it exercises. + pub description: String, + /// Any additional comments about the test. + pub comment: Option, + /// The instance which should be validated against the schema in schema. + pub data: Value, + /// Whether the validation process of this instance should consider the instance valid or not. + pub valid: bool, +} + +#[derive(Debug)] +pub struct Test { + pub draft: &'static str, + pub schema: Value, + pub case: &'static str, + /// The test description, briefly explaining which behavior it exercises. + pub description: &'static str, + /// The instance which should be validated against the schema in schema. + pub data: Value, + /// Whether the validation process of this instance should consider the instance valid or not. + pub valid: bool, +} diff --git a/crates/jsonschema-testsuite/Cargo.toml b/crates/jsonschema-testsuite/Cargo.toml new file mode 100644 index 00000000..77c2e34d --- /dev/null +++ b/crates/jsonschema-testsuite/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "jsonschema-testsuite" +version = "0.1.0" +rust-version.workspace = true +edition.workspace = true +authors.workspace = true +publish = false + +[dependencies] +codegen = { package = "jsonschema-testsuite-codegen", path = "../jsonschema-testsuite-codegen/" } +internal = { package = "jsonschema-testsuite-internal", path = "../jsonschema-testsuite-internal/" } diff --git a/crates/jsonschema-testsuite/README.md b/crates/jsonschema-testsuite/README.md new file mode 100644 index 00000000..1b421be5 --- /dev/null +++ b/crates/jsonschema-testsuite/README.md @@ -0,0 +1,47 @@ +# JSON Schema Test Suite + +A Rust proc macro for generating test suites from the official JSON Schema Test Suite. + +## Features + +- Automatically generates tests for multiple JSON Schema drafts +- Supports selective draft inclusion +- Allows marking specific tests as expected failures + +## Usage + +```rust +use jsonschema_testsuite::{suite, Test}; + +#[suite( + path = "path/to/test/suite", + drafts = ["draft7", "draft2020-12"], + xfail = ["draft7::some::failing::test"] +)] +fn test_suite(test: Test) { + // Your test implementation here +} +``` + +Where `Test` is the following struct: + +```rust +#[derive(Debug)] +pub struct Test { + pub draft: &'static str, + pub schema: Value, + /// Test case description. + pub case: &'static str, + /// The test description, briefly explaining which behavior it exercises. + pub description: &'static str, + /// The instance which should be validated against the schema in schema. + pub data: Value, + /// Whether the validation process of this instance should consider the instance valid or not. + pub valid: bool, +} +``` + +## License + +This project is licensed under the MIT License. + diff --git a/crates/jsonschema-testsuite/src/lib.rs b/crates/jsonschema-testsuite/src/lib.rs new file mode 100644 index 00000000..560528db --- /dev/null +++ b/crates/jsonschema-testsuite/src/lib.rs @@ -0,0 +1,2 @@ +pub use codegen::suite; +pub use internal::Test; diff --git a/crates/jsonschema/Cargo.toml b/crates/jsonschema/Cargo.toml new file mode 100644 index 00000000..cc006ea3 --- /dev/null +++ b/crates/jsonschema/Cargo.toml @@ -0,0 +1,69 @@ +[package] +name = "jsonschema" +version = "0.18.3" +description = "JSON schema validaton library" +keywords = ["jsonschema", "validation"] +categories = ["web-programming"] +readme = "../../README.md" +license.workspace = true +repository.workspace = true +rust-version.workspace = true +edition.workspace = true +authors.workspace = true + +[features] +default = ["resolve-http", "resolve-file"] + +resolve-http = ["reqwest"] +resolve-file = [] +# Deprecated in favor of `jsonschema-cli` +cli = [] +# Deprecated: Drafts 2019-09 & 2020-12 are enabled by default +draft201909 = [] +draft202012 = [] + +[dependencies] +ahash = { version = "0.8", features = ["serde"] } +anyhow = "1.0" +base64 = "0.22" +bytecount = { version = "0.6", features = ["runtime-dispatch-simd"] } +fancy-regex = "0.13" +fraction = { version = "0.15", default-features = false, features = [ + "with-bigint", +] } +iso8601 = "0.6" +itoa = "1" +memchr = "2.7" +num-cmp = "0.1" +once_cell = "1.19" +parking_lot = "0.12" +percent-encoding = "2.3" +regex = "1.10" +reqwest = { version = "0.12", features = [ + "blocking", + "json", +], default-features = false, optional = true } +serde = { version = "1.0", features = ["derive"] } +serde_json.workspace = true +time = { version = "0.3", features = ["parsing", "macros"] } +url = "2.5" +uuid = "1" + +[target.'cfg(target_arch = "wasm32")'.dependencies] +getrandom = { version = "0.2", features = ["js"] } + +[dev-dependencies] +benchmark = { path = "../benchmark/" } +codspeed-criterion-compat = "2.7" +criterion = { version = "0.5", default-features = false } +mockito = "1.1" +testsuite = { package = "jsonschema-testsuite", path = "../jsonschema-testsuite" } +test-case = "3" + +[[bench]] +harness = false +name = "jsonschema" + +[[bench]] +harness = false +name = "keywords" diff --git a/crates/jsonschema/benches/jsonschema.rs b/crates/jsonschema/benches/jsonschema.rs new file mode 100644 index 00000000..c7c28c47 --- /dev/null +++ b/crates/jsonschema/benches/jsonschema.rs @@ -0,0 +1,52 @@ +use benchmark::Benchmark; +use codspeed_criterion_compat::{criterion_group, criterion_main, BenchmarkId, Criterion}; +use jsonschema::JSONSchema; +use serde_json::Value; + +fn bench_compile(c: &mut Criterion, name: &str, schema: &Value) { + c.bench_function(&format!("{}/compile", name), |b| { + b.iter(|| JSONSchema::compile(schema).expect("Valid schema")) + }); +} + +fn bench_is_valid(c: &mut Criterion, name: &str, schema: &Value, instance: &Value) { + let compiled = JSONSchema::compile(schema).expect("Valid schema"); + c.bench_with_input( + BenchmarkId::new(name, "is_valid"), + instance, + |b, instance| { + b.iter(|| { + let _ = compiled.is_valid(instance); + }) + }, + ); +} + +fn bench_validate(c: &mut Criterion, name: &str, schema: &Value, instance: &Value) { + let compiled = JSONSchema::compile(schema).expect("Valid schema"); + c.bench_with_input( + BenchmarkId::new(name, "validate"), + instance, + |b, instance| { + b.iter(|| { + let _ = compiled.validate(instance); + }) + }, + ); +} + +fn run_benchmarks(c: &mut Criterion) { + for benchmark in Benchmark::iter() { + benchmark.run(&mut |name, schema, instances| { + bench_compile(c, name, schema); + for instance in instances { + let name = format!("jsonschema/{}/{}", name, instance.name); + bench_is_valid(c, &name, schema, &instance.data); + bench_validate(c, &name, schema, &instance.data); + } + }); + } +} + +criterion_group!(jsonschema, run_benchmarks); +criterion_main!(jsonschema); diff --git a/crates/jsonschema/benches/keywords.rs b/crates/jsonschema/benches/keywords.rs new file mode 100644 index 00000000..f39ff7a3 --- /dev/null +++ b/crates/jsonschema/benches/keywords.rs @@ -0,0 +1,50 @@ +use benchmark::run_keyword_benchmarks; +use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; +use jsonschema::JSONSchema; +use serde_json::Value; + +fn bench_keyword_compile(c: &mut Criterion, name: &str, schema: &Value) { + c.bench_function(&format!("keyword/{}/compile", name), |b| { + b.iter(|| JSONSchema::compile(schema).expect("Valid schema")) + }); +} + +fn bench_keyword_is_valid(c: &mut Criterion, name: &str, schema: &Value, instance: &Value) { + let compiled = JSONSchema::compile(schema).expect("Valid schema"); + c.bench_with_input( + BenchmarkId::new(format!("keyword/{}", name), "is_valid"), + instance, + |b, instance| { + b.iter(|| { + let _ = compiled.is_valid(instance); + }) + }, + ); +} + +fn bench_keyword_validate(c: &mut Criterion, name: &str, schema: &Value, instance: &Value) { + let compiled = JSONSchema::compile(schema).expect("Valid schema"); + c.bench_with_input( + BenchmarkId::new(format!("keyword/{}", name), "validate"), + instance, + |b, instance| { + b.iter(|| { + let _ = compiled.validate(instance); + }) + }, + ); +} + +fn run_benchmarks(c: &mut Criterion) { + run_keyword_benchmarks(&mut |name, schema, instances| { + bench_keyword_compile(c, name, schema); + for instance in instances { + let name = format!("jsonschema/{}/{}", name, instance.name); + bench_keyword_is_valid(c, &name, schema, &instance.data); + bench_keyword_validate(c, &name, schema, &instance.data); + } + }); +} + +criterion_group!(keywords, run_benchmarks); +criterion_main!(keywords); diff --git a/jsonschema/meta_schemas/draft2019-09/meta/applicator.json b/crates/jsonschema/meta_schemas/draft2019-09/meta/applicator.json similarity index 100% rename from jsonschema/meta_schemas/draft2019-09/meta/applicator.json rename to crates/jsonschema/meta_schemas/draft2019-09/meta/applicator.json diff --git a/jsonschema/meta_schemas/draft2019-09/meta/content.json b/crates/jsonschema/meta_schemas/draft2019-09/meta/content.json similarity index 100% rename from jsonschema/meta_schemas/draft2019-09/meta/content.json rename to crates/jsonschema/meta_schemas/draft2019-09/meta/content.json diff --git a/jsonschema/meta_schemas/draft2019-09/meta/core.json b/crates/jsonschema/meta_schemas/draft2019-09/meta/core.json similarity index 100% rename from jsonschema/meta_schemas/draft2019-09/meta/core.json rename to crates/jsonschema/meta_schemas/draft2019-09/meta/core.json diff --git a/jsonschema/meta_schemas/draft2019-09/meta/format.json b/crates/jsonschema/meta_schemas/draft2019-09/meta/format.json similarity index 100% rename from jsonschema/meta_schemas/draft2019-09/meta/format.json rename to crates/jsonschema/meta_schemas/draft2019-09/meta/format.json diff --git a/jsonschema/meta_schemas/draft2019-09/meta/meta-data.json b/crates/jsonschema/meta_schemas/draft2019-09/meta/meta-data.json similarity index 100% rename from jsonschema/meta_schemas/draft2019-09/meta/meta-data.json rename to crates/jsonschema/meta_schemas/draft2019-09/meta/meta-data.json diff --git a/jsonschema/meta_schemas/draft2019-09/meta/validation.json b/crates/jsonschema/meta_schemas/draft2019-09/meta/validation.json similarity index 100% rename from jsonschema/meta_schemas/draft2019-09/meta/validation.json rename to crates/jsonschema/meta_schemas/draft2019-09/meta/validation.json diff --git a/jsonschema/meta_schemas/draft2019-09/schema.json b/crates/jsonschema/meta_schemas/draft2019-09/schema.json similarity index 100% rename from jsonschema/meta_schemas/draft2019-09/schema.json rename to crates/jsonschema/meta_schemas/draft2019-09/schema.json diff --git a/jsonschema/meta_schemas/draft2020-12/meta/applicator.json b/crates/jsonschema/meta_schemas/draft2020-12/meta/applicator.json similarity index 100% rename from jsonschema/meta_schemas/draft2020-12/meta/applicator.json rename to crates/jsonschema/meta_schemas/draft2020-12/meta/applicator.json diff --git a/jsonschema/meta_schemas/draft2020-12/meta/content.json b/crates/jsonschema/meta_schemas/draft2020-12/meta/content.json similarity index 100% rename from jsonschema/meta_schemas/draft2020-12/meta/content.json rename to crates/jsonschema/meta_schemas/draft2020-12/meta/content.json diff --git a/jsonschema/meta_schemas/draft2020-12/meta/core.json b/crates/jsonschema/meta_schemas/draft2020-12/meta/core.json similarity index 100% rename from jsonschema/meta_schemas/draft2020-12/meta/core.json rename to crates/jsonschema/meta_schemas/draft2020-12/meta/core.json diff --git a/jsonschema/meta_schemas/draft2020-12/meta/format-annotation.json b/crates/jsonschema/meta_schemas/draft2020-12/meta/format-annotation.json similarity index 100% rename from jsonschema/meta_schemas/draft2020-12/meta/format-annotation.json rename to crates/jsonschema/meta_schemas/draft2020-12/meta/format-annotation.json diff --git a/jsonschema/meta_schemas/draft2020-12/meta/meta-data.json b/crates/jsonschema/meta_schemas/draft2020-12/meta/meta-data.json similarity index 100% rename from jsonschema/meta_schemas/draft2020-12/meta/meta-data.json rename to crates/jsonschema/meta_schemas/draft2020-12/meta/meta-data.json diff --git a/jsonschema/meta_schemas/draft2020-12/meta/unevaluated.json b/crates/jsonschema/meta_schemas/draft2020-12/meta/unevaluated.json similarity index 100% rename from jsonschema/meta_schemas/draft2020-12/meta/unevaluated.json rename to crates/jsonschema/meta_schemas/draft2020-12/meta/unevaluated.json diff --git a/jsonschema/meta_schemas/draft2020-12/meta/validation.json b/crates/jsonschema/meta_schemas/draft2020-12/meta/validation.json similarity index 100% rename from jsonschema/meta_schemas/draft2020-12/meta/validation.json rename to crates/jsonschema/meta_schemas/draft2020-12/meta/validation.json diff --git a/jsonschema/meta_schemas/draft2020-12/schema.json b/crates/jsonschema/meta_schemas/draft2020-12/schema.json similarity index 100% rename from jsonschema/meta_schemas/draft2020-12/schema.json rename to crates/jsonschema/meta_schemas/draft2020-12/schema.json diff --git a/jsonschema/meta_schemas/draft4.json b/crates/jsonschema/meta_schemas/draft4.json similarity index 100% rename from jsonschema/meta_schemas/draft4.json rename to crates/jsonschema/meta_schemas/draft4.json diff --git a/jsonschema/meta_schemas/draft6.json b/crates/jsonschema/meta_schemas/draft6.json similarity index 100% rename from jsonschema/meta_schemas/draft6.json rename to crates/jsonschema/meta_schemas/draft6.json diff --git a/jsonschema/meta_schemas/draft7.json b/crates/jsonschema/meta_schemas/draft7.json similarity index 100% rename from jsonschema/meta_schemas/draft7.json rename to crates/jsonschema/meta_schemas/draft7.json diff --git a/jsonschema/src/compilation/context.rs b/crates/jsonschema/src/compilation/context.rs similarity index 100% rename from jsonschema/src/compilation/context.rs rename to crates/jsonschema/src/compilation/context.rs diff --git a/jsonschema/src/compilation/mod.rs b/crates/jsonschema/src/compilation/mod.rs similarity index 99% rename from jsonschema/src/compilation/mod.rs rename to crates/jsonschema/src/compilation/mod.rs index 3ff42e6d..a52512b9 100644 --- a/jsonschema/src/compilation/mod.rs +++ b/crates/jsonschema/src/compilation/mod.rs @@ -24,7 +24,7 @@ use url::Url; pub(crate) const DEFAULT_ROOT_URL: &str = "json-schema:///"; -/// The structure that holds a JSON Schema compiled into a validation tree +/// The structure that holds a JSON Schema nodes. #[derive(Debug)] pub struct JSONSchema { pub(crate) node: SchemaNode, @@ -54,9 +54,7 @@ impl JSONSchema { CompilationOptions::default() } - /// Compile the input schema into a validation tree. - /// - /// The method is equivalent to `JSONSchema::options().compile(schema)` + /// Compile the input schema for faster validation. pub fn compile(schema: &Value) -> Result { Self::options().compile(schema) } diff --git a/jsonschema/src/compilation/options.rs b/crates/jsonschema/src/compilation/options.rs similarity index 81% rename from jsonschema/src/compilation/options.rs rename to crates/jsonschema/src/compilation/options.rs index d638ee06..30fb7064 100644 --- a/jsonschema/src/compilation/options.rs +++ b/crates/jsonschema/src/compilation/options.rs @@ -94,72 +94,66 @@ static META_SCHEMAS: Lazy, Arc>> = "http://json-schema.org/draft-07/schema".into(), Arc::clone(&DRAFT7), ); - #[cfg(feature = "draft201909")] - { - store.insert( - "https://json-schema.org/draft/2019-09/schema".into(), - Arc::clone(&DRAFT201909), - ); - store.insert( - "https://json-schema.org/draft/2019-09/meta/applicator".into(), - Arc::clone(&DRAFT201909_APPLICATOR), - ); - store.insert( - "https://json-schema.org/draft/2019-09/meta/content".into(), - Arc::clone(&DRAFT201909_CONTENT), - ); - store.insert( - "https://json-schema.org/draft/2019-09/meta/core".into(), - Arc::clone(&DRAFT201909_CORE), - ); - store.insert( - "https://json-schema.org/draft/2019-09/meta/format".into(), - Arc::clone(&DRAFT201909_FORMAT), - ); - store.insert( - "https://json-schema.org/draft/2019-09/meta/meta-data".into(), - Arc::clone(&DRAFT201909_META_DATA), - ); - store.insert( - "https://json-schema.org/draft/2019-09/meta/validation".into(), - Arc::clone(&DRAFT201909_VALIDATION), - ); - } - #[cfg(feature = "draft202012")] - { - store.insert( - "https://json-schema.org/draft/2020-12/schema".into(), - Arc::clone(&DRAFT202012), - ); - store.insert( - "https://json-schema.org/draft/2020-12/meta/core".into(), - Arc::clone(&DRAFT202012_CORE), - ); - store.insert( - "https://json-schema.org/draft/2020-12/meta/applicator".into(), - Arc::clone(&DRAFT202012_APPLICATOR), - ); - store.insert( - "https://json-schema.org/draft/2020-12/meta/unevaluated".into(), - Arc::clone(&DRAFT202012_UNEVALUATED), - ); - store.insert( - "https://json-schema.org/draft/2020-12/meta/validation".into(), - Arc::clone(&DRAFT202012_VALIDATION), - ); - store.insert( - "https://json-schema.org/draft/2020-12/meta/meta-data".into(), - Arc::clone(&DRAFT202012_META_DATA), - ); - store.insert( - "https://json-schema.org/draft/2020-12/meta/format-annotation".into(), - Arc::clone(&DRAFT202012_FORMAT_ANNOTATION), - ); - store.insert( - "https://json-schema.org/draft/2020-12/meta/content".into(), - Arc::clone(&DRAFT202012_CONTENT), - ); - } + store.insert( + "https://json-schema.org/draft/2019-09/schema".into(), + Arc::clone(&DRAFT201909), + ); + store.insert( + "https://json-schema.org/draft/2019-09/meta/applicator".into(), + Arc::clone(&DRAFT201909_APPLICATOR), + ); + store.insert( + "https://json-schema.org/draft/2019-09/meta/content".into(), + Arc::clone(&DRAFT201909_CONTENT), + ); + store.insert( + "https://json-schema.org/draft/2019-09/meta/core".into(), + Arc::clone(&DRAFT201909_CORE), + ); + store.insert( + "https://json-schema.org/draft/2019-09/meta/format".into(), + Arc::clone(&DRAFT201909_FORMAT), + ); + store.insert( + "https://json-schema.org/draft/2019-09/meta/meta-data".into(), + Arc::clone(&DRAFT201909_META_DATA), + ); + store.insert( + "https://json-schema.org/draft/2019-09/meta/validation".into(), + Arc::clone(&DRAFT201909_VALIDATION), + ); + store.insert( + "https://json-schema.org/draft/2020-12/schema".into(), + Arc::clone(&DRAFT202012), + ); + store.insert( + "https://json-schema.org/draft/2020-12/meta/core".into(), + Arc::clone(&DRAFT202012_CORE), + ); + store.insert( + "https://json-schema.org/draft/2020-12/meta/applicator".into(), + Arc::clone(&DRAFT202012_APPLICATOR), + ); + store.insert( + "https://json-schema.org/draft/2020-12/meta/unevaluated".into(), + Arc::clone(&DRAFT202012_UNEVALUATED), + ); + store.insert( + "https://json-schema.org/draft/2020-12/meta/validation".into(), + Arc::clone(&DRAFT202012_VALIDATION), + ); + store.insert( + "https://json-schema.org/draft/2020-12/meta/meta-data".into(), + Arc::clone(&DRAFT202012_META_DATA), + ); + store.insert( + "https://json-schema.org/draft/2020-12/meta/format-annotation".into(), + Arc::clone(&DRAFT202012_FORMAT_ANNOTATION), + ); + store.insert( + "https://json-schema.org/draft/2020-12/meta/content".into(), + Arc::clone(&DRAFT202012_CONTENT), + ); store }); @@ -187,84 +181,78 @@ static META_SCHEMA_VALIDATORS: Lazy> = Lazy .compile(&DRAFT7) .expect(EXPECT_MESSAGE), ); - #[cfg(feature = "draft201909")] - { - let mut options = JSONSchema::options(); - options.store.insert( - "https://json-schema.org/draft/2019-09/meta/applicator".into(), - Arc::clone(&DRAFT201909_APPLICATOR), - ); - options.store.insert( - "https://json-schema.org/draft/2019-09/meta/content".into(), - Arc::clone(&DRAFT201909_CONTENT), - ); - options.store.insert( - "https://json-schema.org/draft/2019-09/meta/core".into(), - Arc::clone(&DRAFT201909_CORE), - ); - options.store.insert( - "https://json-schema.org/draft/2019-09/meta/format".into(), - Arc::clone(&DRAFT201909_FORMAT), - ); - options.store.insert( - "https://json-schema.org/draft/2019-09/meta/meta-data".into(), - Arc::clone(&DRAFT201909_META_DATA), - ); - options.store.insert( - "https://json-schema.org/draft/2019-09/meta/validation".into(), - Arc::clone(&DRAFT201909_VALIDATION), - ); - store.insert( - schemas::Draft::Draft201909, - options - .without_schema_validation() - .compile(&DRAFT201909) - .expect(EXPECT_MESSAGE), - ); - } - #[cfg(feature = "draft202012")] - { - let mut options = JSONSchema::options(); - options.store.insert( - "https://json-schema.org/draft/2020-12/meta/applicator".into(), - Arc::clone(&DRAFT202012_APPLICATOR), - ); - options.store.insert( - "https://json-schema.org/draft/2020-12/meta/core".into(), - Arc::clone(&DRAFT202012_CORE), - ); - options.store.insert( - "https://json-schema.org/draft/2020-12/meta/applicator".into(), - Arc::clone(&DRAFT202012_APPLICATOR), - ); - options.store.insert( - "https://json-schema.org/draft/2020-12/meta/unevaluated".into(), - Arc::clone(&DRAFT202012_UNEVALUATED), - ); - options.store.insert( - "https://json-schema.org/draft/2020-12/meta/validation".into(), - Arc::clone(&DRAFT202012_VALIDATION), - ); - options.store.insert( - "https://json-schema.org/draft/2020-12/meta/meta-data".into(), - Arc::clone(&DRAFT202012_META_DATA), - ); - options.store.insert( - "https://json-schema.org/draft/2020-12/meta/format-annotation".into(), - Arc::clone(&DRAFT202012_FORMAT_ANNOTATION), - ); - options.store.insert( - "https://json-schema.org/draft/2020-12/meta/content".into(), - Arc::clone(&DRAFT202012_CONTENT), - ); - store.insert( - schemas::Draft::Draft202012, - options - .without_schema_validation() - .compile(&DRAFT202012) - .expect(EXPECT_MESSAGE), - ) - }; + let mut options = JSONSchema::options(); + options.store.insert( + "https://json-schema.org/draft/2019-09/meta/applicator".into(), + Arc::clone(&DRAFT201909_APPLICATOR), + ); + options.store.insert( + "https://json-schema.org/draft/2019-09/meta/content".into(), + Arc::clone(&DRAFT201909_CONTENT), + ); + options.store.insert( + "https://json-schema.org/draft/2019-09/meta/core".into(), + Arc::clone(&DRAFT201909_CORE), + ); + options.store.insert( + "https://json-schema.org/draft/2019-09/meta/format".into(), + Arc::clone(&DRAFT201909_FORMAT), + ); + options.store.insert( + "https://json-schema.org/draft/2019-09/meta/meta-data".into(), + Arc::clone(&DRAFT201909_META_DATA), + ); + options.store.insert( + "https://json-schema.org/draft/2019-09/meta/validation".into(), + Arc::clone(&DRAFT201909_VALIDATION), + ); + store.insert( + schemas::Draft::Draft201909, + options + .without_schema_validation() + .compile(&DRAFT201909) + .expect(EXPECT_MESSAGE), + ); + let mut options = JSONSchema::options(); + options.store.insert( + "https://json-schema.org/draft/2020-12/meta/applicator".into(), + Arc::clone(&DRAFT202012_APPLICATOR), + ); + options.store.insert( + "https://json-schema.org/draft/2020-12/meta/core".into(), + Arc::clone(&DRAFT202012_CORE), + ); + options.store.insert( + "https://json-schema.org/draft/2020-12/meta/applicator".into(), + Arc::clone(&DRAFT202012_APPLICATOR), + ); + options.store.insert( + "https://json-schema.org/draft/2020-12/meta/unevaluated".into(), + Arc::clone(&DRAFT202012_UNEVALUATED), + ); + options.store.insert( + "https://json-schema.org/draft/2020-12/meta/validation".into(), + Arc::clone(&DRAFT202012_VALIDATION), + ); + options.store.insert( + "https://json-schema.org/draft/2020-12/meta/meta-data".into(), + Arc::clone(&DRAFT202012_META_DATA), + ); + options.store.insert( + "https://json-schema.org/draft/2020-12/meta/format-annotation".into(), + Arc::clone(&DRAFT202012_FORMAT_ANNOTATION), + ); + options.store.insert( + "https://json-schema.org/draft/2020-12/meta/content".into(), + Arc::clone(&DRAFT202012_CONTENT), + ); + store.insert( + schemas::Draft::Draft202012, + options + .without_schema_validation() + .compile(&DRAFT202012) + .expect(EXPECT_MESSAGE), + ); store }); diff --git a/jsonschema/src/content_encoding.rs b/crates/jsonschema/src/content_encoding.rs similarity index 100% rename from jsonschema/src/content_encoding.rs rename to crates/jsonschema/src/content_encoding.rs diff --git a/jsonschema/src/content_media_type.rs b/crates/jsonschema/src/content_media_type.rs similarity index 100% rename from jsonschema/src/content_media_type.rs rename to crates/jsonschema/src/content_media_type.rs diff --git a/jsonschema/src/error.rs b/crates/jsonschema/src/error.rs similarity index 99% rename from jsonschema/src/error.rs rename to crates/jsonschema/src/error.rs index 4451c5f4..a226bf57 100644 --- a/jsonschema/src/error.rs +++ b/crates/jsonschema/src/error.rs @@ -70,7 +70,7 @@ pub enum ValidationErrorKind { AdditionalProperties { unexpected: Vec }, /// The input value is not valid under any of the schemas listed in the 'anyOf' keyword. AnyOf, - /// Results from a [`fancy_regex::Error::BacktrackLimitExceeded`] variant when matching + /// Results from a [`fancy_regex::RuntimeError::BacktrackLimitExceeded`] variant when matching BacktrackLimitExceeded { error: fancy_regex::Error }, /// The input value doesn't match expected constant. Constant { expected_value: Value }, diff --git a/jsonschema/src/keywords/additional_items.rs b/crates/jsonschema/src/keywords/additional_items.rs similarity index 100% rename from jsonschema/src/keywords/additional_items.rs rename to crates/jsonschema/src/keywords/additional_items.rs diff --git a/jsonschema/src/keywords/additional_properties.rs b/crates/jsonschema/src/keywords/additional_properties.rs similarity index 100% rename from jsonschema/src/keywords/additional_properties.rs rename to crates/jsonschema/src/keywords/additional_properties.rs diff --git a/jsonschema/src/keywords/all_of.rs b/crates/jsonschema/src/keywords/all_of.rs similarity index 100% rename from jsonschema/src/keywords/all_of.rs rename to crates/jsonschema/src/keywords/all_of.rs diff --git a/jsonschema/src/keywords/any_of.rs b/crates/jsonschema/src/keywords/any_of.rs similarity index 100% rename from jsonschema/src/keywords/any_of.rs rename to crates/jsonschema/src/keywords/any_of.rs diff --git a/jsonschema/src/keywords/boolean.rs b/crates/jsonschema/src/keywords/boolean.rs similarity index 100% rename from jsonschema/src/keywords/boolean.rs rename to crates/jsonschema/src/keywords/boolean.rs diff --git a/jsonschema/src/keywords/const_.rs b/crates/jsonschema/src/keywords/const_.rs similarity index 100% rename from jsonschema/src/keywords/const_.rs rename to crates/jsonschema/src/keywords/const_.rs diff --git a/jsonschema/src/keywords/contains.rs b/crates/jsonschema/src/keywords/contains.rs similarity index 92% rename from jsonschema/src/keywords/contains.rs rename to crates/jsonschema/src/keywords/contains.rs index 03338896..2bf37a0c 100644 --- a/jsonschema/src/keywords/contains.rs +++ b/crates/jsonschema/src/keywords/contains.rs @@ -9,7 +9,6 @@ use crate::{ }; use serde_json::{Map, Value}; -#[cfg(any(feature = "draft201909", feature = "draft202012"))] use super::helpers::map_get_u64; pub(crate) struct ContainsValidator { @@ -418,16 +417,10 @@ pub(crate) fn compile<'a>( Draft::Draft4 | Draft::Draft6 | Draft::Draft7 => { Some(ContainsValidator::compile(schema, context)) } - #[cfg(all(feature = "draft201909", feature = "draft202012"))] Draft::Draft201909 | Draft::Draft202012 => compile_contains(parent, schema, context), - #[cfg(all(feature = "draft201909", not(feature = "draft202012")))] - Draft::Draft201909 => compile_contains(parent, schema, context), - #[cfg(all(feature = "draft202012", not(feature = "draft201909")))] - Draft::Draft202012 => compile_contains(parent, schema, context), } } -#[cfg(any(feature = "draft201909", feature = "draft202012"))] #[inline] fn compile_contains<'a>( parent: &'a Map, @@ -456,30 +449,6 @@ mod tests { use crate::tests_util; use serde_json::json; - #[test] - fn max_contains_with_decimal() { - tests_util::is_valid( - &json!({ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "contains": {"const": 1}, - "maxContains": 1.0 - }), - &json!([1]), - ); - } - - #[test] - fn min_contains_with_decimal() { - tests_util::is_valid( - &json!({ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "contains": {"const": 1}, - "minContains": 2.0 - }), - &json!([1, 1]), - ); - } - #[test] fn schema_path() { tests_util::assert_schema_path(&json!({"contains": {"const": 2}}), &json!([]), "/contains") diff --git a/jsonschema/src/keywords/content.rs b/crates/jsonschema/src/keywords/content.rs similarity index 100% rename from jsonschema/src/keywords/content.rs rename to crates/jsonschema/src/keywords/content.rs diff --git a/jsonschema/src/keywords/custom.rs b/crates/jsonschema/src/keywords/custom.rs similarity index 100% rename from jsonschema/src/keywords/custom.rs rename to crates/jsonschema/src/keywords/custom.rs diff --git a/jsonschema/src/keywords/dependencies.rs b/crates/jsonschema/src/keywords/dependencies.rs similarity index 100% rename from jsonschema/src/keywords/dependencies.rs rename to crates/jsonschema/src/keywords/dependencies.rs diff --git a/jsonschema/src/keywords/enum_.rs b/crates/jsonschema/src/keywords/enum_.rs similarity index 100% rename from jsonschema/src/keywords/enum_.rs rename to crates/jsonschema/src/keywords/enum_.rs diff --git a/jsonschema/src/keywords/exclusive_maximum.rs b/crates/jsonschema/src/keywords/exclusive_maximum.rs similarity index 100% rename from jsonschema/src/keywords/exclusive_maximum.rs rename to crates/jsonschema/src/keywords/exclusive_maximum.rs diff --git a/jsonschema/src/keywords/exclusive_minimum.rs b/crates/jsonschema/src/keywords/exclusive_minimum.rs similarity index 100% rename from jsonschema/src/keywords/exclusive_minimum.rs rename to crates/jsonschema/src/keywords/exclusive_minimum.rs diff --git a/jsonschema/src/keywords/format.rs b/crates/jsonschema/src/keywords/format.rs similarity index 97% rename from jsonschema/src/keywords/format.rs rename to crates/jsonschema/src/keywords/format.rs index ac616f8c..27f87af7 100644 --- a/jsonschema/src/keywords/format.rs +++ b/crates/jsonschema/src/keywords/format.rs @@ -460,7 +460,6 @@ pub(crate) fn compile<'a>( "idn-hostname" if draft_version == Draft::Draft7 => { Some(IDNHostnameValidator::compile(context)) } - #[cfg(feature = "draft201909")] "idn-hostname" if draft_version == Draft::Draft201909 => { Some(IDNHostnameValidator::compile(context)) } @@ -469,17 +468,14 @@ pub(crate) fn compile<'a>( "iri-reference" if draft_version == Draft::Draft7 => { Some(IRIReferenceValidator::compile(context)) } - #[cfg(feature = "draft201909")] "iri-reference" if draft_version == Draft::Draft201909 => { Some(IRIReferenceValidator::compile(context)) } "iri" if draft_version == Draft::Draft7 => Some(IRIValidator::compile(context)), - #[cfg(feature = "draft201909")] "iri" if draft_version == Draft::Draft201909 => Some(IRIValidator::compile(context)), "json-pointer" if draft_version == Draft::Draft6 || draft_version == Draft::Draft7 => { Some(JSONPointerValidator::compile(context)) } - #[cfg(feature = "draft201909")] "json-pointer" if draft_version == Draft::Draft201909 => { Some(JSONPointerValidator::compile(context)) } @@ -487,7 +483,6 @@ pub(crate) fn compile<'a>( "relative-json-pointer" if draft_version == Draft::Draft7 => { Some(RelativeJSONPointerValidator::compile(context)) } - #[cfg(feature = "draft201909")] "relative-json-pointer" if draft_version == Draft::Draft201909 => { Some(RelativeJSONPointerValidator::compile(context)) } @@ -495,21 +490,17 @@ pub(crate) fn compile<'a>( "uri-reference" if draft_version == Draft::Draft6 || draft_version == Draft::Draft7 => { Some(URIReferenceValidator::compile(context)) } - #[cfg(feature = "draft201909")] "uri-reference" if draft_version == Draft::Draft201909 => { Some(URIReferenceValidator::compile(context)) } "uri-template" if draft_version == Draft::Draft6 || draft_version == Draft::Draft7 => { Some(URITemplateValidator::compile(context)) } - #[cfg(feature = "draft201909")] "uri-template" if draft_version == Draft::Draft201909 => { Some(URITemplateValidator::compile(context)) } - #[cfg(feature = "draft201909")] "uuid" if draft_version == Draft::Draft201909 => Some(UUIDValidator::compile(context)), "uri" => Some(URIValidator::compile(context)), - #[cfg(feature = "draft201909")] "duration" if draft_version == Draft::Draft201909 => { Some(DurationValidator::compile(context)) } @@ -540,9 +531,10 @@ pub(crate) fn compile<'a>( mod tests { use serde_json::json; - #[cfg(feature = "draft201909")] - use crate::schemas::Draft::Draft201909; - use crate::{compilation::JSONSchema, error::ValidationErrorKind, tests_util}; + use crate::{ + compilation::JSONSchema, error::ValidationErrorKind, schemas::Draft::Draft201909, + tests_util, + }; #[test] fn ignored_format() { @@ -587,7 +579,6 @@ mod tests { tests_util::assert_schema_path(&json!({"format": "date"}), &json!("bla"), "/format") } - #[cfg(feature = "draft201909")] #[test] fn uuid() { let schema = json!({"format": "uuid", "type": "string"}); @@ -616,7 +607,6 @@ mod tests { tests_util::is_not_valid(&schema, &failing_instance); } - #[cfg(feature = "draft201909")] #[test] fn duration() { let schema = json!({"format": "duration", "type": "string"}); diff --git a/jsonschema/src/keywords/helpers.rs b/crates/jsonschema/src/keywords/helpers.rs similarity index 100% rename from jsonschema/src/keywords/helpers.rs rename to crates/jsonschema/src/keywords/helpers.rs diff --git a/jsonschema/src/keywords/if_.rs b/crates/jsonschema/src/keywords/if_.rs similarity index 100% rename from jsonschema/src/keywords/if_.rs rename to crates/jsonschema/src/keywords/if_.rs diff --git a/jsonschema/src/keywords/items.rs b/crates/jsonschema/src/keywords/items.rs similarity index 99% rename from jsonschema/src/keywords/items.rs rename to crates/jsonschema/src/keywords/items.rs index a66c4401..4c377fdd 100644 --- a/jsonschema/src/keywords/items.rs +++ b/crates/jsonschema/src/keywords/items.rs @@ -237,7 +237,6 @@ pub(crate) fn compile<'a>( match schema { Value::Array(items) => Some(ItemsArrayValidator::compile(items, context)), Value::Object(_) | Value::Bool(false) => { - #[cfg(feature = "draft202012")] if let Some(Value::Array(prefix_items)) = parent.get("prefixItems") { return Some(ItemsObjectSkipPrefixValidator::compile( schema, diff --git a/jsonschema/src/keywords/legacy/maximum_draft_4.rs b/crates/jsonschema/src/keywords/legacy/maximum_draft_4.rs similarity index 100% rename from jsonschema/src/keywords/legacy/maximum_draft_4.rs rename to crates/jsonschema/src/keywords/legacy/maximum_draft_4.rs diff --git a/jsonschema/src/keywords/legacy/minimum_draft_4.rs b/crates/jsonschema/src/keywords/legacy/minimum_draft_4.rs similarity index 100% rename from jsonschema/src/keywords/legacy/minimum_draft_4.rs rename to crates/jsonschema/src/keywords/legacy/minimum_draft_4.rs diff --git a/jsonschema/src/keywords/legacy/mod.rs b/crates/jsonschema/src/keywords/legacy/mod.rs similarity index 100% rename from jsonschema/src/keywords/legacy/mod.rs rename to crates/jsonschema/src/keywords/legacy/mod.rs diff --git a/jsonschema/src/keywords/legacy/type_draft_4.rs b/crates/jsonschema/src/keywords/legacy/type_draft_4.rs similarity index 100% rename from jsonschema/src/keywords/legacy/type_draft_4.rs rename to crates/jsonschema/src/keywords/legacy/type_draft_4.rs diff --git a/jsonschema/src/keywords/max_items.rs b/crates/jsonschema/src/keywords/max_items.rs similarity index 96% rename from jsonschema/src/keywords/max_items.rs rename to crates/jsonschema/src/keywords/max_items.rs index 68760268..735bd99e 100644 --- a/jsonschema/src/keywords/max_items.rs +++ b/crates/jsonschema/src/keywords/max_items.rs @@ -93,11 +93,6 @@ mod tests { use crate::tests_util; use serde_json::json; - #[test] - fn with_decimal() { - tests_util::is_valid(&json!({"maxItems": 2.0}), &json!([1])); - } - #[test] fn schema_path() { tests_util::assert_schema_path(&json!({"maxItems": 1}), &json!([1, 2]), "/maxItems") diff --git a/jsonschema/src/keywords/max_length.rs b/crates/jsonschema/src/keywords/max_length.rs similarity index 96% rename from jsonschema/src/keywords/max_length.rs rename to crates/jsonschema/src/keywords/max_length.rs index 236ebf78..b8074a1e 100644 --- a/jsonschema/src/keywords/max_length.rs +++ b/crates/jsonschema/src/keywords/max_length.rs @@ -93,11 +93,6 @@ mod tests { use crate::tests_util; use serde_json::json; - #[test] - fn with_decimal() { - tests_util::is_valid(&json!({"maxLength": 2.0}), &json!("f")); - } - #[test] fn schema_path() { tests_util::assert_schema_path(&json!({"maxLength": 1}), &json!("ab"), "/maxLength") diff --git a/jsonschema/src/keywords/max_properties.rs b/crates/jsonschema/src/keywords/max_properties.rs similarity index 95% rename from jsonschema/src/keywords/max_properties.rs rename to crates/jsonschema/src/keywords/max_properties.rs index 2c99b366..2eb9cddc 100644 --- a/jsonschema/src/keywords/max_properties.rs +++ b/crates/jsonschema/src/keywords/max_properties.rs @@ -93,11 +93,6 @@ mod tests { use crate::tests_util; use serde_json::json; - #[test] - fn with_decimal() { - tests_util::is_valid(&json!({"maxProperties": 2.0}), &json!({"foo": 1})); - } - #[test] fn schema_path() { tests_util::assert_schema_path( diff --git a/jsonschema/src/keywords/maximum.rs b/crates/jsonschema/src/keywords/maximum.rs similarity index 100% rename from jsonschema/src/keywords/maximum.rs rename to crates/jsonschema/src/keywords/maximum.rs diff --git a/jsonschema/src/keywords/min_items.rs b/crates/jsonschema/src/keywords/min_items.rs similarity index 96% rename from jsonschema/src/keywords/min_items.rs rename to crates/jsonschema/src/keywords/min_items.rs index 779ac7c5..91063d40 100644 --- a/jsonschema/src/keywords/min_items.rs +++ b/crates/jsonschema/src/keywords/min_items.rs @@ -93,11 +93,6 @@ mod tests { use crate::tests_util; use serde_json::json; - #[test] - fn with_decimal() { - tests_util::is_valid(&json!({"minItems": 1.0}), &json!([1, 2])); - } - #[test] fn schema_path() { tests_util::assert_schema_path(&json!({"minItems": 1}), &json!([]), "/minItems") diff --git a/jsonschema/src/keywords/min_length.rs b/crates/jsonschema/src/keywords/min_length.rs similarity index 96% rename from jsonschema/src/keywords/min_length.rs rename to crates/jsonschema/src/keywords/min_length.rs index 56e07083..3fab7712 100644 --- a/jsonschema/src/keywords/min_length.rs +++ b/crates/jsonschema/src/keywords/min_length.rs @@ -93,11 +93,6 @@ mod tests { use crate::tests_util; use serde_json::json; - #[test] - fn with_decimal() { - tests_util::is_valid(&json!({"minLength": 2.0}), &json!("foo")); - } - #[test] fn schema_path() { tests_util::assert_schema_path(&json!({"minLength": 1}), &json!(""), "/minLength") diff --git a/jsonschema/src/keywords/min_properties.rs b/crates/jsonschema/src/keywords/min_properties.rs similarity index 95% rename from jsonschema/src/keywords/min_properties.rs rename to crates/jsonschema/src/keywords/min_properties.rs index 99d7d039..0938a23c 100644 --- a/jsonschema/src/keywords/min_properties.rs +++ b/crates/jsonschema/src/keywords/min_properties.rs @@ -93,11 +93,6 @@ mod tests { use crate::tests_util; use serde_json::json; - #[test] - fn with_decimal() { - tests_util::is_valid(&json!({"minProperties": 1.0}), &json!({"foo": 1, "bar": 2})); - } - #[test] fn schema_path() { tests_util::assert_schema_path( diff --git a/jsonschema/src/keywords/minimum.rs b/crates/jsonschema/src/keywords/minimum.rs similarity index 100% rename from jsonschema/src/keywords/minimum.rs rename to crates/jsonschema/src/keywords/minimum.rs diff --git a/jsonschema/src/keywords/mod.rs b/crates/jsonschema/src/keywords/mod.rs similarity index 99% rename from jsonschema/src/keywords/mod.rs rename to crates/jsonschema/src/keywords/mod.rs index 71701fe5..264b6c69 100644 --- a/jsonschema/src/keywords/mod.rs +++ b/crates/jsonschema/src/keywords/mod.rs @@ -35,7 +35,6 @@ pub(crate) mod property_names; pub(crate) mod ref_; pub(crate) mod required; pub(crate) mod type_; -#[cfg(any(feature = "draft201909", feature = "draft202012"))] pub(crate) mod unevaluated_properties; pub(crate) mod unique_items; use crate::{error, validator::Validate}; diff --git a/jsonschema/src/keywords/multiple_of.rs b/crates/jsonschema/src/keywords/multiple_of.rs similarity index 100% rename from jsonschema/src/keywords/multiple_of.rs rename to crates/jsonschema/src/keywords/multiple_of.rs diff --git a/jsonschema/src/keywords/not.rs b/crates/jsonschema/src/keywords/not.rs similarity index 100% rename from jsonschema/src/keywords/not.rs rename to crates/jsonschema/src/keywords/not.rs diff --git a/jsonschema/src/keywords/one_of.rs b/crates/jsonschema/src/keywords/one_of.rs similarity index 100% rename from jsonschema/src/keywords/one_of.rs rename to crates/jsonschema/src/keywords/one_of.rs diff --git a/jsonschema/src/keywords/pattern.rs b/crates/jsonschema/src/keywords/pattern.rs similarity index 100% rename from jsonschema/src/keywords/pattern.rs rename to crates/jsonschema/src/keywords/pattern.rs diff --git a/jsonschema/src/keywords/pattern_properties.rs b/crates/jsonschema/src/keywords/pattern_properties.rs similarity index 100% rename from jsonschema/src/keywords/pattern_properties.rs rename to crates/jsonschema/src/keywords/pattern_properties.rs diff --git a/jsonschema/src/keywords/prefix_items.rs b/crates/jsonschema/src/keywords/prefix_items.rs similarity index 99% rename from jsonschema/src/keywords/prefix_items.rs rename to crates/jsonschema/src/keywords/prefix_items.rs index 74a58bc7..7f18beb1 100644 --- a/jsonschema/src/keywords/prefix_items.rs +++ b/crates/jsonschema/src/keywords/prefix_items.rs @@ -125,7 +125,7 @@ pub(crate) fn compile<'a>( } } -#[cfg(all(test, feature = "draft202012"))] +#[cfg(test)] mod tests { use crate::{compilation::JSONSchema, tests_util}; use serde_json::{json, Value}; diff --git a/jsonschema/src/keywords/properties.rs b/crates/jsonschema/src/keywords/properties.rs similarity index 100% rename from jsonschema/src/keywords/properties.rs rename to crates/jsonschema/src/keywords/properties.rs diff --git a/jsonschema/src/keywords/property_names.rs b/crates/jsonschema/src/keywords/property_names.rs similarity index 100% rename from jsonschema/src/keywords/property_names.rs rename to crates/jsonschema/src/keywords/property_names.rs diff --git a/jsonschema/src/keywords/ref_.rs b/crates/jsonschema/src/keywords/ref_.rs similarity index 69% rename from jsonschema/src/keywords/ref_.rs rename to crates/jsonschema/src/keywords/ref_.rs index 6616220e..9e3a3bf4 100644 --- a/jsonschema/src/keywords/ref_.rs +++ b/crates/jsonschema/src/keywords/ref_.rs @@ -146,118 +146,15 @@ pub(crate) fn compile<'a>( } pub(crate) const fn supports_adjacent_validation(draft: Draft) -> bool { - match draft { - #[cfg(feature = "draft201909")] - Draft::Draft201909 => true, - #[cfg(feature = "draft202012")] - Draft::Draft202012 => true, - _ => false, - } + matches!(draft, Draft::Draft201909 | Draft::Draft202012) } #[cfg(test)] mod tests { - use crate::{tests_util, Draft, JSONSchema}; + use crate::{tests_util, JSONSchema}; use serde_json::{json, Value}; use test_case::test_case; - #[test_case( - &json!({ - "$ref": "http://json-schema.org/draft-04/schema#" - }), - &json!({ - "definitions": { - "foo": { - "type": "integer" - } - } - }) - )] - #[test_case( - &json!({ - "$ref": "http://json-schema.org/draft-06/schema#" - }), - &json!({ - "definitions": { - "foo": { - "type": "integer" - } - } - }) - )] - #[test_case( - &json!({ - "$ref": "http://json-schema.org/draft-07/schema#" - }), - &json!({ - "definitions": { - "foo": { - "type": "integer" - } - } - }) - )] - #[cfg_attr(feature = "draft201909", test_case( - &json!({ - "$schema": "https://json-schema.org/draft/2019-09/schema", - "$ref": "https://json-schema.org/draft/2019-09/schema" - }), - &json!({ - "$defs": { - "foo": { - "type": "integer" - } - } - }) - ))] - #[cfg_attr(feature = "draft202012", test_case( - &json!({ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$ref": "https://json-schema.org/draft/2020-12/schema" - }), - &json!({ - "$defs": { - "foo": { - "type": "integer" - } - } - }) - ))] - fn definition_against_metaschema(schema: &Value, instance: &Value) { - tests_util::is_valid(schema, instance); - } - - #[test] - fn test_ref_prevents_sibling_id_from_changing_the_base_uri() { - let schema = json!({ - "id": "http://localhost:1234/sibling_id/base/", - "definitions": { - "foo": { - "id": "http://localhost:1234/sibling_id/foo.json", - "type": "string" - }, - "base_foo": { - "$comment": "this canonical uri is http://localhost:1234/sibling_id/base/foo.json", - "id": "foo.json", - "type": "number" - } - }, - "allOf": [ - { - "$comment": "$ref resolves to http://localhost:1234/sibling_id/base/foo.json, not http://localhost:1234/sibling_id/foo.json", - "id": "http://localhost:1234/sibling_id/", - "$ref": "foo.json" - } - ] - }); - let instance = json!("a"); - let compiled = JSONSchema::options() - .with_draft(Draft::Draft4) - .compile(&schema) - .expect("Invalid schema"); - tests_util::is_not_valid_with(&compiled, &instance); - } - #[test_case( &json!({ "properties": { diff --git a/jsonschema/src/keywords/required.rs b/crates/jsonschema/src/keywords/required.rs similarity index 100% rename from jsonschema/src/keywords/required.rs rename to crates/jsonschema/src/keywords/required.rs diff --git a/jsonschema/src/keywords/type_.rs b/crates/jsonschema/src/keywords/type_.rs similarity index 100% rename from jsonschema/src/keywords/type_.rs rename to crates/jsonschema/src/keywords/type_.rs diff --git a/jsonschema/src/keywords/unevaluated_properties.rs b/crates/jsonschema/src/keywords/unevaluated_properties.rs similarity index 98% rename from jsonschema/src/keywords/unevaluated_properties.rs rename to crates/jsonschema/src/keywords/unevaluated_properties.rs index 5abf5b06..5eb983a0 100644 --- a/jsonschema/src/keywords/unevaluated_properties.rs +++ b/crates/jsonschema/src/keywords/unevaluated_properties.rs @@ -523,7 +523,7 @@ impl PatternSubvalidator { } } - had_match.then(|| true) + had_match.then_some(true) } fn validate_property<'instance>( @@ -564,7 +564,7 @@ impl PatternSubvalidator { } } - had_match.then(|| output) + had_match.then_some(output) } } @@ -683,7 +683,7 @@ impl SubschemaSubvalidator { // the property must be evaluated by it. SubschemaBehavior::Any => mapped .filter_map(|(property_result, instance_valid)| { - instance_valid.then(|| property_result).flatten() + instance_valid.then_some(property_result).flatten() }) .find(|x| *x), } @@ -762,7 +762,7 @@ impl SubschemaSubvalidator { .filter_map(|(property_errors, instance_errors)| { instance_errors .is_empty() - .then(|| property_errors) + .then_some(property_errors) .flatten() }) .filter(|x| x.is_empty()) @@ -841,7 +841,7 @@ impl SubschemaSubvalidator { .filter_map(|(property_output, instance_output)| { instance_output .is_valid() - .then(|| property_output) + .then_some(property_output) .flatten() }) .find(|x| x.is_valid()), @@ -1146,7 +1146,7 @@ impl DependentSchemaSubvalidator { .iter() .find_map(|(dependent_property_name, node)| { value_has_object_key(instance, dependent_property_name) - .then(|| node) + .then_some(node) .and_then(|node| { node.is_valid_property(instance, property_instance, property_name) }) @@ -1165,7 +1165,7 @@ impl DependentSchemaSubvalidator { .iter() .find_map(|(dependent_property_name, node)| { value_has_object_key(instance, dependent_property_name) - .then(|| node) + .then_some(node) .and_then(|node| { node.validate_property( instance, @@ -1190,7 +1190,7 @@ impl DependentSchemaSubvalidator { .iter() .find_map(|(dependent_property_name, node)| { value_has_object_key(instance, dependent_property_name) - .then(|| node) + .then_some(node) .and_then(|node| { node.apply_property( instance, @@ -1352,25 +1352,10 @@ mod tests { use crate::{tests_util, Draft}; use serde_json::json; - #[cfg(all(feature = "draft201909", not(feature = "draft202012")))] - const fn get_draft_version() -> Draft { - Draft::Draft201909 - } - - #[cfg(all(feature = "draft202012", not(feature = "draft201909")))] - const fn get_draft_version() -> Draft { - Draft::Draft202012 - } - - #[cfg(all(feature = "draft201909", feature = "draft202012"))] - const fn get_draft_version() -> Draft { - Draft::Draft202012 - } - #[test] fn one_of() { tests_util::is_valid_with_draft( - get_draft_version(), + Draft::Draft202012, &json!({ "oneOf": [ { "properties": { "foo": { "const": "bar" } } }, @@ -1385,7 +1370,7 @@ mod tests { #[test] fn any_of() { tests_util::is_valid_with_draft( - get_draft_version(), + Draft::Draft202012, &json!({ "anyOf": [ { "properties": { "foo": { "minLength": 10 } } }, @@ -1400,7 +1385,7 @@ mod tests { #[test] fn all_of() { tests_util::is_not_valid_with_draft( - get_draft_version(), + Draft::Draft202012, &json!({ "allOf": [ { "properties": { "foo": { "type": "string" } } }, @@ -1434,13 +1419,13 @@ mod tests { }); tests_util::is_valid_with_draft( - get_draft_version(), + Draft::Draft202012, &schema, &json!({ "foo": "wee", "another": "thing" }), ); tests_util::is_not_valid_with_draft( - get_draft_version(), + Draft::Draft202012, &schema, &json!({ "foo": "wee", "another": false }), ); diff --git a/jsonschema/src/keywords/unique_items.rs b/crates/jsonschema/src/keywords/unique_items.rs similarity index 100% rename from jsonschema/src/keywords/unique_items.rs rename to crates/jsonschema/src/keywords/unique_items.rs diff --git a/crates/jsonschema/src/lib.rs b/crates/jsonschema/src/lib.rs new file mode 100644 index 00000000..3f094e91 --- /dev/null +++ b/crates/jsonschema/src/lib.rs @@ -0,0 +1,562 @@ +//! A high-performance JSON Schema validator for Rust. +//! +//! - 📚 Support for popular JSON Schema drafts +//! - 🔧 Custom keywords and format validators +//! - 🌐 Remote reference fetching (network/file) +//! - 🎨 `Basic` output style as per JSON Schema spec +//! +//! ## Supported drafts +//! +//! Compliance levels vary across drafts, with newer versions having some unimplemented keywords. +//! +//! - ![Draft 2020-12](https://img.shields.io/endpoint?url=https%3A%2F%2Fbowtie.report%2Fbadges%2Frust-jsonschema%2Fcompliance%2Fdraft2020-12.json) +//! - ![Draft 2019-09](https://img.shields.io/endpoint?url=https%3A%2F%2Fbowtie.report%2Fbadges%2Frust-jsonschema%2Fcompliance%2Fdraft2019-09.json) +//! - ![Draft 7](https://img.shields.io/endpoint?url=https%3A%2F%2Fbowtie.report%2Fbadges%2Frust-jsonschema%2Fcompliance%2Fdraft7.json) +//! - ![Draft 6](https://img.shields.io/endpoint?url=https%3A%2F%2Fbowtie.report%2Fbadges%2Frust-jsonschema%2Fcompliance%2Fdraft6.json) +//! - ![Draft 4](https://img.shields.io/endpoint?url=https%3A%2F%2Fbowtie.report%2Fbadges%2Frust-jsonschema%2Fcompliance%2Fdraft4.json) +//! +//! # Validation +//! +//! The `jsonschema` crate offers two main approaches to validation: one-off validation and reusable validators. +//! +//! ## One-off Validation +//! +//! For simple use cases where you need to validate an instance against a schema once, use the `is_valid` function: +//! +//! ```rust +//! use serde_json::json; +//! +//! let schema = json!({"type": "string"}); +//! let instance = json!("Hello, world!"); +//! +//! assert!(jsonschema::is_valid(&schema, &instance)); +//! ``` +//! +//! ## Reusable Validators +//! +//! For better performance, especially when validating multiple instances against the same schema, build a validator once and reuse it: +//! +//! ```rust +//! use serde_json::json; +//! +//! let schema = json!({"type": "string"}); +//! let validator = jsonschema::compile(&schema) +//! .expect("Invalid schema"); +//! +//! assert!(validator.is_valid(&json!("Hello, world!"))); +//! assert!(!validator.is_valid(&json!(42))); +//! +//! // Iterate over all errors +//! let instance = json!(42); +//! let result = validator.validate(&instance); +//! if let Err(errors) = result { +//! for error in errors { +//! eprintln!("Error: {}", error); +//! eprintln!("Location: {}", error.instance_path); +//! } +//! } +//! ``` +//! +//! # Configuration +//! +//! `jsonschema` provides a builder for configuration options via `JSONSchema::options()`. +//! +//! Here is how you can explicitly set the JSON Schema draft version: +//! +//! ```rust +//! use jsonschema::{JSONSchema, Draft}; +//! use serde_json::json; +//! +//! let schema = json!({"type": "string"}); +//! let validator = JSONSchema::options() +//! .with_draft(Draft::Draft7) +//! .compile(&schema) +//! .expect("Invalid schema"); +//! ``` +//! +//! For a complete list of configuration options and their usage, please refer to the [`CompilationOptions`] struct. +//! +//! # Reference Resolving +//! +//! By default, `jsonschema` resolves HTTP references using `reqwest` and file references from the local file system. +//! +//! To enable HTTPS support, add the `rustls-tls` feature to `reqwest` in your `Cargo.toml`: +//! +//! ```toml +//! reqwest = { version = "*", features = ["rustls-tls"] } +//! ``` +//! +//! You can disable the default behavior using crate features: +//! +//! - Disable HTTP resolving: `default-features = false, features = ["resolve-file"]` +//! - Disable file resolving: `default-features = false, features = ["resolve-http"]` +//! - Disable both: `default-features = false` +//! +//! You can implement a custom resolver to handle external references. Here's an example that uses a static map of schemas: +//! +//! ```rust +//! # fn main() -> Result<(), Box> { +//! use std::{collections::HashMap, sync::Arc}; +//! use anyhow::anyhow; +//! use jsonschema::{JSONSchema, SchemaResolver, SchemaResolverError}; +//! use serde_json::{json, Value}; +//! use url::Url; +//! +//! struct StaticSchemaResolver { +//! schemas: HashMap>, +//! } +//! +//! impl SchemaResolver for StaticSchemaResolver { +//! fn resolve( +//! &self, +//! _root_schema: &serde_json::Value, +//! url: &Url, +//! _original_reference: &str +//! ) -> Result, SchemaResolverError> { +//! self.schemas +//! .get(url.as_str()) +//! .cloned() +//! .ok_or_else(|| anyhow!("Schema not found: {}", url)) +//! } +//! } +//! +//! let mut schemas = HashMap::new(); +//! schemas.insert( +//! "https://example.com/person.json".to_string(), +//! Arc::new(json!({ +//! "type": "object", +//! "properties": { +//! "name": { "type": "string" }, +//! "age": { "type": "integer" } +//! }, +//! "required": ["name", "age"] +//! })), +//! ); +//! +//! let resolver = StaticSchemaResolver { schemas }; +//! +//! let schema = json!({ +//! "$ref": "https://example.com/person.json" +//! }); +//! +//! let validator = JSONSchema::options() +//! .with_resolver(resolver) +//! .compile(&schema) +//! .expect("Invalid schema"); +//! +//! assert!(validator.is_valid(&json!({ +//! "name": "Alice", +//! "age": 30 +//! }))); +//! +//! assert!(!validator.is_valid(&json!({ +//! "name": "Bob" +//! }))); +//! # Ok(()) +//! # } +//! ``` +//! # Output Styles +//! +//! `jsonschema` supports the `basic` output style as defined in JSON Schema Draft 2019-09. +//! This styles allow you to serialize validation results in a standardized format using `serde`. +//! +//! ```rust +//! # fn main() -> Result<(), Box> { +//! use jsonschema::BasicOutput; +//! use serde_json::json; +//! +//! let schema_json = json!({ +//! "title": "string value", +//! "type": "string" +//! }); +//! let instance = json!("some string"); +//! let schema = jsonschema::compile(&schema_json) +//! .expect("Invalid schema"); +//! +//! let output: BasicOutput = schema.apply(&instance).basic(); +//! let output_json = serde_json::to_value(output)?; +//! +//! assert_eq!( +//! output_json, +//! json!({ +//! "valid": true, +//! "annotations": [ +//! { +//! "keywordLocation": "", +//! "instanceLocation": "", +//! "annotations": { +//! "title": "string value" +//! } +//! } +//! ] +//! }) +//! ); +//! # Ok(()) +//! # } +//! ``` +//! +//! # Custom Keywords +//! +//! `jsonschema` allows you to extend its functionality by implementing custom validation logic through custom keywords. +//! This feature is particularly useful when you need to validate against domain-specific rules that aren't covered by the standard JSON Schema keywords. +//! +//! To implement a custom keyword, you need to: +//! 1. Create a struct that implements the `Keyword` trait +//! 2. Create a factory function or closure that produces instances of your custom keyword +//! 3. Register the custom keyword with the `JSONSchema` instance using the `with_keyword` method +//! +//! Here's a complete example: +//! +//! ```rust +//! use jsonschema::{ +//! paths::{JSONPointer, JsonPointerNode}, +//! ErrorIterator, JSONSchema, Keyword, ValidationError, +//! }; +//! use serde_json::{json, Map, Value}; +//! use std::iter::once; +//! +//! // Step 1: Implement the Keyword trait +//! struct EvenNumberValidator; +//! +//! impl Keyword for EvenNumberValidator { +//! fn validate<'instance>( +//! &self, +//! instance: &'instance Value, +//! instance_path: &JsonPointerNode, +//! ) -> ErrorIterator<'instance> { +//! if let Value::Number(n) = instance { +//! if n.as_u64().map_or(false, |n| n % 2 == 0) { +//! Box::new(None.into_iter()) +//! } else { +//! let error = ValidationError::custom( +//! JSONPointer::default(), +//! instance_path.into(), +//! instance, +//! "Number must be even", +//! ); +//! Box::new(once(error)) +//! } +//! } else { +//! let error = ValidationError::custom( +//! JSONPointer::default(), +//! instance_path.into(), +//! instance, +//! "Value must be a number", +//! ); +//! Box::new(once(error)) +//! } +//! } +//! +//! fn is_valid(&self, instance: &Value) -> bool { +//! instance.as_u64().map_or(false, |n| n % 2 == 0) +//! } +//! } +//! +//! // Step 2: Create a factory function +//! fn even_number_validator_factory<'a>( +//! _parent: &'a Map, +//! value: &'a Value, +//! _path: JSONPointer, +//! ) -> Result, ValidationError<'a>> { +//! // You can use the `value` parameter to configure your validator if needed +//! if value.as_bool() == Some(true) { +//! Ok(Box::new(EvenNumberValidator)) +//! } else { +//! Err(ValidationError::custom( +//! JSONPointer::default(), +//! JSONPointer::default(), +//! value, +//! "The 'even-number' keyword must be set to true", +//! )) +//! } +//! } +//! +//! // Step 3: Use the custom keyword +//! fn main() -> Result<(), Box> { +//! let schema = json!({"even-number": true, "type": "integer"}); +//! let validator = JSONSchema::options() +//! .with_keyword("even-number", even_number_validator_factory) +//! .compile(&schema) +//! .expect("Invalid schema"); +//! +//! assert!(validator.is_valid(&json!(2))); +//! assert!(!validator.is_valid(&json!(3))); +//! assert!(!validator.is_valid(&json!("not a number"))); +//! +//! Ok(()) +//! } +//! ``` +//! +//! In this example, we've created a custom `even-number` keyword that validates whether a number is even. +//! The `EvenNumberValidator` implements the actual validation logic, while the `even_number_validator_factory` +//! creates instances of the validator and allows for additional configuration based on the keyword's value in the schema. +//! +//! You can also use a closure instead of a factory function for simpler cases: +//! +//! ```rust +//! # use jsonschema::{ +//! # paths::{JSONPointer, JsonPointerNode}, +//! # ErrorIterator, JSONSchema, Keyword, ValidationError, +//! # }; +//! # use serde_json::{json, Map, Value}; +//! # use std::iter::once; +//! # +//! # struct EvenNumberValidator; +//! # +//! # impl Keyword for EvenNumberValidator { +//! # fn validate<'instance>( +//! # &self, +//! # instance: &'instance Value, +//! # instance_path: &JsonPointerNode, +//! # ) -> ErrorIterator<'instance> { +//! # Box::new(None.into_iter()) +//! # } +//! # +//! # fn is_valid(&self, instance: &Value) -> bool { +//! # true +//! # } +//! # } +//! let schema = json!({"even-number": true, "type": "integer"}); +//! let validator = JSONSchema::options() +//! .with_keyword("even-number", |_, _, _| { +//! Ok(Box::new(EvenNumberValidator)) +//! }) +//! .compile(&schema) +//! .expect("Invalid schema"); +//! ``` +//! +//! # Custom Formats +//! +//! JSON Schema allows for format validation through the `format` keyword. While `jsonschema` +//! provides built-in validators for standard formats, you can also define custom format validators +//! for domain-specific string formats. +//! +//! To implement a custom format validator: +//! +//! 1. Define a function or a closure that takes a `&str` and returns a `bool`. +//! 2. Register the function with `JSONSchema::options().with_format()`. +//! +//! ```rust +//! use jsonschema::JSONSchema; +//! use serde_json::json; +//! +//! // Step 1: Define the custom format validator function +//! fn ends_with_42(s: &str) -> bool { +//! s.ends_with("42!") +//! } +//! +//! # fn main() -> Result<(), Box> { +//! // Step 2: Create a schema using the custom format +//! let schema = json!({ +//! "type": "string", +//! "format": "ends-with-42" +//! }); +//! +//! // Step 3: Compile the schema with the custom format +//! let validator = JSONSchema::options() +//! .with_format("ends-with-42", ends_with_42) +//! .with_format("ends-with-43", |s: &str| s.ends_with("43!")) +//! .compile(&schema) +//! .expect("Invalid schema"); +//! +//! // Step 4: Validate instances +//! assert!(validator.is_valid(&json!("Hello42!"))); +//! assert!(!validator.is_valid(&json!("Hello43!"))); +//! assert!(!validator.is_valid(&json!(42))); // Not a string +//! # Ok(()) +//! # } +//! ``` +//! +//! ### Notes on Custom Format Validators +//! +//! - Custom format validators are only called for string instances. +//! - Format validation can be disabled globally or per-draft using `CompilationOptions`. +//! Ensure format validation is enabled if you're using custom formats. +mod compilation; +mod content_encoding; +mod content_media_type; +pub mod error; +mod keywords; +pub mod output; +pub mod paths; +pub mod primitive_type; +pub(crate) mod properties; +mod resolver; +mod schema_node; +mod schemas; +mod validator; + +pub use compilation::{options::CompilationOptions, JSONSchema}; +pub use error::{ErrorIterator, ValidationError}; +pub use keywords::custom::Keyword; +pub use output::BasicOutput; +pub use resolver::{SchemaResolver, SchemaResolverError}; +pub use schemas::Draft; + +use serde_json::Value; + +/// A shortcut for validating `instance` against `schema`. Draft version is detected automatically. +/// ```rust +/// use jsonschema::is_valid; +/// use serde_json::json; +/// +/// let schema = json!({"maxLength": 5}); +/// let instance = json!("foo"); +/// assert!(is_valid(&schema, &instance)); +/// ``` +/// +/// This function panics if an invalid schema is passed. +#[must_use] +#[inline] +pub fn is_valid(schema: &Value, instance: &Value) -> bool { + let compiled = JSONSchema::compile(schema).expect("Invalid schema"); + compiled.is_valid(instance) +} + +/// Compile the input schema for faster validation. +pub fn compile(schema: &Value) -> Result { + JSONSchema::compile(schema) +} + +#[cfg(test)] +pub(crate) mod tests_util { + use super::JSONSchema; + use crate::ValidationError; + use serde_json::Value; + + pub(crate) fn is_not_valid_with(compiled: &JSONSchema, instance: &Value) { + assert!( + !compiled.is_valid(instance), + "{} should not be valid (via is_valid)", + instance + ); + assert!( + compiled.validate(instance).is_err(), + "{} should not be valid (via validate)", + instance + ); + assert!( + !compiled.apply(instance).basic().is_valid(), + "{} should not be valid (via apply)", + instance + ); + } + + pub(crate) fn is_not_valid(schema: &Value, instance: &Value) { + let compiled = JSONSchema::compile(schema).unwrap(); + is_not_valid_with(&compiled, instance) + } + + pub(crate) fn is_not_valid_with_draft(draft: crate::Draft, schema: &Value, instance: &Value) { + let compiled = JSONSchema::options() + .with_draft(draft) + .compile(schema) + .unwrap(); + is_not_valid_with(&compiled, instance) + } + + pub(crate) fn expect_errors(schema: &Value, instance: &Value, errors: &[&str]) { + assert_eq!( + JSONSchema::compile(schema) + .expect("Should be a valid schema") + .validate(instance) + .expect_err(format!("{} should not be valid", instance).as_str()) + .map(|e| e.to_string()) + .collect::>(), + errors + ) + } + + pub(crate) fn is_valid_with(compiled: &JSONSchema, instance: &Value) { + if let Err(mut errors) = compiled.validate(instance) { + let first = errors.next().expect("Errors iterator is empty"); + panic!( + "{} should be valid (via validate). Error: {} at {}", + instance, first, first.instance_path + ); + } + assert!( + compiled.is_valid(instance), + "{} should be valid (via is_valid)", + instance + ); + assert!( + compiled.apply(instance).basic().is_valid(), + "{} should be valid (via apply)", + instance + ); + } + + pub(crate) fn is_valid(schema: &Value, instance: &Value) { + let compiled = JSONSchema::compile(schema).unwrap(); + is_valid_with(&compiled, instance); + } + + pub(crate) fn is_valid_with_draft(draft: crate::Draft, schema: &Value, instance: &Value) { + let compiled = JSONSchema::options() + .with_draft(draft) + .compile(schema) + .unwrap(); + is_valid_with(&compiled, instance) + } + + pub(crate) fn validate(schema: &Value, instance: &Value) -> ValidationError<'static> { + let compiled = JSONSchema::compile(schema).unwrap(); + let err = compiled + .validate(instance) + .expect_err("Should be an error") + .next() + .expect("Should be an error") + .into_owned(); + err + } + + pub(crate) fn assert_schema_path(schema: &Value, instance: &Value, expected: &str) { + let error = validate(schema, instance); + assert_eq!(error.schema_path.to_string(), expected) + } + + pub(crate) fn assert_schema_paths(schema: &Value, instance: &Value, expected: &[&str]) { + let compiled = JSONSchema::compile(schema).unwrap(); + let errors = compiled.validate(instance).expect_err("Should be an error"); + for (error, schema_path) in errors.zip(expected) { + assert_eq!(error.schema_path.to_string(), *schema_path) + } + } +} + +#[cfg(test)] +mod tests { + use super::{is_valid, Draft, JSONSchema}; + use serde_json::json; + use test_case::test_case; + + #[test] + fn test_is_valid() { + let schema = json!({"minLength": 5}); + let valid = json!("foobar"); + let invalid = json!("foo"); + assert!(is_valid(&schema, &valid)); + assert!(!is_valid(&schema, &invalid)); + } + + #[test_case(Draft::Draft4)] + #[test_case(Draft::Draft6)] + #[test_case(Draft::Draft7)] + fn meta_schemas(draft: Draft) { + // See GH-258 + for schema in [json!({"enum": [0, 0.0]}), json!({"enum": []})] { + assert!(JSONSchema::options() + .with_draft(draft) + .compile(&schema) + .is_ok()) + } + } + + #[test] + fn incomplete_escape_in_pattern() { + // See GH-253 + let schema = json!({"pattern": "\\u"}); + assert!(JSONSchema::compile(&schema).is_err()) + } +} diff --git a/jsonschema/src/output.rs b/crates/jsonschema/src/output.rs similarity index 100% rename from jsonschema/src/output.rs rename to crates/jsonschema/src/output.rs diff --git a/jsonschema/src/paths.rs b/crates/jsonschema/src/paths.rs similarity index 100% rename from jsonschema/src/paths.rs rename to crates/jsonschema/src/paths.rs diff --git a/jsonschema/src/primitive_type.rs b/crates/jsonschema/src/primitive_type.rs similarity index 100% rename from jsonschema/src/primitive_type.rs rename to crates/jsonschema/src/primitive_type.rs diff --git a/jsonschema/src/properties.rs b/crates/jsonschema/src/properties.rs similarity index 100% rename from jsonschema/src/properties.rs rename to crates/jsonschema/src/properties.rs diff --git a/jsonschema/src/resolver.rs b/crates/jsonschema/src/resolver.rs similarity index 100% rename from jsonschema/src/resolver.rs rename to crates/jsonschema/src/resolver.rs diff --git a/jsonschema/src/schema_node.rs b/crates/jsonschema/src/schema_node.rs similarity index 100% rename from jsonschema/src/schema_node.rs rename to crates/jsonschema/src/schema_node.rs diff --git a/jsonschema/src/schemas.rs b/crates/jsonschema/src/schemas.rs similarity index 53% rename from jsonschema/src/schemas.rs rename to crates/jsonschema/src/schemas.rs index 7e3302d8..4d2b0265 100644 --- a/jsonschema/src/schemas.rs +++ b/crates/jsonschema/src/schemas.rs @@ -3,40 +3,24 @@ use serde_json::{Map, Value}; /// JSON Schema Draft version #[non_exhaustive] -#[derive(Debug, PartialEq, Copy, Clone, Hash, Eq)] +#[derive(Debug, Default, PartialEq, Copy, Clone, Hash, Eq)] pub enum Draft { /// JSON Schema Draft 4 Draft4, /// JSON Schema Draft 6 Draft6, /// JSON Schema Draft 7 + #[default] Draft7, - #[cfg(feature = "draft201909")] /// JSON Schema Draft 2019-09 Draft201909, - - #[cfg(feature = "draft202012")] /// JSON Schema Draft 2020-12 Draft202012, } -impl Default for Draft { - fn default() -> Self { - Draft::Draft7 - } -} - impl Draft { pub(crate) const fn validate_formats_by_default(self) -> bool { - match self { - Draft::Draft4 | Draft::Draft6 | Draft::Draft7 => true, - #[cfg(all(feature = "draft201909", feature = "draft202012"))] - Draft::Draft201909 | Draft::Draft202012 => false, - #[cfg(all(feature = "draft201909", not(feature = "draft202012")))] - Draft::Draft201909 => false, - #[cfg(all(feature = "draft202012", not(feature = "draft201909")))] - Draft::Draft202012 => false, - } + matches!(self, Draft::Draft4 | Draft::Draft6 | Draft::Draft7) } } @@ -57,88 +41,62 @@ impl Draft { "anyOf" => Some(keywords::any_of::compile), "const" => match self { Draft::Draft4 => None, - Draft::Draft6 | Draft::Draft7 => Some(keywords::const_::compile), - #[cfg(feature = "draft201909")] - Draft::Draft201909 => Some(keywords::const_::compile), - #[cfg(feature = "draft202012")] - Draft::Draft202012 => Some(keywords::const_::compile), + Draft::Draft6 | Draft::Draft7 | Draft::Draft201909 | Draft::Draft202012 => { + Some(keywords::const_::compile) + } }, "contains" => match self { Draft::Draft4 => None, - Draft::Draft6 | Draft::Draft7 => Some(keywords::contains::compile), - #[cfg(feature = "draft201909")] - Draft::Draft201909 => Some(keywords::contains::compile), - #[cfg(feature = "draft202012")] - Draft::Draft202012 => Some(keywords::contains::compile), + Draft::Draft6 | Draft::Draft7 | Draft::Draft201909 | Draft::Draft202012 => { + Some(keywords::contains::compile) + } }, "contentMediaType" => match self { Draft::Draft7 | Draft::Draft6 => Some(keywords::content::compile_media_type), - Draft::Draft4 => None, - #[cfg(feature = "draft201909")] - // Should be collected as an annotation - Draft::Draft201909 => None, - #[cfg(feature = "draft202012")] - Draft::Draft202012 => None, + Draft::Draft4 | Draft::Draft201909 | Draft::Draft202012 => None, }, "contentEncoding" => match self { Draft::Draft7 | Draft::Draft6 => Some(keywords::content::compile_content_encoding), - Draft::Draft4 => None, - #[cfg(feature = "draft201909")] - // Should be collected as an annotation - Draft::Draft201909 => None, - #[cfg(feature = "draft202012")] - Draft::Draft202012 => None, + Draft::Draft4 | Draft::Draft201909 | Draft::Draft202012 => None, }, "dependencies" => Some(keywords::dependencies::compile), - #[cfg(any(feature = "draft201909", feature = "draft202012"))] "dependentRequired" => Some(keywords::dependencies::compile_dependent_required), - #[cfg(any(feature = "draft201909", feature = "draft202012"))] "dependentSchemas" => Some(keywords::dependencies::compile_dependent_schemas), "enum" => Some(keywords::enum_::compile), "exclusiveMaximum" => match self { - Draft::Draft7 | Draft::Draft6 => Some(keywords::exclusive_maximum::compile), Draft::Draft4 => None, - #[cfg(feature = "draft201909")] - Draft::Draft201909 => Some(keywords::exclusive_maximum::compile), - #[cfg(feature = "draft202012")] - Draft::Draft202012 => Some(keywords::exclusive_maximum::compile), + Draft::Draft7 | Draft::Draft6 | Draft::Draft201909 | Draft::Draft202012 => { + Some(keywords::exclusive_maximum::compile) + } }, "exclusiveMinimum" => match self { - Draft::Draft7 | Draft::Draft6 => Some(keywords::exclusive_minimum::compile), Draft::Draft4 => None, - #[cfg(feature = "draft201909")] - Draft::Draft201909 => Some(keywords::exclusive_minimum::compile), - #[cfg(feature = "draft202012")] - Draft::Draft202012 => Some(keywords::exclusive_minimum::compile), + Draft::Draft7 | Draft::Draft6 | Draft::Draft201909 | Draft::Draft202012 => { + Some(keywords::exclusive_minimum::compile) + } }, "format" => Some(keywords::format::compile), "if" => match self { - Draft::Draft7 => Some(keywords::if_::compile), Draft::Draft6 | Draft::Draft4 => None, - #[cfg(feature = "draft201909")] - Draft::Draft201909 => Some(keywords::if_::compile), - #[cfg(feature = "draft202012")] - Draft::Draft202012 => Some(keywords::if_::compile), + Draft::Draft7 | Draft::Draft201909 | Draft::Draft202012 => { + Some(keywords::if_::compile) + } }, "items" => Some(keywords::items::compile), "maximum" => match self { Draft::Draft4 => Some(keywords::legacy::maximum_draft_4::compile), - Draft::Draft6 | Draft::Draft7 => Some(keywords::maximum::compile), - #[cfg(feature = "draft201909")] - Draft::Draft201909 => Some(keywords::maximum::compile), - #[cfg(feature = "draft202012")] - Draft::Draft202012 => Some(keywords::maximum::compile), + Draft::Draft6 | Draft::Draft7 | Draft::Draft201909 | Draft::Draft202012 => { + Some(keywords::maximum::compile) + } }, "maxItems" => Some(keywords::max_items::compile), "maxLength" => Some(keywords::max_length::compile), "maxProperties" => Some(keywords::max_properties::compile), "minimum" => match self { Draft::Draft4 => Some(keywords::legacy::minimum_draft_4::compile), - Draft::Draft6 | Draft::Draft7 => Some(keywords::minimum::compile), - #[cfg(feature = "draft201909")] - Draft::Draft201909 => Some(keywords::minimum::compile), - #[cfg(feature = "draft202012")] - Draft::Draft202012 => Some(keywords::minimum::compile), + Draft::Draft6 | Draft::Draft7 | Draft::Draft201909 | Draft::Draft202012 => { + Some(keywords::minimum::compile) + } }, "minItems" => Some(keywords::min_items::compile), "minLength" => Some(keywords::min_length::compile), @@ -148,31 +106,25 @@ impl Draft { "oneOf" => Some(keywords::one_of::compile), "pattern" => Some(keywords::pattern::compile), "patternProperties" => Some(keywords::pattern_properties::compile), - #[cfg(feature = "draft202012")] "prefixItems" => Some(keywords::prefix_items::compile), "properties" => Some(keywords::properties::compile), "propertyNames" => match self { Draft::Draft4 => None, - Draft::Draft6 | Draft::Draft7 => Some(keywords::property_names::compile), - #[cfg(feature = "draft201909")] - Draft::Draft201909 => Some(keywords::property_names::compile), - #[cfg(feature = "draft202012")] - Draft::Draft202012 => Some(keywords::property_names::compile), + Draft::Draft6 | Draft::Draft7 | Draft::Draft201909 | Draft::Draft202012 => { + Some(keywords::property_names::compile) + } }, "required" => Some(keywords::required::compile), "type" => match self { Draft::Draft4 => Some(keywords::legacy::type_draft_4::compile), - Draft::Draft6 | Draft::Draft7 => Some(keywords::type_::compile), - #[cfg(feature = "draft201909")] - Draft::Draft201909 => Some(keywords::type_::compile), - #[cfg(feature = "draft202012")] - Draft::Draft202012 => Some(keywords::type_::compile), + Draft::Draft6 | Draft::Draft7 | Draft::Draft201909 | Draft::Draft202012 => { + Some(keywords::type_::compile) + } }, "unevaluatedProperties" => match self { - #[cfg(feature = "draft201909")] - Draft::Draft201909 => Some(keywords::unevaluated_properties::compile), - #[cfg(feature = "draft202012")] - Draft::Draft202012 => Some(keywords::unevaluated_properties::compile), + Draft::Draft201909 | Draft::Draft202012 => { + Some(keywords::unevaluated_properties::compile) + } _ => None, }, "uniqueItems" => Some(keywords::unique_items::compile), @@ -185,9 +137,7 @@ impl Draft { #[inline] pub(crate) fn draft_from_url(url: &str) -> Option { match url.trim_end_matches('#') { - #[cfg(feature = "draft202012")] "https://json-schema.org/draft/2020-12/schema" => Some(Draft::Draft202012), - #[cfg(feature = "draft201909")] "https://json-schema.org/draft/2019-09/schema" => Some(Draft::Draft201909), "http://json-schema.org/draft-07/schema" => Some(Draft::Draft7), "http://json-schema.org/draft-06/schema" => Some(Draft::Draft6), @@ -225,8 +175,8 @@ mod tests { use serde_json::{json, Value}; use test_case::test_case; - #[cfg_attr(feature = "draft201909", test_case(&json!({"$schema": "https://json-schema.org/draft/2019-09/schema"}), Some(Draft::Draft201909)))] - #[cfg_attr(feature = "draft202012", test_case(&json!({"$schema": "https://json-schema.org/draft/2020-12/schema"}), Some(Draft::Draft202012)))] + #[test_case(&json!({"$schema": "https://json-schema.org/draft/2019-09/schema"}), Some(Draft::Draft201909))] + #[test_case(&json!({"$schema": "https://json-schema.org/draft/2020-12/schema"}), Some(Draft::Draft202012))] #[test_case(&json!({"$schema": "http://json-schema.org/draft-07/schema#"}), Some(Draft::Draft7))] #[test_case(&json!({"$schema": "http://json-schema.org/draft-06/schema#"}), Some(Draft::Draft6))] #[test_case(&json!({"$schema": "http://json-schema.org/draft-04/schema#"}), Some(Draft::Draft4))] diff --git a/jsonschema/src/validator.rs b/crates/jsonschema/src/validator.rs similarity index 100% rename from jsonschema/src/validator.rs rename to crates/jsonschema/src/validator.rs diff --git a/crates/jsonschema/tests/draft7_instance_paths.json b/crates/jsonschema/tests/draft7_instance_paths.json new file mode 100644 index 00000000..996bbc0b --- /dev/null +++ b/crates/jsonschema/tests/draft7_instance_paths.json @@ -0,0 +1,2257 @@ +{ + "additionalItems.json": [ + { + "suite_id": 0, + "tests": [ + { + "id": 1, + "instance_path": [ + "3" + ] + } + ] + }, + { + "suite_id": 3, + "tests": [ + { + "id": 4, + "instance_path": [] + } + ] + }, + { + "suite_id": 7, + "tests": [ + { + "id": 0, + "instance_path": [ + "1" + ] + } + ] + }, + { + "suite_id": 8, + "tests": [ + { + "id": 1, + "instance_path": [ + "1" + ] + } + ] + } + ], + "additionalProperties.json": [ + { + "suite_id": 0, + "tests": [ + { + "id": 1, + "instance_path": [] + } + ] + }, + { + "suite_id": 1, + "tests": [ + { + "id": 1, + "instance_path": [] + } + ] + }, + { + "suite_id": 2, + "tests": [ + { + "id": 2, + "instance_path": [ + "quux" + ] + } + ] + }, + { + "suite_id": 3, + "tests": [ + { + "id": 1, + "instance_path": [ + "foo" + ] + } + ] + }, + { + "suite_id": 5, + "tests": [ + { + "id": 0, + "instance_path": [ + "foo" + ] + } + ] + } + ], + "allOf.json": [ + { + "suite_id": 0, + "tests": [ + { + "id": 1, + "instance_path": [] + }, + { + "id": 2, + "instance_path": [] + }, + { + "id": 3, + "instance_path": [ + "bar" + ] + } + ] + }, + { + "suite_id": 1, + "tests": [ + { + "id": 1, + "instance_path": [] + }, + { + "id": 2, + "instance_path": [] + }, + { + "id": 3, + "instance_path": [] + }, + { + "id": 4, + "instance_path": [] + } + ] + }, + { + "suite_id": 2, + "tests": [ + { + "id": 1, + "instance_path": [] + } + ] + }, + { + "suite_id": 4, + "tests": [ + { + "id": 0, + "instance_path": [] + } + ] + }, + { + "suite_id": 5, + "tests": [ + { + "id": 0, + "instance_path": [] + } + ] + }, + { + "suite_id": 8, + "tests": [ + { + "id": 1, + "instance_path": [] + } + ] + }, + { + "suite_id": 9, + "tests": [ + { + "id": 1, + "instance_path": [] + } + ] + }, + { + "suite_id": 10, + "tests": [ + { + "id": 1, + "instance_path": [] + } + ] + }, + { + "suite_id": 11, + "tests": [ + { + "id": 0, + "instance_path": [] + }, + { + "id": 1, + "instance_path": [] + }, + { + "id": 2, + "instance_path": [] + }, + { + "id": 3, + "instance_path": [] + }, + { + "id": 4, + "instance_path": [] + }, + { + "id": 5, + "instance_path": [] + }, + { + "id": 6, + "instance_path": [] + } + ] + } + ], + "anyOf.json": [ + { + "suite_id": 0, + "tests": [ + { + "id": 3, + "instance_path": [] + } + ] + }, + { + "suite_id": 1, + "tests": [ + { + "id": 0, + "instance_path": [] + }, + { + "id": 2, + "instance_path": [] + } + ] + }, + { + "suite_id": 4, + "tests": [ + { + "id": 0, + "instance_path": [] + } + ] + }, + { + "suite_id": 5, + "tests": [ + { + "id": 3, + "instance_path": [] + } + ] + }, + { + "suite_id": 7, + "tests": [ + { + "id": 1, + "instance_path": [] + } + ] + } + ], + "boolean_schema.json": [ + { + "suite_id": 1, + "tests": [ + { + "id": 0, + "instance_path": [] + }, + { + "id": 1, + "instance_path": [] + }, + { + "id": 2, + "instance_path": [] + }, + { + "id": 3, + "instance_path": [] + }, + { + "id": 4, + "instance_path": [] + }, + { + "id": 5, + "instance_path": [] + }, + { + "id": 6, + "instance_path": [] + }, + { + "id": 7, + "instance_path": [] + }, + { + "id": 8, + "instance_path": [] + } + ] + } + ], + "const.json": [ + { + "suite_id": 0, + "tests": [ + { + "id": 1, + "instance_path": [] + }, + { + "id": 2, + "instance_path": [] + } + ] + }, + { + "suite_id": 1, + "tests": [ + { + "id": 2, + "instance_path": [] + }, + { + "id": 3, + "instance_path": [] + } + ] + }, + { + "suite_id": 2, + "tests": [ + { + "id": 1, + "instance_path": [] + }, + { + "id": 2, + "instance_path": [] + } + ] + }, + { + "suite_id": 3, + "tests": [ + { + "id": 1, + "instance_path": [] + } + ] + }, + { + "suite_id": 4, + "tests": [ + { + "id": 1, + "instance_path": [] + }, + { + "id": 2, + "instance_path": [] + } + ] + }, + { + "suite_id": 5, + "tests": [ + { + "id": 1, + "instance_path": [] + }, + { + "id": 2, + "instance_path": [] + } + ] + }, + { + "suite_id": 6, + "tests": [ + { + "id": 1, + "instance_path": [] + }, + { + "id": 2, + "instance_path": [] + } + ] + }, + { + "suite_id": 7, + "tests": [ + { + "id": 1, + "instance_path": [] + }, + { + "id": 2, + "instance_path": [] + } + ] + }, + { + "suite_id": 8, + "tests": [ + { + "id": 1, + "instance_path": [] + }, + { + "id": 2, + "instance_path": [] + } + ] + }, + { + "suite_id": 9, + "tests": [ + { + "id": 1, + "instance_path": [] + }, + { + "id": 2, + "instance_path": [] + } + ] + }, + { + "suite_id": 10, + "tests": [ + { + "id": 0, + "instance_path": [] + }, + { + "id": 3, + "instance_path": [] + }, + { + "id": 4, + "instance_path": [] + }, + { + "id": 5, + "instance_path": [] + } + ] + }, + { + "suite_id": 11, + "tests": [ + { + "id": 0, + "instance_path": [] + } + ] + }, + { + "suite_id": 12, + "tests": [ + { + "id": 1, + "instance_path": [] + }, + { + "id": 3, + "instance_path": [] + }, + { + "id": 4, + "instance_path": [] + } + ] + }, + { + "suite_id": 13, + "tests": [ + { + "id": 1, + "instance_path": [] + }, + { + "id": 3, + "instance_path": [] + } + ] + }, + { + "suite_id": 14, + "tests": [ + { + "id": 1, + "instance_path": [] + } + ] + } + ], + "contains.json": [ + { + "suite_id": 0, + "tests": [ + { + "id": 3, + "instance_path": [] + }, + { + "id": 4, + "instance_path": [] + } + ] + }, + { + "suite_id": 1, + "tests": [ + { + "id": 2, + "instance_path": [] + } + ] + }, + { + "suite_id": 2, + "tests": [ + { + "id": 1, + "instance_path": [] + } + ] + }, + { + "suite_id": 3, + "tests": [ + { + "id": 0, + "instance_path": [] + }, + { + "id": 1, + "instance_path": [] + } + ] + }, + { + "suite_id": 4, + "tests": [ + { + "id": 0, + "instance_path": [] + }, + { + "id": 1, + "instance_path": [ + "0" + ] + }, + { + "id": 3, + "instance_path": [] + } + ] + } + ], + "default.json": [ + { + "suite_id": 2, + "tests": [ + { + "id": 1, + "instance_path": [ + "alpha" + ] + } + ] + } + ], + "definitions.json": [ + { + "suite_id": 0, + "tests": [ + { + "id": 1, + "instance_path": [ + "definitions", + "foo", + "type" + ] + } + ] + } + ], + "dependencies.json": [ + { + "suite_id": 0, + "tests": [ + { + "id": 3, + "instance_path": [] + } + ] + }, + { + "suite_id": 2, + "tests": [ + { + "id": 3, + "instance_path": [] + }, + { + "id": 4, + "instance_path": [] + }, + { + "id": 5, + "instance_path": [] + } + ] + }, + { + "suite_id": 3, + "tests": [ + { + "id": 2, + "instance_path": [ + "foo" + ] + }, + { + "id": 3, + "instance_path": [ + "bar" + ] + }, + { + "id": 4, + "instance_path": [ + "bar" + ] + } + ] + }, + { + "suite_id": 4, + "tests": [ + { + "id": 1, + "instance_path": [] + }, + { + "id": 2, + "instance_path": [] + } + ] + }, + { + "suite_id": 5, + "tests": [ + { + "id": 3, + "instance_path": [] + }, + { + "id": 4, + "instance_path": [] + }, + { + "id": 5, + "instance_path": [] + }, + { + "id": 6, + "instance_path": [] + } + ] + } + ], + "enum.json": [ + { + "suite_id": 0, + "tests": [ + { + "id": 1, + "instance_path": [] + } + ] + }, + { + "suite_id": 1, + "tests": [ + { + "id": 1, + "instance_path": [] + }, + { + "id": 2, + "instance_path": [] + }, + { + "id": 4, + "instance_path": [] + } + ] + }, + { + "suite_id": 2, + "tests": [ + { + "id": 2, + "instance_path": [] + } + ] + }, + { + "suite_id": 3, + "tests": [ + { + "id": 1, + "instance_path": [ + "foo" + ] + }, + { + "id": 2, + "instance_path": [ + "bar" + ] + }, + { + "id": 4, + "instance_path": [] + }, + { + "id": 5, + "instance_path": [] + } + ] + }, + { + "suite_id": 4, + "tests": [ + { + "id": 2, + "instance_path": [] + } + ] + }, + { + "suite_id": 5, + "tests": [ + { + "id": 1, + "instance_path": [] + }, + { + "id": 2, + "instance_path": [] + } + ] + }, + { + "suite_id": 6, + "tests": [ + { + "id": 1, + "instance_path": [] + }, + { + "id": 2, + "instance_path": [] + } + ] + }, + { + "suite_id": 9, + "tests": [ + { + "id": 0, + "instance_path": [] + } + ] + }, + { + "suite_id": 11, + "tests": [ + { + "id": 0, + "instance_path": [] + } + ] + }, + { + "suite_id": 13, + "tests": [ + { + "id": 1, + "instance_path": [] + } + ] + } + ], + "exclusiveMaximum.json": [ + { + "suite_id": 0, + "tests": [ + { + "id": 1, + "instance_path": [] + }, + { + "id": 2, + "instance_path": [] + } + ] + } + ], + "exclusiveMinimum.json": [ + { + "suite_id": 0, + "tests": [ + { + "id": 1, + "instance_path": [] + }, + { + "id": 2, + "instance_path": [] + } + ] + } + ], + "if-then-else.json": [ + { + "suite_id": 3, + "tests": [ + { + "id": 1, + "instance_path": [] + } + ] + }, + { + "suite_id": 4, + "tests": [ + { + "id": 2, + "instance_path": [] + } + ] + }, + { + "suite_id": 5, + "tests": [ + { + "id": 1, + "instance_path": [] + }, + { + "id": 3, + "instance_path": [] + } + ] + }, + { + "suite_id": 7, + "tests": [ + { + "id": 1, + "instance_path": [] + } + ] + }, + { + "suite_id": 8, + "tests": [ + { + "id": 0, + "instance_path": [] + } + ] + }, + { + "suite_id": 9, + "tests": [ + { + "id": 2, + "instance_path": [] + }, + { + "id": 3, + "instance_path": [] + } + ] + } + ], + "infinite-loop-detection.json": [ + { + "suite_id": 0, + "tests": [ + { + "id": 1, + "instance_path": [ + "foo" + ] + } + ] + } + ], + "items.json": [ + { + "suite_id": 0, + "tests": [ + { + "id": 1, + "instance_path": [ + "1" + ] + } + ] + }, + { + "suite_id": 1, + "tests": [ + { + "id": 1, + "instance_path": [ + "0" + ] + } + ] + }, + { + "suite_id": 3, + "tests": [ + { + "id": 0, + "instance_path": [ + "0" + ] + } + ] + }, + { + "suite_id": 4, + "tests": [ + { + "id": 1, + "instance_path": [ + "1" + ] + } + ] + }, + { + "suite_id": 5, + "tests": [ + { + "id": 1, + "instance_path": [] + }, + { + "id": 2, + "instance_path": [ + "0" + ] + }, + { + "id": 3, + "instance_path": [ + "0" + ] + }, + { + "id": 4, + "instance_path": [ + "0", + "0" + ] + } + ] + }, + { + "suite_id": 6, + "tests": [ + { + "id": 1, + "instance_path": [ + "0", + "0", + "0", + "0" + ] + }, + { + "id": 2, + "instance_path": [ + "0", + "0", + "0" + ] + } + ] + } + ], + "maxItems.json": [ + { + "suite_id": 0, + "tests": [ + { + "id": 2, + "instance_path": [] + } + ] + } + ], + "maxLength.json": [ + { + "suite_id": 0, + "tests": [ + { + "id": 2, + "instance_path": [] + } + ] + } + ], + "maxProperties.json": [ + { + "suite_id": 0, + "tests": [ + { + "id": 2, + "instance_path": [] + } + ] + }, + { + "suite_id": 1, + "tests": [ + { + "id": 1, + "instance_path": [] + } + ] + } + ], + "maximum.json": [ + { + "suite_id": 0, + "tests": [ + { + "id": 2, + "instance_path": [] + } + ] + }, + { + "suite_id": 1, + "tests": [ + { + "id": 3, + "instance_path": [] + } + ] + } + ], + "minItems.json": [ + { + "suite_id": 0, + "tests": [ + { + "id": 2, + "instance_path": [] + } + ] + } + ], + "minLength.json": [ + { + "suite_id": 0, + "tests": [ + { + "id": 2, + "instance_path": [] + }, + { + "id": 4, + "instance_path": [] + } + ] + } + ], + "minProperties.json": [ + { + "suite_id": 0, + "tests": [ + { + "id": 2, + "instance_path": [] + } + ] + } + ], + "minimum.json": [ + { + "suite_id": 0, + "tests": [ + { + "id": 2, + "instance_path": [] + } + ] + }, + { + "suite_id": 1, + "tests": [ + { + "id": 4, + "instance_path": [] + }, + { + "id": 5, + "instance_path": [] + } + ] + } + ], + "multipleOf.json": [ + { + "suite_id": 0, + "tests": [ + { + "id": 1, + "instance_path": [] + } + ] + }, + { + "suite_id": 1, + "tests": [ + { + "id": 2, + "instance_path": [] + } + ] + }, + { + "suite_id": 2, + "tests": [ + { + "id": 1, + "instance_path": [] + } + ] + }, + { + "suite_id": 3, + "tests": [ + { + "id": 0, + "instance_path": [] + } + ] + } + ], + "not.json": [ + { + "suite_id": 0, + "tests": [ + { + "id": 1, + "instance_path": [] + } + ] + }, + { + "suite_id": 1, + "tests": [ + { + "id": 1, + "instance_path": [] + }, + { + "id": 2, + "instance_path": [] + } + ] + }, + { + "suite_id": 2, + "tests": [ + { + "id": 2, + "instance_path": [] + } + ] + }, + { + "suite_id": 3, + "tests": [ + { + "id": 0, + "instance_path": [ + "foo" + ] + } + ] + }, + { + "suite_id": 4, + "tests": [ + { + "id": 0, + "instance_path": [] + } + ] + } + ], + "oneOf.json": [ + { + "suite_id": 0, + "tests": [ + { + "id": 2, + "instance_path": [] + }, + { + "id": 3, + "instance_path": [] + } + ] + }, + { + "suite_id": 1, + "tests": [ + { + "id": 0, + "instance_path": [] + }, + { + "id": 2, + "instance_path": [] + } + ] + }, + { + "suite_id": 2, + "tests": [ + { + "id": 0, + "instance_path": [] + } + ] + }, + { + "suite_id": 4, + "tests": [ + { + "id": 0, + "instance_path": [] + } + ] + }, + { + "suite_id": 5, + "tests": [ + { + "id": 0, + "instance_path": [] + } + ] + }, + { + "suite_id": 6, + "tests": [ + { + "id": 2, + "instance_path": [] + }, + { + "id": 3, + "instance_path": [] + } + ] + }, + { + "suite_id": 7, + "tests": [ + { + "id": 1, + "instance_path": [] + } + ] + }, + { + "suite_id": 8, + "tests": [ + { + "id": 0, + "instance_path": [] + }, + { + "id": 3, + "instance_path": [] + } + ] + }, + { + "suite_id": 9, + "tests": [ + { + "id": 2, + "instance_path": [] + }, + { + "id": 3, + "instance_path": [] + } + ] + }, + { + "suite_id": 10, + "tests": [ + { + "id": 1, + "instance_path": [] + } + ] + } + ], + "optional/bignum.json": [ + { + "suite_id": 4, + "tests": [ + { + "id": 0, + "instance_path": [] + } + ] + }, + { + "suite_id": 6, + "tests": [ + { + "id": 0, + "instance_path": [] + } + ] + } + ], + "optional/content.json": [ + { + "suite_id": 0, + "tests": [ + { + "id": 1, + "instance_path": [] + } + ] + }, + { + "suite_id": 1, + "tests": [ + { + "id": 1, + "instance_path": [] + } + ] + }, + { + "suite_id": 2, + "tests": [ + { + "id": 1, + "instance_path": [] + }, + { + "id": 2, + "instance_path": [] + } + ] + } + ], + "optional/ecmascript-regex.json": [ + { + "suite_id": 0, + "tests": [ + { + "id": 0, + "instance_path": [] + } + ] + }, + { + "suite_id": 1, + "tests": [ + { + "id": 0, + "instance_path": [] + } + ] + }, + { + "suite_id": 2, + "tests": [ + { + "id": 0, + "instance_path": [] + } + ] + }, + { + "suite_id": 3, + "tests": [ + { + "id": 0, + "instance_path": [] + } + ] + }, + { + "suite_id": 4, + "tests": [ + { + "id": 1, + "instance_path": [] + }, + { + "id": 2, + "instance_path": [] + } + ] + }, + { + "suite_id": 5, + "tests": [ + { + "id": 0, + "instance_path": [] + } + ] + }, + { + "suite_id": 6, + "tests": [ + { + "id": 1, + "instance_path": [] + } + ] + }, + { + "suite_id": 7, + "tests": [ + { + "id": 0, + "instance_path": [] + } + ] + }, + { + "suite_id": 8, + "tests": [ + { + "id": 9, + "instance_path": [] + }, + { + "id": 10, + "instance_path": [] + } + ] + }, + { + "suite_id": 9, + "tests": [ + { + "id": 0, + "instance_path": [] + }, + { + "id": 1, + "instance_path": [] + }, + { + "id": 2, + "instance_path": [] + }, + { + "id": 3, + "instance_path": [] + }, + { + "id": 4, + "instance_path": [] + }, + { + "id": 5, + "instance_path": [] + }, + { + "id": 6, + "instance_path": [] + }, + { + "id": 7, + "instance_path": [] + }, + { + "id": 8, + "instance_path": [] + } + ] + } + ], + "pattern.json": [ + { + "suite_id": 0, + "tests": [ + { + "id": 1, + "instance_path": [] + } + ] + } + ], + "patternProperties.json": [ + { + "suite_id": 0, + "tests": [ + { + "id": 2, + "instance_path": [ + "foo" + ] + }, + { + "id": 3, + "instance_path": [ + "foo" + ] + } + ] + }, + { + "suite_id": 1, + "tests": [ + { + "id": 3, + "instance_path": [ + "a" + ] + }, + { + "id": 4, + "instance_path": [ + "aaaa" + ] + }, + { + "id": 5, + "instance_path": [ + "aaa" + ] + } + ] + }, + { + "suite_id": 2, + "tests": [ + { + "id": 1, + "instance_path": [ + "a31b" + ] + }, + { + "id": 3, + "instance_path": [ + "a_X_3" + ] + } + ] + }, + { + "suite_id": 3, + "tests": [ + { + "id": 1, + "instance_path": [ + "bar" + ] + }, + { + "id": 2, + "instance_path": [ + "bar" + ] + }, + { + "id": 3, + "instance_path": [ + "foobar" + ] + } + ] + } + ], + "properties.json": [ + { + "suite_id": 0, + "tests": [ + { + "id": 1, + "instance_path": [ + "bar" + ] + }, + { + "id": 2, + "instance_path": [ + "bar" + ] + } + ] + }, + { + "suite_id": 1, + "tests": [ + { + "id": 1, + "instance_path": [ + "foo" + ] + }, + { + "id": 2, + "instance_path": [ + "foo" + ] + }, + { + "id": 4, + "instance_path": [ + "fxo" + ] + }, + { + "id": 7, + "instance_path": [ + "quux" + ] + } + ] + }, + { + "suite_id": 2, + "tests": [ + { + "id": 2, + "instance_path": [ + "bar" + ] + }, + { + "id": 3, + "instance_path": [ + "bar" + ] + } + ] + }, + { + "suite_id": 3, + "tests": [ + { + "id": 1, + "instance_path": [ + "foo\tbar" + ] + } + ] + } + ], + "propertyNames.json": [ + { + "suite_id": 0, + "tests": [ + { + "id": 1, + "instance_path": [] + } + ] + }, + { + "suite_id": 3, + "tests": [ + { + "id": 0, + "instance_path": [] + } + ] + } + ], + "ref.json": [ + { + "suite_id": 0, + "tests": [ + { + "id": 2, + "instance_path": [] + }, + { + "id": 3, + "instance_path": [ + "foo" + ] + } + ] + }, + { + "suite_id": 1, + "tests": [ + { + "id": 1, + "instance_path": [ + "bar" + ] + } + ] + }, + { + "suite_id": 2, + "tests": [ + { + "id": 1, + "instance_path": [ + "1" + ] + } + ] + }, + { + "suite_id": 3, + "tests": [ + { + "id": 0, + "instance_path": [ + "slash" + ] + }, + { + "id": 1, + "instance_path": [ + "tilde" + ] + }, + { + "id": 2, + "instance_path": [ + "percent" + ] + } + ] + }, + { + "suite_id": 4, + "tests": [ + { + "id": 1, + "instance_path": [] + } + ] + }, + { + "suite_id": 5, + "tests": [ + { + "id": 2, + "instance_path": [ + "foo" + ] + } + ] + }, + { + "suite_id": 7, + "tests": [ + { + "id": 1, + "instance_path": [ + "minLength" + ] + } + ] + }, + { + "suite_id": 8, + "tests": [ + { + "id": 1, + "instance_path": [ + "$ref" + ] + } + ] + }, + { + "suite_id": 9, + "tests": [ + { + "id": 1, + "instance_path": [ + "$ref" + ] + } + ] + }, + { + "suite_id": 11, + "tests": [ + { + "id": 0, + "instance_path": [] + } + ] + }, + { + "suite_id": 12, + "tests": [ + { + "id": 1, + "instance_path": [ + "nodes", + "0", + "subtree", + "nodes", + "0", + "value" + ] + } + ] + }, + { + "suite_id": 13, + "tests": [ + { + "id": 1, + "instance_path": [ + "foo\"bar" + ] + } + ] + }, + { + "suite_id": 14, + "tests": [ + { + "id": 1, + "instance_path": [] + } + ] + }, + { + "suite_id": 15, + "tests": [ + { + "id": 1, + "instance_path": [] + } + ] + }, + { + "suite_id": 16, + "tests": [ + { + "id": 1, + "instance_path": [] + } + ] + }, + { + "suite_id": 17, + "tests": [ + { + "id": 0, + "instance_path": [] + }, + { + "id": 1, + "instance_path": [] + } + ] + } + ], + "refRemote.json": [ + { + "suite_id": 0, + "tests": [ + { + "id": 1, + "instance_path": [] + } + ] + }, + { + "suite_id": 1, + "tests": [ + { + "id": 1, + "instance_path": [] + } + ] + }, + { + "suite_id": 2, + "tests": [ + { + "id": 1, + "instance_path": [] + } + ] + } + ], + "required.json": [ + { + "suite_id": 0, + "tests": [ + { + "id": 1, + "instance_path": [] + } + ] + }, + { + "suite_id": 3, + "tests": [ + { + "id": 1, + "instance_path": [] + } + ] + } + ], + "type.json": [ + { + "suite_id": 0, + "tests": [ + { + "id": 2, + "instance_path": [] + }, + { + "id": 3, + "instance_path": [] + }, + { + "id": 4, + "instance_path": [] + }, + { + "id": 5, + "instance_path": [] + }, + { + "id": 6, + "instance_path": [] + }, + { + "id": 7, + "instance_path": [] + }, + { + "id": 8, + "instance_path": [] + } + ] + }, + { + "suite_id": 1, + "tests": [ + { + "id": 3, + "instance_path": [] + }, + { + "id": 4, + "instance_path": [] + }, + { + "id": 5, + "instance_path": [] + }, + { + "id": 6, + "instance_path": [] + }, + { + "id": 7, + "instance_path": [] + }, + { + "id": 8, + "instance_path": [] + } + ] + }, + { + "suite_id": 2, + "tests": [ + { + "id": 0, + "instance_path": [] + }, + { + "id": 1, + "instance_path": [] + }, + { + "id": 5, + "instance_path": [] + }, + { + "id": 6, + "instance_path": [] + }, + { + "id": 7, + "instance_path": [] + }, + { + "id": 8, + "instance_path": [] + } + ] + }, + { + "suite_id": 3, + "tests": [ + { + "id": 0, + "instance_path": [] + }, + { + "id": 1, + "instance_path": [] + }, + { + "id": 2, + "instance_path": [] + }, + { + "id": 4, + "instance_path": [] + }, + { + "id": 5, + "instance_path": [] + }, + { + "id": 6, + "instance_path": [] + } + ] + }, + { + "suite_id": 4, + "tests": [ + { + "id": 0, + "instance_path": [] + }, + { + "id": 1, + "instance_path": [] + }, + { + "id": 2, + "instance_path": [] + }, + { + "id": 3, + "instance_path": [] + }, + { + "id": 5, + "instance_path": [] + }, + { + "id": 6, + "instance_path": [] + } + ] + }, + { + "suite_id": 5, + "tests": [ + { + "id": 0, + "instance_path": [] + }, + { + "id": 1, + "instance_path": [] + }, + { + "id": 2, + "instance_path": [] + }, + { + "id": 3, + "instance_path": [] + }, + { + "id": 4, + "instance_path": [] + }, + { + "id": 5, + "instance_path": [] + }, + { + "id": 6, + "instance_path": [] + }, + { + "id": 9, + "instance_path": [] + } + ] + }, + { + "suite_id": 6, + "tests": [ + { + "id": 0, + "instance_path": [] + }, + { + "id": 1, + "instance_path": [] + }, + { + "id": 2, + "instance_path": [] + }, + { + "id": 3, + "instance_path": [] + }, + { + "id": 4, + "instance_path": [] + }, + { + "id": 5, + "instance_path": [] + }, + { + "id": 6, + "instance_path": [] + }, + { + "id": 7, + "instance_path": [] + }, + { + "id": 8, + "instance_path": [] + } + ] + }, + { + "suite_id": 7, + "tests": [ + { + "id": 2, + "instance_path": [] + }, + { + "id": 3, + "instance_path": [] + }, + { + "id": 4, + "instance_path": [] + }, + { + "id": 5, + "instance_path": [] + }, + { + "id": 6, + "instance_path": [] + } + ] + }, + { + "suite_id": 8, + "tests": [ + { + "id": 1, + "instance_path": [] + } + ] + }, + { + "suite_id": 9, + "tests": [ + { + "id": 2, + "instance_path": [] + }, + { + "id": 3, + "instance_path": [] + }, + { + "id": 4, + "instance_path": [] + } + ] + }, + { + "suite_id": 10, + "tests": [ + { + "id": 3, + "instance_path": [] + }, + { + "id": 4, + "instance_path": [] + } + ] + } + ] +} diff --git a/jsonschema/tests/output.rs b/crates/jsonschema/tests/output.rs similarity index 100% rename from jsonschema/tests/output.rs rename to crates/jsonschema/tests/output.rs diff --git a/crates/jsonschema/tests/suite b/crates/jsonschema/tests/suite new file mode 160000 index 00000000..9fc880bf --- /dev/null +++ b/crates/jsonschema/tests/suite @@ -0,0 +1 @@ +Subproject commit 9fc880bfb6d8ccd093bc82431f17d13681ffae8e diff --git a/crates/jsonschema/tests/suite.rs b/crates/jsonschema/tests/suite.rs new file mode 100644 index 00000000..678c90dd --- /dev/null +++ b/crates/jsonschema/tests/suite.rs @@ -0,0 +1,255 @@ +use jsonschema::{Draft, JSONSchema}; +use std::fs; +use testsuite::{suite, Test}; + +#[suite( + path = "crates/jsonschema/tests/suite", + drafts = [ + "draft4", + "draft6", + "draft7", + "draft2019-09", + "draft2020-12", + ], + xfail = [ + "draft4::optional::bignum::integer::a_bignum_is_an_integer", + "draft4::optional::bignum::integer::a_negative_bignum_is_an_integer", + "draft4::optional::ecmascript_regex", + "draft4::ref_remote::location_independent_identifier_in_remote_ref", + "draft6::optional::ecmascript_regex", + "draft6::ref_remote::location_independent_identifier_in_remote_ref", + "draft6::ref_remote::ref_to_ref_finds_location_independent_id", + "draft7::optional::ecmascript_regex", + "draft7::optional::format::idn_hostname::validation_of_internationalized_host_names", + "draft7::optional::format::time::validation_of_time_strings", + "draft7::ref_remote::location_independent_identifier_in_remote_ref", + "draft7::ref_remote::ref_to_ref_finds_location_independent_id", + "draft2019-09::anchor", + "draft2019-09::defs", + "draft2019-09::format::validation_of", + "draft2019-09::id::invalid_use_of_fragments_in_location_independent_id", + "draft2019-09::optional::anchor", + "draft2019-09::optional::cross_draft::refs_to_historic_drafts_are_processed_as_historic_drafts", + "draft2019-09::optional::ecmascript_regex::d_in_pattern_properties_matches_0_9_not_unicode_digits", + "draft2019-09::optional::ecmascript_regex::w_in_pattern_properties_matches_a_za_z0_9_not_unicode_letters", + "draft2019-09::optional::format::duration::validation_of_duration_strings", + "draft2019-09::optional::format::idn_hostname::validation_of_internationalized_host_names", + "draft2019-09::optional::format::time::validation_of_time_strings", + "draft2019-09::optional::format::uuid::uuid_format::no_dashes", + "draft2019-09::optional::ref_of_unknown_keyword::reference_internals_of_known_non_applicator", + "draft2019-09::r#ref::order_of_evaluation_id_and_anchor_and_ref", + "draft2019-09::r#ref::ref_with_recursive_anchor", + "draft2019-09::r#ref::urn_base_uri_with_urn_and_anchor_ref", + "draft2019-09::recursive_ref", + "draft2019-09::ref_remote::anchor_within_remote_ref", + "draft2019-09::ref_remote::base_uri_change_change_folder", + "draft2019-09::ref_remote::location_independent_identifier_in_remote_ref", + "draft2019-09::ref_remote::ref_to_ref_finds_detached_anchor", + "draft2019-09::ref_remote::remote_http_ref_with", + "draft2019-09::unevaluated_items", + "draft2019-09::unevaluated_properties::unevaluated_properties_with_recursive_ref", + "draft2019-09::vocabulary::schema_that_uses_custom_metaschema_with_with_no_validation_vocabulary", + "draft2020-12::anchor", + "draft2020-12::defs::validate_definition_against_metaschema::invalid_definition_schema", + "draft2020-12::dynamic_ref", + "draft2020-12::format::date_format", + "draft2020-12::format::date_format", + "draft2020-12::format::date_time_format", + "draft2020-12::format::date_time_format", + "draft2020-12::format::email_format", + "draft2020-12::format::email_format", + "draft2020-12::format::hostname_format", + "draft2020-12::format::hostname_format", + "draft2020-12::format::idn_email_format", + "draft2020-12::format::idn_email_format", + "draft2020-12::format::ipv4_format", + "draft2020-12::format::ipv4_format", + "draft2020-12::format::ipv6_format", + "draft2020-12::format::ipv6_format", + "draft2020-12::format::regex_format", + "draft2020-12::format::regex_format", + "draft2020-12::format::time_format", + "draft2020-12::format::time_format", + "draft2020-12::format::uri_format", + "draft2020-12::format::uri_format", + "draft2020-12::format::validation_of", + "draft2020-12::id::invalid_use_of_fragments_in_location_independent_id", + "draft2020-12::optional::anchor::anchor_inside_an_enum_is_not_a_real_identifier", + "draft2020-12::optional::cross_draft::refs_to_historic_drafts_are_processed_as_historic_drafts", + "draft2020-12::optional::dynamic_ref::dynamic_ref_skips_over_intermediate_resources_pointer_reference_across_resource_boundary", + "draft2020-12::optional::ecmascript_regex::a_is_not_an_ecma_262_control_escape", + "draft2020-12::optional::ecmascript_regex::d_in_pattern_properties_matches_0_9_not_unicode_digits", + "draft2020-12::optional::ecmascript_regex::w_in_pattern_properties_matches_a_za_z0_9_not_unicode_letters", + "draft2020-12::optional::format::duration::validation_of_duration_strings", + "draft2020-12::optional::format::email::validation_of_e_mail_addresses", + "draft2020-12::optional::format::idn_hostname::validation_of_internationalized_host_names", + "draft2020-12::optional::format::iri::validation_of_ir_is", + "draft2020-12::optional::format::iri_reference::validation_of_iri_references::an_invalid", + "draft2020-12::optional::format::json_pointer::validation_of_json_pointers_json_string_representation", + "draft2020-12::optional::format::relative_json_pointer::validation_of_relative_json_pointers_rjp", + "draft2020-12::optional::format::time::validation_of_time_strings", + "draft2020-12::optional::format::uri_reference::validation_of_uri_references::an_invalid", + "draft2020-12::optional::format::uri_template::format_uri_template::an_invalid_uri", + "draft2020-12::optional::format::uuid::uuid_format", + "draft2020-12::optional::format::uuid::uuid_format::bad_characters_not_hex", + "draft2020-12::optional::format::uuid::uuid_format::missing_section", + "draft2020-12::optional::format::uuid::uuid_format::no_dashes", + "draft2020-12::optional::format::uuid::uuid_format::wrong_length", + "draft2020-12::optional::ref_of_unknown_keyword::reference_internals_of_known_non_applicator", + "draft2020-12::r#ref::order_of_evaluation_id_and_anchor_and_ref", + "draft2020-12::r#ref::urn_base_uri_with_urn_and_anchor_ref", + "draft2020-12::ref_remote::anchor_within_remote_ref", + "draft2020-12::ref_remote::base_uri_change_change_folder", + "draft2020-12::ref_remote::location_independent_identifier_in_remote_ref", + "draft2020-12::ref_remote::ref_to_ref_finds_detached_anchor", + "draft2020-12::ref_remote::remote_http_ref_with_different_id", + "draft2020-12::ref_remote::remote_http_ref_with_different_urn_id", + "draft2020-12::ref_remote::remote_http_ref_with_nested_absolute_ref", + "draft2020-12::unevaluated_properties::unevaluated_properties_with_dynamic_ref", + "draft2020-12::unevaluated_items", + "draft2020-12::vocabulary", + ] +)] +fn test_suite(test: Test) { + let draft = match test.draft { + "draft4" => Draft::Draft4, + "draft6" => Draft::Draft6, + "draft7" => Draft::Draft7, + "draft2019-09" => Draft::Draft201909, + "draft2020-12" => Draft::Draft202012, + _ => panic!("Unsupported draft"), + }; + + let compiled = JSONSchema::options() + .with_draft(draft) + .should_validate_formats(true) + .compile(&test.schema) + .expect("should not fail to compile schema"); + + let result = compiled.validate(&test.data); + + if test.valid { + if let Err(mut errors_iterator) = result { + let first_error = errors_iterator.next(); + assert!( + first_error.is_none(), + "Test case should not have validation errors:\nGroup: {}\nTest case: {}\nSchema: {}\nInstance: {}\nError: {:?}", + test.case, + test.description, + test.schema, + test.data, + first_error, + ); + } + assert!( + compiled.is_valid(&test.data), + "Test case should be valid:\nCase: {}\nTest: {}\nSchema: {}\nInstance: {}", + test.case, + test.description, + test.schema, + test.data, + ); + let output = compiled.apply(&test.data).basic(); + assert!( + output.is_valid(), + "Test case should be valid via basic output:\nCase: {}\nTest: {}\nSchema: {}\nInstance: {}\nError: {:?}", + test.case, + test.description, + test.schema, + test.data, + output + ); + } else { + assert!( + result.is_err(), + "Test case should have validation errors:\nCase: {}\nTest: {}\nSchema: {}\nInstance: {}", + test.case, + test.description, + test.schema, + test.data, + ); + let errors = result.unwrap_err(); + for error in errors { + let pointer = error.instance_path.to_string(); + assert_eq!( + test.data.pointer(&pointer), Some(&*error.instance), + "Expected error instance did not match actual error instance:\nCase: {}\nTest: {}\nSchema: {}\nInstance: {}\nExpected pointer: {:#?}\nActual pointer: {:#?}", + test.case, + test.description, + test.schema, + test.data, + &*error.instance, + &pointer, + ); + } + assert!( + !compiled.is_valid(&test.data), + "Test case should be invalid:\nCase: {}\nTest: {}\nSchema: {}\nInstance: {}", + test.case, + test.description, + test.schema, + test.data, + ); + let output = compiled.apply(&test.data).basic(); + assert!( + !output.is_valid(), + "Test case should be invalid via basic output:\nCase: {}\nTest: {}\nSchema: {}\nInstance: {}", + test.case, + test.description, + test.schema, + test.data, + ); + } +} + +#[test] +fn test_instance_path() { + let expectations: serde_json::Value = + serde_json::from_str(include_str!("draft7_instance_paths.json")).expect("Valid JSON"); + for (filename, expected) in expectations.as_object().expect("Is object") { + let test_file = fs::read_to_string(format!("tests/suite/tests/draft7/{}", filename)) + .unwrap_or_else(|_| panic!("Valid file: {}", filename)); + let data: serde_json::Value = serde_json::from_str(&test_file).expect("Valid JSON"); + for item in expected.as_array().expect("Is array") { + let suite_id = item["suite_id"].as_u64().expect("Is integer") as usize; + let raw_schema = &data[suite_id]["schema"]; + let schema = JSONSchema::options() + .compile(raw_schema) + .unwrap_or_else(|_| { + panic!( + "Valid schema. File: {}; Suite ID: {}; Schema: {}", + filename, suite_id, raw_schema + ) + }); + for test_data in item["tests"].as_array().expect("Valid array") { + let test_id = test_data["id"].as_u64().expect("Is integer") as usize; + let instance_path: Vec<&str> = test_data["instance_path"] + .as_array() + .expect("Valid array") + .iter() + .map(|value| value.as_str().expect("A string")) + .collect(); + let instance = &data[suite_id]["tests"][test_id]["data"]; + let error = schema + .validate(instance) + .expect_err(&format!( + "\nFile: {}\nSuite: {}\nTest: {}", + filename, + &data[suite_id]["description"], + &data[suite_id]["tests"][test_id]["description"], + )) + .next() + .expect("Validation error"); + assert_eq!( + error.instance_path.clone().into_vec(), + instance_path, + "\nFile: {}\nSuite: {}\nTest: {}\nError: {}", + filename, + &data[suite_id]["description"], + &data[suite_id]["tests"][test_id]["description"], + &error + ) + } + } + } +} diff --git a/jsonschema-test-suite/CHANGELOG.md b/jsonschema-test-suite/CHANGELOG.md deleted file mode 100644 index 785d0f50..00000000 --- a/jsonschema-test-suite/CHANGELOG.md +++ /dev/null @@ -1,14 +0,0 @@ -# Changelog - -## 0.3.0 (2020-06-29) - -* Ignore tests accepts regular expressions - -## 0.2.0 (2020-06-22) - -* Fix documentation links -* Fix test_name definition in case of mulitple nested directories - -## 0.1.0 (2020-06-22) - -* Initial Release diff --git a/jsonschema-test-suite/Cargo.toml b/jsonschema-test-suite/Cargo.toml deleted file mode 100644 index 0397156a..00000000 --- a/jsonschema-test-suite/Cargo.toml +++ /dev/null @@ -1,31 +0,0 @@ -[package] -name = "json_schema_test_suite" -version = "0.3.0" -authors = ["Samuele Maci "] -edition = "2021" -publish = true -readme = "README.md" -keywords = ["jsonschema"] -categories = ["development-tools::procedural-macro-helpers", "development-tools::testing"] -description = "Procedural Macro Attribute to run all the test cases described in JSON-Schema-Test-Suite" -repository = "https://github.com/macisamuele/json-schema-test-suite-rs" -documentation = "http://docs.rs/json-schema-test-suite" -license = "MIT" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html -[lib] -path = "src/lib.rs" - -[dev-dependencies] -lazy_static = "1" -mockito = "0" -reqwest = { version = "0", default-features = false, features = [ "blocking", "json" ] } -serde_json = "1" - -[dependencies] -json_schema_test_suite_proc_macro = { path = "./proc_macro", version = "=0.3.0" } -json_schema_test_suite_test_case = { path = "./test_case", version = "=0.3.0" } -serde_json = "1" - -[workspace] -members = ["proc_macro", "test_case"] diff --git a/jsonschema-test-suite/LICENSE b/jsonschema-test-suite/LICENSE deleted file mode 100644 index 411af225..00000000 --- a/jsonschema-test-suite/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2020 Samuele Maci - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/jsonschema-test-suite/README.md b/jsonschema-test-suite/README.md deleted file mode 100644 index c931f19a..00000000 --- a/jsonschema-test-suite/README.md +++ /dev/null @@ -1,75 +0,0 @@ -# json_schema_test_suite - -[![ci](https://github.com/macisamuele/json-schema-test-suite-rs/workflows/ci/badge.svg)](https://github.com/macisamuele/json-schema-test-suite-rs/actions) -[![codecov](https://codecov.io/gh/macisamuele/json-schema-test-suite-rs/branch/master/graph/badge.svg)](https://codecov.io/gh/macisamuele/json-schema-test-suite-rs) -[![Crates.io](https://img.shields.io/crates/v/json-schema-test-suite.svg)](https://crates.io/crates/json-schema-test-suite) -[![docs.rs](https://docs.rs/json_schema_test_suite/badge.svg)](https://docs.rs/json-schema-test-suite/) - -The crate provides a procedural macro attribute that allow to generate all the test cases -described by [JSON-Schema-Test-Suite](https://github.com/json-schema-org/JSON-Schema-Test-Suite). - -The main objective is to ensure that for each test a mock server is started and will be able to -capture all the requests, ensuring that tests can be ran only interacting with `cargo test` - -In order to use the procedural macro attribute there are few assumptions that are made: - -* [`lazy_static`](https://crates.io/crates/lazy_static) dependency is added into your `[dev-dependencies]` - section of the `Cargo.toml` file -* [`mockito`](https://crates.io/crates/mockito) dependency is added into your `[dev-dependencies]` - section of the `Cargo.toml` file -* [`serde-json`](https://crates.io/crates/serde-json) dependency is added into your `[dev-dependencies]` - section of the `Cargo.toml` file -* the annotated method signature should be: `fn (&str, json_schema_test_suite::TestCase) -> ()` - -## How to use - -### Cargo.toml - -```toml -# Ensure that the following lines are present into your Cargo.toml file -[dev-dependencies] -json_schema_test_suite = "0" -# Be careful with dependencies version (using `*` version is never recommended). -# The proc-macro uses nothing fancy with the dependencies, so any version should work well :) -lazy_static = "*" -mockito = "*" -serde_json = "*" -``` - -### Rust test file example - -```rust -use json_schema_test_suite::{json_schema_test_suite, TestCase}; - -// If no tests should be ignored -#[json_schema_test_suite( - // path separator is assumed to be `/` (regardless of the run platform) - "path/to/JSON-Schema-Test-Suite/repository", - // test directory (in /tests/) - "draft7" -)] -// If some tests needs to be ignored, just defined them into `{ ... }` as follow -#[json_schema_test_suite( - // path separator is assumed to be `/` (regardless of the run platform) - "path/to/JSON-Schema-Test-Suite/repository", - // test directory (in /tests/) - "draft6", - { // Names, as generated by the macro, of the tests to ignore - // NOTE: They can be regular expression as well. - "name of the tests to ignore", - } -)] -fn my_simple_test( - // address of the HTTP server providing the remote files of JSON-Schema-Test-Suite. - // The format will be: `hostname:port`. - // This parameter is passed because by starting a mock server we might not start it - // into `localhost:1234` as expected by JSON-Schema-Test-Suite - server_address: &str, - // Representation of the test case - test_case: TestCase, -) { - // TODO: Add here your testing logic -} -``` - -License: MIT diff --git a/jsonschema-test-suite/proc_macro/.gitignore b/jsonschema-test-suite/proc_macro/.gitignore deleted file mode 100644 index 1b72444a..00000000 --- a/jsonschema-test-suite/proc_macro/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -/Cargo.lock -/target diff --git a/jsonschema-test-suite/proc_macro/Cargo.toml b/jsonschema-test-suite/proc_macro/Cargo.toml deleted file mode 100644 index 03b94798..00000000 --- a/jsonschema-test-suite/proc_macro/Cargo.toml +++ /dev/null @@ -1,31 +0,0 @@ -[package] -name = "json_schema_test_suite_proc_macro" -version = "0.3.0" -authors = ["Samuele Maci "] -edition = "2021" -publish = true -keywords = ["jsonschema"] -categories = ["development-tools::procedural-macro-helpers", "development-tools::testing"] -description = "Procedural Macro Attribute to run all the test cases described in JSON-Schema-Test-Suite" -repository = "https://github.com/macisamuele/json-schema-test-suite-rs" -documentation = "http://docs.rs/json-schema-test-suite-proc-macro" -license = "MIT" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html -[lib] -proc-macro = true -path = "src/lib.rs" - -[dev-dependencies] -lazy_static = "1" -mockito = "0" -serde_json = "1" - -[dependencies] -json_schema_test_suite_test_case = { path = "../test_case", version = "=0.3.0" } -proc-macro2 = "1" -regex = "1" -quote = "1" -serde = { version = "1", features = [ "derive" ] } -serde_json = "1" -syn = { version = "1", features = ["full"] } diff --git a/jsonschema-test-suite/proc_macro/README.md b/jsonschema-test-suite/proc_macro/README.md deleted file mode 100644 index 6c71387e..00000000 --- a/jsonschema-test-suite/proc_macro/README.md +++ /dev/null @@ -1,8 +0,0 @@ -# json_schema_test_suite_proc_macro - -This crate is supposed to support [`json_schema_test_suite`](https://crates.io/crates/json_schema_test_suite) -by exporting `json_schema_test_suite` procedural macro. - -Please refer to [`json-schema-test-suite-proc-macro` docs](https://docs.rs/json-schema-test-suite) for more informaton. - -License: MIT diff --git a/jsonschema-test-suite/proc_macro/src/attribute_config.rs b/jsonschema-test-suite/proc_macro/src/attribute_config.rs deleted file mode 100644 index 9f8f96b3..00000000 --- a/jsonschema-test-suite/proc_macro/src/attribute_config.rs +++ /dev/null @@ -1,48 +0,0 @@ -use regex::Regex; -use std::path::{Path, PathBuf, MAIN_SEPARATOR}; -use syn::{ - braced, - parse::{Parse, ParseStream}, - LitStr, Token, -}; - -#[derive(Debug)] -pub(crate) struct AttrConfig { - pub(crate) json_schema_test_suite_path: PathBuf, - pub(crate) draft_folder: String, - pub(crate) tests_to_exclude_regex: Vec, -} - -impl Parse for AttrConfig { - fn parse(input: ParseStream) -> Result { - let json_schema_test_suite_path_str: String = input.parse::()?.value(); - let _ = input.parse::()?; - let draft_folder: String = input.parse::()?.value(); - let tests_to_exclude_regex: Vec = if input.parse::().is_ok() { - let tests_to_exclude_tokens = { - let braced_content; - braced!(braced_content in input); - #[allow(clippy::redundant_closure_for_method_calls)] - let res: syn::punctuated::Punctuated = - braced_content.parse_terminated(|v| v.parse())?; - res - }; - tests_to_exclude_tokens - .iter() - .filter_map(|content| Regex::new(&format!("^{}$", content.value())).ok()) - .collect() - } else { - vec![] - }; - - let json_schema_test_suite_path = - Path::new(&json_schema_test_suite_path_str.replace("/", &MAIN_SEPARATOR.to_string())) - .to_path_buf(); - - Ok(Self { - json_schema_test_suite_path, - draft_folder, - tests_to_exclude_regex, - }) - } -} diff --git a/jsonschema-test-suite/proc_macro/src/lib.rs b/jsonschema-test-suite/proc_macro/src/lib.rs deleted file mode 100644 index 059f56b2..00000000 --- a/jsonschema-test-suite/proc_macro/src/lib.rs +++ /dev/null @@ -1,138 +0,0 @@ -//! This crate is supposed to support [`json_schema_test_suite`](https://crates.io/crates/json_schema_test_suite) -//! by exporting `json_schema_test_suite` procedural macro. -//! -//! Please refer to [`json-schema-test-suite-proc-macro` docs](https://docs.rs/json-schema-test-suite) for more informaton. -#![warn( - clippy::cast_possible_truncation, - clippy::doc_markdown, - clippy::explicit_iter_loop, - clippy::match_same_arms, - clippy::needless_borrow, - clippy::needless_pass_by_value, - clippy::option_map_unwrap_or, - clippy::option_map_unwrap_or_else, - clippy::option_unwrap_used, - clippy::pedantic, - clippy::print_stdout, - clippy::redundant_closure, - clippy::result_map_unwrap_or_else, - clippy::result_unwrap_used, - clippy::trivially_copy_pass_by_ref, - missing_debug_implementations, - missing_docs, - trivial_casts, - unreachable_pub, - unsafe_code, - unused_extern_crates, - unused_import_braces, - unused_qualifications, - unused_results, - variant_size_differences -)] -mod attribute_config; -mod mockito_mocks; -mod test_case; - -use json_schema_test_suite_test_case::TestCase; -use proc_macro::TokenStream; -use proc_macro2::{Span, TokenStream as TokenStream2}; -use quote::{format_ident, quote}; -use regex::Regex; -use syn::{parse_macro_input, Ident, ItemFn}; - -fn test_token_stream( - tests_to_ignore_regex: &[Regex], - wrapped_test_ident: &Ident, - test: &TestCase, -) -> TokenStream2 { - let name = Ident::new(&test.name, Span::call_site()); - - let maybe_ignore = if tests_to_ignore_regex - .iter() - .any(|regex| regex.is_match(&test.name)) - { - quote! { #[ignore] } - } else { - quote! {} - }; - - let wrapped_test_case = test_case::WrappedTestCase::from(test); - quote! { - #[test] - #maybe_ignore - fn #name() { - setup_mocks(); - - super::#wrapped_test_ident( - &mockito::server_address().to_string(), - #wrapped_test_case, - ); - } - } -} - -/// Procedural macro that allows a test to be executed for all the configurations defined -/// by [JSON-Schema-Test-Suite](https://github.com/json-schema-org/JSON-Schema-Test-Suite) -/// -/// The `proc_macro_attribute` should be used on a function with the current signature -/// ```rust -/// # use json_schema_test_suite_test_case::TestCase; -/// fn my_simple_test( -/// // address of the HTTP server providing the remote files of JSON-Schema-Test-Suite. The format will be: `hostname:port` -/// // This parameter is passed because by starting a mock server we might not start it into `localhost:1234` as expected by JSON-Schema-Test-Suite -/// server_address: &str, -/// // Representation of the test case (includes draft_version, descriptions, schema, instance, expected_valid) -/// test_case: TestCase, -/// ) { -/// // TODO: Add here your testing logic -/// } -/// ``` -#[proc_macro_attribute] -pub fn json_schema_test_suite(attr: TokenStream, item: TokenStream) -> TokenStream { - let proc_macro_attributes = parse_macro_input!(attr as attribute_config::AttrConfig); - let item_fn = parse_macro_input!(item as ItemFn); - - let original_function_ident = &item_fn.sig.ident; - let tests_token_stream: Vec = test_case::load( - &proc_macro_attributes.json_schema_test_suite_path, - &proc_macro_attributes.draft_folder, - ) - .iter() - .map(|test| { - test_token_stream( - &proc_macro_attributes.tests_to_exclude_regex, - original_function_ident, - test, - ) - }) - .collect(); - - let setup_mockito_mocks_token_stream = - mockito_mocks::setup(&proc_macro_attributes.json_schema_test_suite_path); - - let mod_name = format_ident!( - "{}_{}", - original_function_ident, - proc_macro_attributes.draft_folder.replace("-", "_") - ); - - let output = quote! { - #item_fn - - mod #mod_name { - lazy_static::lazy_static! { - static ref MOCKS: Vec = vec![ - #(#setup_mockito_mocks_token_stream),* - ]; - } - - fn setup_mocks() { - // Dereference to ensure that lazy_static actually invokes the mocks creation - let _ = *MOCKS; - } - - #(#tests_token_stream)* - } - }; - output.into() -} diff --git a/jsonschema-test-suite/proc_macro/src/mockito_mocks.rs b/jsonschema-test-suite/proc_macro/src/mockito_mocks.rs deleted file mode 100644 index 1f501050..00000000 --- a/jsonschema-test-suite/proc_macro/src/mockito_mocks.rs +++ /dev/null @@ -1,59 +0,0 @@ -use proc_macro2::TokenStream; -use quote::quote; -use std::{fs, path::Path}; - -pub(crate) fn setup(json_schema_test_suite_path: &Path) -> Vec { - fn remote_paths(dir: &Path) -> Vec { - let mut paths = vec![]; - for result_entry in fs::read_dir(dir) - .unwrap_or_else(|_| panic!("Remotes directory not found: {}", dir.display())) - { - if let Ok(entry) = result_entry { - let path = entry.path(); - if entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false) { - paths.extend(remote_paths(&path)); - } else { - paths.push(path.to_str().map_or_else( - || { - panic!( - "No issues are expected while converting path={} to string", - path.display() - ); - }, - ToString::to_string, - )); - } - } - } - paths - } - - let remote_base_path = json_schema_test_suite_path.join("remotes"); - let base_path = remote_base_path.to_str().unwrap_or_else(|| { - panic!( - "No issues are expected while converting remote_base_path={} to string", - remote_base_path.display() - ); - }); - remote_paths(&remote_base_path) - .iter() - .filter_map(|remote_path| { - let path = remote_path - .trim_start_matches(base_path) - .replace(std::path::MAIN_SEPARATOR, "/"); - if let Ok(file_content) = fs::read_to_string(remote_path) { - Some(quote! { - mockito::mock("GET", #path) - .with_body( - #file_content - // Replace static links to localhost:1234 to the mockito generated server address - .replace("localhost:1234", &mockito::server_address().to_string()) - ) - .create() - }) - } else { - None - } - }) - .collect() -} diff --git a/jsonschema-test-suite/proc_macro/src/test_case.rs b/jsonschema-test-suite/proc_macro/src/test_case.rs deleted file mode 100644 index c78d00ce..00000000 --- a/jsonschema-test-suite/proc_macro/src/test_case.rs +++ /dev/null @@ -1,153 +0,0 @@ -pub(crate) use json_schema_test_suite_test_case::TestCase; -use serde::Deserialize; -use serde_json::{from_reader, Value}; -use std::{ffi::OsStr, fs, fs::File, path::Path}; - -#[derive(Debug, Deserialize)] -struct JSONSchemaTest { - data: Value, - description: String, - valid: bool, -} - -#[derive(Debug, Deserialize)] -struct JSONSchemaTestGroup { - description: String, - schema: Value, - tests: Vec, -} - -/// Extract the draft version from the path of the test file. -fn draft_version(json_schema_test_suite_path: &Path, file_path: &Path) -> String { - file_path - .strip_prefix(json_schema_test_suite_path.join("tests")) - .ok() - .and_then(Path::to_str) - .map(|v| v.split(std::path::MAIN_SEPARATOR)) - .and_then(|mut v| v.next()) - .map_or_else( - || { - panic!( - "No issues are expected while extracting the draft-version from the file_path. json_schema_test_suite_path={}, file_path={}", - json_schema_test_suite_path.display(), - file_path.display() - ) - }, - ToString::to_string, - ) -} - -fn load_inner(json_schema_test_suite_path: &Path, dir: &Path, prefix: &str) -> Vec { - let mut tests = vec![]; - for result_entry in fs::read_dir(dir).unwrap_or_else(|_| { - panic!( - r#"JSON Schema Test Suite not found. -Please ensure the test suite has been initialized correctly. -Run `git submodule init` and `git submodule update` in the root directory to initialize it. -If the issue persists, please verify the path to `{}` is correct."#, - dir.display() - ) - }) { - if let Ok(entry) = result_entry { - let path = entry.path(); - if entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false) { - tests.extend(load_inner( - json_schema_test_suite_path, - &path, - &format!( - "{}{}_", - prefix, - path.file_name().and_then(OsStr::to_str).unwrap_or_else(|| { - panic!( - "No issues are expected while extracting the filename from path={}", - path.display() - ) - }) - ), - )); - } else if let Ok(file_reader) = File::open(&path) { - let test_groups: Vec = from_reader(file_reader).unwrap_or_else(|_| { - panic!( - r#"{} does not contain valid content. Expected something like [{{"schema": ..., "tests": [{{"data": ..., "is_valid": ...}}, ...]}}]"#, - path.display() - ); - }); - - tests.extend(test_groups.iter().enumerate().flat_map(|(gid, test_group)| { - test_group - .tests - .iter() - .enumerate() - .map(|(tid, test_case)| TestCase { - name: format!( - "{}{}_{}_{}", - prefix, - path.file_stem() - .and_then(OsStr::to_str) - .unwrap_or_else(|| { - panic!( - "No issues are expected while extracting the filename (without extension) from path={}", - path.display() - ); - }) - .replace('-', "_"), - gid, - tid - ), - draft_version: draft_version(json_schema_test_suite_path, &path), - group_description: test_group.description.clone(), - test_case_description: test_case.description.clone(), - schema: test_group.schema.clone(), - instance: test_case.data.clone(), - is_valid: test_case.valid, - }) - .collect::>() - })) - } - } - } - tests -} - -/// Load all the test cases present into `draft_folder` -pub(crate) fn load(json_schema_test_suite_path: &Path, draft_folder: &str) -> Vec { - load_inner( - json_schema_test_suite_path, - &json_schema_test_suite_path.join("tests").join(draft_folder), - "", - ) -} - -pub(crate) struct WrappedTestCase<'a>(&'a TestCase); -impl<'a> From<&'a TestCase> for WrappedTestCase<'a> { - fn from(value: &'a TestCase) -> Self { - Self(value) - } -} - -use proc_macro2::TokenStream; -use quote::{quote, ToTokens}; -impl<'a> ToTokens for WrappedTestCase<'a> { - fn to_tokens(&self, tokens: &mut TokenStream) { - let name = &self.0.name; - let draft_version = &self.0.draft_version; - let group_description = &self.0.group_description; - let test_case_description = &self.0.test_case_description; - let schema_str = self.0.schema.to_string(); - let instance_str = self.0.instance.to_string(); - let is_valid = self.0.is_valid; - - let output = quote! { - json_schema_test_suite::TestCase { - name: #name.to_string(), - draft_version: #draft_version.to_string(), - group_description: #group_description.to_string(), - test_case_description: #test_case_description.to_string(), - schema: serde_json::from_str(#schema_str).unwrap(), - instance: serde_json::from_str(#instance_str).unwrap(), - is_valid: #is_valid, - } - }; - tokens.extend(output); - } -} diff --git a/jsonschema-test-suite/rustfmt.toml b/jsonschema-test-suite/rustfmt.toml deleted file mode 100644 index bf772232..00000000 --- a/jsonschema-test-suite/rustfmt.toml +++ /dev/null @@ -1,2 +0,0 @@ -imports_granularity = "Crate" -edition = "2021" diff --git a/jsonschema-test-suite/src/lib.rs b/jsonschema-test-suite/src/lib.rs deleted file mode 100644 index a834e04e..00000000 --- a/jsonschema-test-suite/src/lib.rs +++ /dev/null @@ -1,101 +0,0 @@ -//! [![ci](https://github.com/macisamuele/json-schema-test-suite-rs/workflows/ci/badge.svg)](https://github.com/macisamuele/json-schema-test-suite-rs/actions) -//! [![codecov](https://codecov.io/gh/macisamuele/json-schema-test-suite-rs/branch/master/graph/badge.svg)](https://codecov.io/gh/macisamuele/json-schema-test-suite-rs) -//! [![Crates.io](https://img.shields.io/crates/v/json-schema-test-suite.svg)](https://crates.io/crates/json-schema-test-suite) -//! [![docs.rs](https://docs.rs/json_schema_test_suite/badge.svg)](https://docs.rs/json-schema-test-suite/) -//! -//! The crate provides a procedural macro attribute that allow to generate all the test cases -//! described by [JSON-Schema-Test-Suite](https://github.com/json-schema-org/JSON-Schema-Test-Suite). -//! -//! The main objective is to ensure that for each test a mock server is started and will be able to -//! capture all the requests, ensuring that tests can be ran only interacting with `cargo test` -//! -//! In order to ude the procedural macro attribute there are few assumptions that are made: -//! -//! * [`lazy_static`](https://crates.io/crates/lazy_static) dependency is added into your `[dev-dependencies]` -//! section of the `Cargo.toml` file -//! * [`mockito`](https://crates.io/crates/mockito) dependency is added into your `[dev-dependencies]` -//! section of the `Cargo.toml` file -//! * [`serde-json`](https://crates.io/crates/serde-json) dependency is added into your `[dev-dependencies]` -//! section of the `Cargo.toml` file -//! * the annotated method signature should be: `fn (&str, json_schema_test_suite::TestCase) -> ()` -//! -//! # How to use -//! -//! ## Cargo.toml -//! -//! ```toml -//! # Ensure that the following lines are present into your Cargo.toml file -//! [dev-dependencies] -//! json_schema_test_suite = "0" -//! # Be careful with dependencies version (using `*` version is never recommended). -//! # The proc-macro uses nothing fancy with the dependencies, so any version should work well :) -//! lazy_static = "*" -//! mockito = "*" -//! serde_json = "*" -//! ``` -//! -//! ## Rust test file example -//! -//! ```rust -//! use json_schema_test_suite::{json_schema_test_suite, TestCase}; -//! -//! // If no tests should be ignored -//! #[json_schema_test_suite( -//! // path separator is assumed to be `/` (regardless of the run platform) -//! "path/to/JSON-Schema-Test-Suite/repository", -//! // test directory (in /tests/) -//! "draft7" -//! )] -//! // If some tests needs to be ignored, just defined them into `{ ... }` as follow -//! #[json_schema_test_suite( -//! // path separator is assumed to be `/` (regardless of the run platform) -//! "path/to/JSON-Schema-Test-Suite/repository", -//! // test directory (in /tests/) -//! "draft6", -//! { // Names, as generated by the macro, of the tests to ignore -//! // NOTE: They can be regular expression as well. -//! "name of the tests to ignore", -//! } -//! )] -//! fn my_simple_test( -//! // address of the HTTP server providing the remote files of JSON-Schema-Test-Suite. -//! // The format will be: `hostname:port`. -//! // This parameter is passed because by starting a mock server we might not start it -//! // into `localhost:1234` as expected by JSON-Schema-Test-Suite -//! server_address: &str, -//! // Representation of the test case -//! test_case: TestCase, -//! ) { -//! // TODO: Add here your testing logic -//! } -//! ``` -#![warn( - clippy::cast_possible_truncation, - clippy::doc_markdown, - clippy::explicit_iter_loop, - clippy::match_same_arms, - clippy::needless_borrow, - clippy::needless_pass_by_value, - clippy::option_map_unwrap_or, - clippy::option_map_unwrap_or_else, - clippy::option_unwrap_used, - clippy::pedantic, - clippy::print_stdout, - clippy::redundant_closure, - clippy::result_map_unwrap_or_else, - clippy::result_unwrap_used, - clippy::trivially_copy_pass_by_ref, - missing_debug_implementations, - missing_docs, - trivial_casts, - unreachable_pub, - unsafe_code, - unused_extern_crates, - unused_import_braces, - unused_qualifications, - unused_results, - variant_size_differences -)] - -pub use json_schema_test_suite_proc_macro::json_schema_test_suite; -pub use json_schema_test_suite_test_case::TestCase; diff --git a/jsonschema-test-suite/test_case/.gitignore b/jsonschema-test-suite/test_case/.gitignore deleted file mode 100644 index 1b72444a..00000000 --- a/jsonschema-test-suite/test_case/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -/Cargo.lock -/target diff --git a/jsonschema-test-suite/test_case/Cargo.toml b/jsonschema-test-suite/test_case/Cargo.toml deleted file mode 100644 index fa40d374..00000000 --- a/jsonschema-test-suite/test_case/Cargo.toml +++ /dev/null @@ -1,20 +0,0 @@ -[package] -name = "json_schema_test_suite_test_case" -version = "0.3.0" -authors = ["Samuele Maci "] -edition = "2021" -publish = true -keywords = ["jsonschema"] -categories = ["development-tools::procedural-macro-helpers", "development-tools::testing"] -description = "Procedural Macro Attribute to run all the test cases described in JSON-Schema-Test-Suite" -repository = "https://github.com/macisamuele/json-schema-test-suite-rs" -documentation = "http://docs.rs/json-schema-test-suite-test-case" -license = "MIT" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html -[lib] -path = "src/lib.rs" - -[dependencies] -serde = { version = "1", features = [ "derive" ] } -serde_json = "1" diff --git a/jsonschema-test-suite/test_case/README.md b/jsonschema-test-suite/test_case/README.md deleted file mode 100644 index 51a2ad87..00000000 --- a/jsonschema-test-suite/test_case/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# json_schema_test_suite_test_case - -This crate is supposed to support [`json_schema_test_suite`](https://crates.io/crates/json_schema_test_suite) -by exporting the base struct to allow users of `json_schema_test_suite` to have a single entity representing the test information. - -This crate is needed because currently, at the time of writing, we are not allowed to export structs from a proc-macro library -Please refer to [`json-schema-test-suite` docs](https://docs.rs/json-schema-test-suite) for more informaton. - -License: MIT diff --git a/jsonschema-test-suite/test_case/src/lib.rs b/jsonschema-test-suite/test_case/src/lib.rs deleted file mode 100644 index 6513b644..00000000 --- a/jsonschema-test-suite/test_case/src/lib.rs +++ /dev/null @@ -1,54 +0,0 @@ -//! This crate is supposed to support [`json_schema_test_suite`](https://crates.io/crates/json_schema_test_suite) -//! by exporting the base struct to allow users of `json_schema_test_suite` to have a single entity representing the test information. -//! -//! This crate is needed because currently, at the time of writing, we are not allowed to export structs from a proc-macro library -//! Please refer to [`json-schema-test-suite` docs](https://docs.rs/json-schema-test-suite) for more informaton. -#![warn( - clippy::cast_possible_truncation, - clippy::doc_markdown, - clippy::explicit_iter_loop, - clippy::match_same_arms, - clippy::needless_borrow, - clippy::needless_pass_by_value, - clippy::option_map_unwrap_or, - clippy::option_map_unwrap_or_else, - clippy::option_unwrap_used, - clippy::pedantic, - clippy::print_stdout, - clippy::redundant_closure, - clippy::result_map_unwrap_or_else, - clippy::result_unwrap_used, - clippy::trivially_copy_pass_by_ref, - missing_debug_implementations, - missing_docs, - trivial_casts, - unreachable_pub, - unsafe_code, - unused_extern_crates, - unused_import_braces, - unused_qualifications, - unused_results, - variant_size_differences -)] - -use serde_json::Value; - -/// Detailed information about the individual test case in JSON-Schema-Test-Suite -#[derive(Clone, Debug)] -pub struct TestCase { - /// Test name (unique identifier) - pub name: String, - /// String representation of the draft version (equialent to the test directory) - /// This is usefull in case your test needs to be aware of the draft version under test - pub draft_version: String, - /// Description of the test group as provided by JSON-Schema-Test-Suite - pub group_description: String, - /// Description of the test group as provided by JSON-Schema-Test-Suite - pub test_case_description: String, - /// Schema to be tested - pub schema: Value, - /// Instance to be validated against the `schema - pub instance: Value, - /// Expected validity status as from JSON-Schema-Test-Suite - pub is_valid: bool, -} diff --git a/jsonschema-test-suite/tests/introduce-failure.rs b/jsonschema-test-suite/tests/introduce-failure.rs deleted file mode 100644 index 33e8dcdc..00000000 --- a/jsonschema-test-suite/tests/introduce-failure.rs +++ /dev/null @@ -1,19 +0,0 @@ -use json_schema_test_suite::{json_schema_test_suite, TestCase}; - -#[json_schema_test_suite("path/to/JSON-Schema-Test-Suite/repository", "draft7", {"ref_0_0", r"optional_format_regex_0_\d+"})] -fn no_op_test_with_some_failures(server_address: &str, test_case: TestCase) { - if test_case.name == "ref_0_0" { - // Test that failure is properly ignored via the macro argument - panic!("The test should fail, but we ensure that this is ignored") - } else if test_case.name == "ref_0_1" { - // Test that mocks work as expected - match reqwest::blocking::get(&format!("http://{}/integer.json", server_address)) - .and_then(reqwest::blocking::Response::json::) - { - Ok(json_resp) => assert_eq!(json_resp, serde_json::json!({"type": "integer"})), - Err(err) => panic!("Issue while interacting with mocks: {:?}", err), - } - } else if test_case.name.starts_with("optional_format_regex_0_") { - panic!("We want to force a falure for all the test named like 'optional_format_regex_0_.*' to ensure that the test ignore list includes them") - } -} diff --git a/jsonschema-test-suite/tests/no-op.rs b/jsonschema-test-suite/tests/no-op.rs deleted file mode 100644 index fa3629b6..00000000 --- a/jsonschema-test-suite/tests/no-op.rs +++ /dev/null @@ -1,4 +0,0 @@ -use json_schema_test_suite::{json_schema_test_suite, TestCase}; - -#[json_schema_test_suite("path/to/JSON-Schema-Test-Suite/repository", "draft7")] -fn no_op_test(_server_address: &str, _test_case: TestCase) {} diff --git a/jsonschema/Cargo.toml b/jsonschema/Cargo.toml deleted file mode 100644 index 5490b4c6..00000000 --- a/jsonschema/Cargo.toml +++ /dev/null @@ -1,104 +0,0 @@ -[package] -authors = ["Dmitry Dygalo "] -description = "A crate for performing JSON schema validation" -edition = "2021" -exclude = [ - "tests", - "python", - "benches/data/*.json", - ".github", - ".yamllint", - ".pre-commit-config.yaml", - ".gitignore", - ".gitmodules", - "*.md", -] -keywords = ["jsonschema", "validation"] -license = "MIT" -name = "jsonschema" -readme = "../README.md" -repository = "https://github.com/Stranger6667/jsonschema-rs" -version = "0.18.3" -rust-version = "1.56.1" - -categories = ["web-programming"] - -[[bin]] -name = "jsonschema" - -[features] -cli = ["clap"] -default = ["resolve-http", "resolve-file", "cli"] -draft201909 = [] -draft202012 = [] - -resolve-http = ["reqwest"] -resolve-file = [] - -[dependencies] -ahash = { version = "0.8", features = ["serde"] } -anyhow = "1.0" -base64 = "0.22" -bytecount = { version = "0.6", features = ["runtime-dispatch-simd"] } -clap = { version = "4.5", features = ["derive"], optional = true } -fancy-regex = "0.13" -fraction = { version = "0.15", default-features = false, features = [ - "with-bigint", -] } -iso8601 = "0.6" -itoa = "1" -memchr = "2.7" -num-cmp = "0.1" -once_cell = "1.19" -parking_lot = "0.12" -percent-encoding = "2.3" -regex = "1.10" -reqwest = { version = "0.12", features = [ - "blocking", - "json", -], default-features = false, optional = true } -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" -time = { version = "0.3", features = ["parsing", "macros"] } -url = "2.5" -uuid = "1" - -[target.'cfg(target_arch = "wasm32")'.dependencies] -getrandom = { version = "0.2", features = ["js"] } - -[dev-dependencies] -bench_helpers = { path = "../bench_helpers" } -boon = "0.6" -codspeed-criterion-compat = "2.6.0" -criterion = { version = "0.5.1", features = [], default-features = false } -json_schema_test_suite = { version = "0.3.0", path = "../jsonschema-test-suite" } -jsonschema-valid = "0.5" -lazy_static = "1.4" # Needed for json schema test suite -mockito = "0.31" -paste = "1.0" -test-case = "3" -valico = "4" - -# Benchmarks for `jsonschema` -[[bench]] -harness = false -name = "jsonschema" - -# Benchmarks for `valico` -[[bench]] -harness = false -name = "valico" - -# Benchmarks for `jsonschema_valid` -[[bench]] -harness = false -name = "jsonschema_valid" - -# Benchmarks for `boon` -[[bench]] -harness = false -name = "boon" - -[profile.release] -codegen-units = 1 -lto = "fat" diff --git a/jsonschema/benches/boon.rs b/jsonschema/benches/boon.rs deleted file mode 100644 index 98cdd2ec..00000000 --- a/jsonschema/benches/boon.rs +++ /dev/null @@ -1,53 +0,0 @@ -use bench_helpers::{bench_citm, bench_fast, bench_geojson, bench_openapi, bench_swagger}; -use codspeed_criterion_compat::{criterion_group, criterion_main, Criterion}; - -macro_rules! boon_bench { - ($c:tt, $name:expr, $schema:ident, $instance:ident) => {{ - let mut schemas = boon::Schemas::new(); - let mut compiler = boon::Compiler::new(); - compiler.add_resource("schema.json", $schema).unwrap(); - let id = compiler.compile("schema.json", &mut schemas).unwrap(); - assert!(schemas.validate(&$instance, id).is_ok(), "Invalid instance"); - $c.bench_function(&format!("{} boon/validate", $name), |b| { - b.iter(|| { - let _ = schemas.validate(&$instance, id).is_ok(); - }); - }); - }}; -} - -fn large_schemas(c: &mut Criterion) { - // Open API JSON Schema - // Only `jsonschema` works correctly - other libraries do not recognize `zuora` as valid - bench_openapi(&mut |name, schema, instance| boon_bench!(c, name, schema, instance)); - // Swagger JSON Schema - bench_swagger(&mut |name, schema, instance| boon_bench!(c, name, schema, instance)); - // Canada borders in GeoJSON - bench_geojson(&mut |name, schema, instance| boon_bench!(c, name, schema, instance)); - // CITM catalog - bench_citm(&mut |name, schema, instance| boon_bench!(c, name, schema, instance)); -} - -fn fast_schema(c: &mut Criterion) { - bench_fast(&mut |name, schema, valid, invalid| { - let mut schemas = boon::Schemas::new(); - let mut compiler = boon::Compiler::new(); - compiler.add_resource("schema.json", schema).unwrap(); - let id = compiler.compile("schema.json", &mut schemas).unwrap(); - assert!(schemas.validate(&valid, id).is_ok(), "Invalid instance"); - assert!(schemas.validate(&invalid, id).is_err(), "Invalid instance"); - c.bench_function(&format!("{} boon/is_valid/valid", name), |b| { - b.iter(|| { - let _ = schemas.validate(&valid, id).is_ok(); - }); - }); - c.bench_function(&format!("{} boon/is_valid/invalid", name), |b| { - b.iter(|| { - let _ = schemas.validate(&invalid, id).is_ok(); - }); - }); - }); -} - -criterion_group!(arbitrary, large_schemas, fast_schema); -criterion_main!(arbitrary); diff --git a/jsonschema/benches/jsonschema.rs b/jsonschema/benches/jsonschema.rs deleted file mode 100644 index 2dd6e59f..00000000 --- a/jsonschema/benches/jsonschema.rs +++ /dev/null @@ -1,166 +0,0 @@ -use bench_helpers::{ - bench_citm, bench_fast, bench_geojson, bench_keywords, bench_openapi, bench_swagger, -}; -use codspeed_criterion_compat::{criterion_group, criterion_main, Bencher, BenchmarkId, Criterion}; -use jsonschema::{paths::JsonPointerNode, JSONSchema}; -use serde_json::Value; - -macro_rules! jsonschema_bench { - ($c:tt, $name:expr, $schema:ident, $instance:ident) => {{ - let compiled = JSONSchema::options() - .compile(&$schema) - .expect("Invalid schema"); - assert!(compiled.is_valid(&$instance), "Invalid instance"); - assert!(compiled.validate(&$instance).is_ok(), "Invalid instance"); - $c.bench_function(&format!("{} jsonschema/compile", $name), |b| { - b.iter(|| JSONSchema::options().compile(&$schema)) - }); - $c.bench_function(&format!("{} jsonschema/is_valid", $name), |b| { - b.iter(|| compiled.is_valid(&$instance)) - }); - $c.bench_function(&format!("{} jsonschema/validate", $name), |b| { - b.iter(|| compiled.validate(&$instance).ok()) - }); - }}; -} - -fn large_schemas(c: &mut Criterion) { - // Open API JSON Schema - // Only `jsonschema` works correctly - other libraries do not recognize `zuora` as valid - bench_openapi(&mut |name, schema, instance| jsonschema_bench!(c, name, schema, instance)); - // Swagger JSON Schema - bench_swagger(&mut |name, schema, instance| jsonschema_bench!(c, name, schema, instance)); - // Canada borders in GeoJSON - bench_geojson(&mut |name, schema, instance| jsonschema_bench!(c, name, schema, instance)); - // CITM catalog - bench_citm(&mut |name, schema, instance| jsonschema_bench!(c, name, schema, instance)); -} - -fn fast_schema(c: &mut Criterion) { - bench_fast(&mut |name, schema, valid, invalid| { - let compiled = JSONSchema::compile(&schema).expect("Valid schema"); - assert!(compiled.is_valid(&valid)); - assert!(!compiled.is_valid(&invalid)); - c.bench_function(&format!("{} jsonschema/compile", name), |b| { - b.iter(|| JSONSchema::compile(&schema).expect("Valid schema")) - }); - c.bench_function(&format!("{} jsonschema/is_valid/valid", name), |b| { - b.iter(|| compiled.is_valid(&valid)) - }); - c.bench_function(&format!("{} jsonschema/validate/valid", name), |b| { - b.iter(|| compiled.validate(&valid).ok()) - }); - c.bench_function(&format!("{} jsonschema/is_valid/invalid", name), |b| { - b.iter(|| compiled.is_valid(&invalid)) - }); - c.bench_function(&format!("{} jsonschema/validate/invalid", name), |b| { - b.iter(|| { - let _: Vec<_> = compiled - .validate(&invalid) - .expect_err("There should be errors") - .collect(); - }) - }); - }); -} - -fn keywords(c: &mut Criterion) { - bench_keywords( - c, - &|_: &str| false, - &|schema: &Value, instance: &Value| { - let compiled = JSONSchema::compile(schema).expect("Valid schema"); - compiled.is_valid(instance) - }, - &mut |c: &mut Criterion, name: &str, schema: &Value| { - c.bench_with_input( - BenchmarkId::new(name, "jsonschema/compile"), - schema, - |b, schema| { - b.iter(|| { - JSONSchema::compile(schema).expect("Valid schema"); - }) - }, - ); - }, - validate_valid, - validate_invalid, - ) -} - -fn validate_valid(c: &mut Criterion, name: &str, schema: &Value, instance: &Value) { - let compiled = JSONSchema::compile(schema).expect("Valid schema"); - c.bench_with_input( - BenchmarkId::new(name, "jsonschema/is_valid/valid"), - instance, - |b, instance| { - b.iter(|| { - let _ = compiled.is_valid(instance); - }) - }, - ); - c.bench_with_input( - BenchmarkId::new(name, "jsonschema/validate/valid"), - instance, - |b, instance| { - b.iter(|| { - compiled.validate(instance).ok(); - }) - }, - ); -} - -fn validate_invalid(c: &mut Criterion, name: &str, schema: &Value, instance: &Value) { - let compiled = JSONSchema::compile(schema).expect("Valid schema"); - c.bench_with_input( - BenchmarkId::new(name, "jsonschema/is_valid/invalid"), - instance, - |b, instance| { - b.iter(|| { - let _ = compiled.is_valid(instance); - }) - }, - ); - c.bench_with_input( - BenchmarkId::new(name, "jsonschema/validate/invalid"), - instance, - |b, instance| { - b.iter(|| { - let _: Vec<_> = compiled - .validate(instance) - .expect_err("There should be errors") - .collect(); - }) - }, - ); -} - -fn json_pointer_node(c: &mut Criterion) { - fn bench(b: &mut Bencher, pointer: &JsonPointerNode) { - b.iter(|| { - let _ = pointer.to_vec(); - }) - } - let empty = JsonPointerNode::new(); - c.bench_with_input(BenchmarkId::new("jsonpointer", "empty"), &empty, bench); - let root = JsonPointerNode::new(); - let node = root.push("entry"); - let node = node.push("entry"); - let node = node.push("entry"); - c.bench_with_input(BenchmarkId::new("jsonpointer", "small"), &node, bench); - let root = JsonPointerNode::new(); - let node = root.push("entry"); - let node = node.push("entry"); - let node = node.push("entry"); - let node = node.push("entry"); - let node = node.push("entry"); - let node = node.push("entry"); - let node = node.push("entry"); - let node = node.push("entry"); - let node = node.push("entry"); - c.bench_with_input(BenchmarkId::new("jsonpointer", "big"), &node, bench); -} - -criterion_group!(common, large_schemas, fast_schema, json_pointer_node); -criterion_group!(specific, keywords); -criterion_main!(common, specific); diff --git a/jsonschema/benches/jsonschema_valid.rs b/jsonschema/benches/jsonschema_valid.rs deleted file mode 100644 index fa2be498..00000000 --- a/jsonschema/benches/jsonschema_valid.rs +++ /dev/null @@ -1,100 +0,0 @@ -use bench_helpers::{bench_citm, bench_fast, bench_geojson, bench_keywords}; -use codspeed_criterion_compat::{criterion_group, criterion_main, BenchmarkId, Criterion}; -use jsonschema_valid::schemas; -use serde_json::Value; - -macro_rules! jsonschema_valid_bench { - ($c:tt, $name:expr, $schema:ident, $instance:ident, $draft:expr) => {{ - let cfg = - jsonschema_valid::Config::from_schema(&$schema, Some($draft)).expect("Invalid schema"); - assert!( - jsonschema_valid::validate(&cfg, &$instance).is_ok(), - "Invalid instance" - ); - $c.bench_function(&format!("{} jsonschema_valid/validate/valid", $name), |b| { - // There is no specialized method for fast boolean return value - b.iter(|| jsonschema_valid::validate(&cfg, &$instance).is_ok()) - }); - }}; -} - -fn large_schemas(c: &mut Criterion) { - // Canada borders in GeoJSON - bench_geojson(&mut |name, schema, instance| { - jsonschema_valid_bench!(c, name, schema, instance, schemas::Draft::Draft7) - }); - // CITM catalog - bench_citm(&mut |name, schema, instance| { - jsonschema_valid_bench!(c, name, schema, instance, schemas::Draft::Draft7) - }); -} - -fn fast_schema(c: &mut Criterion) { - bench_fast(&mut |name, schema, valid, invalid| { - let cfg = jsonschema_valid::Config::from_schema(&schema, Some(schemas::Draft::Draft7)) - .expect("Valid schema"); - c.bench_function(&format!("{} jsonschema_valid/compile", name), |b| { - b.iter(|| { - jsonschema_valid::Config::from_schema(&schema, Some(schemas::Draft::Draft7)) - .expect("Valid schema") - }) - }); - c.bench_function(&format!("{} jsonschema_valid/validate/valid", name), |b| { - b.iter(|| jsonschema_valid::validate(&cfg, &valid)) - }); - c.bench_function( - &format!("{} jsonschema_valid/validate/invalid", name), - |b| b.iter(|| jsonschema_valid::validate(&cfg, &invalid).ok()), - ); - }); -} - -fn keywords(c: &mut Criterion) { - bench_keywords( - c, - &|name: &str| { - // Bug in `jsonschema_valid` - // `Option::unwrap()` on a `None` value' - // https://github.com/mdboom/jsonschema-valid/blob/de1da64fb624085eccde290e036a2ed592656f38/src/validators.rs#L531 - name == "multiple_of_integer" - }, - &|schema: &Value, instance: &Value| { - let compiled = - jsonschema_valid::Config::from_schema(schema, Some(schemas::Draft::Draft7)) - .expect("Valid schema"); - let result = jsonschema_valid::validate(&compiled, instance).is_ok(); - result - }, - &mut |c: &mut Criterion, name: &str, schema: &Value| { - c.bench_with_input( - BenchmarkId::new(name, "jsonschema_valid/compile"), - schema, - |b, schema| { - b.iter(|| { - jsonschema_valid::Config::from_schema(schema, Some(schemas::Draft::Draft7)) - .expect("Valid schema") - }) - }, - ); - }, - validate, - validate, - ) -} - -fn validate(c: &mut Criterion, name: &str, schema: &Value, instance: &Value) { - let compiled = jsonschema_valid::Config::from_schema(schema, Some(schemas::Draft::Draft7)) - .expect("Valid schema"); - c.bench_with_input( - BenchmarkId::new(name, "jsonschema_valid"), - &(compiled, instance), - |b, (compiled, instance)| { - b.iter(|| { - let _ = jsonschema_valid::validate(compiled, instance); - }) - }, - ); -} - -criterion_group!(jsonschema_valid, large_schemas, fast_schema, keywords); -criterion_main!(jsonschema_valid); diff --git a/jsonschema/benches/valico.rs b/jsonschema/benches/valico.rs deleted file mode 100644 index 220be035..00000000 --- a/jsonschema/benches/valico.rs +++ /dev/null @@ -1,98 +0,0 @@ -use bench_helpers::{bench_citm, bench_fast, bench_geojson, bench_keywords, bench_swagger}; -use codspeed_criterion_compat::{criterion_group, criterion_main, BenchmarkId, Criterion}; -use serde_json::Value; -use valico::json_schema; - -macro_rules! valico_bench { - ($c:tt, $name:expr, $schema:ident, $instance:ident) => {{ - let mut scope = json_schema::Scope::new(); - let compiled = scope - .compile_and_return($schema.clone(), false) - .expect("Valid schema"); - assert!(compiled.validate(&$instance).is_valid(), "Invalid instance"); - $c.bench_function(&format!("valico {}", $name), |b| { - // There is no specialized method for fast boolean return value - b.iter(|| compiled.validate(&$instance).is_valid()) - }); - }}; -} - -fn large_schemas(c: &mut Criterion) { - // Swagger JSON Schema - bench_swagger(&mut |name, schema, instance| valico_bench!(c, name, schema, instance)); - // Canada borders in GeoJSON - bench_geojson(&mut |name, schema, instance| valico_bench!(c, name, schema, instance)); - // CITM catalog - bench_citm(&mut |name, schema, instance| valico_bench!(c, name, schema, instance)); -} - -fn fast_schema(c: &mut Criterion) { - bench_fast(&mut |name, schema, valid, invalid| { - let mut scope = json_schema::Scope::new(); - let compiled = scope - .compile_and_return(schema.clone(), false) - .expect("Valid schema"); - c.bench_function(&format!("{} valico/compile", name), |b| { - b.iter(|| { - let mut scope = json_schema::Scope::new(); - scope - .compile_and_return(schema.clone(), false) - .expect("Valid schema"); - }) - }); - c.bench_function(&format!("{} valico/validate/valid", name), |b| { - b.iter(|| compiled.validate(&valid).is_valid()) - }); - c.bench_function(&format!("{} valico/validate/invalid", name), |b| { - b.iter(|| compiled.validate(&invalid).is_valid()) - }); - }); -} - -fn keywords(c: &mut Criterion) { - bench_keywords( - c, - &|_: &str| false, - &|schema: &Value, instance: &Value| { - let mut scope = json_schema::Scope::new(); - let compiled = scope - .compile_and_return(schema.clone(), false) - .expect("Valid schema"); - compiled.validate(instance).is_valid() - }, - &mut |c: &mut Criterion, name: &str, schema: &Value| { - c.bench_with_input( - BenchmarkId::new(name, "valico/compile"), - schema, - |b, schema| { - b.iter_with_setup( - || schema.clone(), - |schema| { - let mut scope = json_schema::Scope::new(); - scope - .compile_and_return(schema, false) - .expect("Valid schema"); - }, - ) - }, - ); - }, - validate, - validate, - ) -} - -fn validate(c: &mut Criterion, name: &str, schema: &Value, instance: &Value) { - let mut scope = json_schema::Scope::new(); - let compiled = scope - .compile_and_return(schema.clone(), false) - .expect("Valid schema"); - c.bench_with_input(BenchmarkId::new(name, "valico"), instance, |b, instance| { - b.iter(|| { - compiled.validate(instance).is_valid(); - }) - }); -} - -criterion_group!(valico, large_schemas, fast_schema, keywords); -criterion_main!(valico); diff --git a/jsonschema/rustfmt.toml b/jsonschema/rustfmt.toml deleted file mode 100644 index bf772232..00000000 --- a/jsonschema/rustfmt.toml +++ /dev/null @@ -1,2 +0,0 @@ -imports_granularity = "Crate" -edition = "2021" diff --git a/jsonschema/src/lib.rs b/jsonschema/src/lib.rs deleted file mode 100644 index 284e210c..00000000 --- a/jsonschema/src/lib.rs +++ /dev/null @@ -1,273 +0,0 @@ -//! # jsonschema -//! -//! A crate for performing fast JSON Schema validation. It is fast due to schema compilation into -//! a validation tree, which reduces runtime costs for working with schema parameters. -//! -//! Supports: -//! - JSON Schema drafts 4, 6, 7 (except some optional test cases); -//! - Loading remote documents via HTTP(S); -//! -//! Partially supported drafts (some keywords are not implemented): -//! - Draft 2019-09 (requires the `draft201909` feature enabled) -//! - Draft 2020-12 (requires the `draft202012` feature enabled) -//! -//! This library is functional and ready for use, but its API is still evolving to the 1.0 API. -//! -//! ## Usage Examples: -//! A schema can be compiled with two main flavours: -//! * using default configurations -//! ```rust -//! # use jsonschema::JSONSchema; -//! # use serde_json::json; -//! # fn foo() { -//! # let schema = json!({"maxLength": 5}); -//! let compiled_schema = JSONSchema::compile(&schema).expect("A valid schema"); -//! # } -//! ``` -//! * using custom configurations (such as define a Draft version) -//! ```rust -//! # use jsonschema::{Draft, JSONSchema}; -//! # use serde_json::json; -//! # fn foo() { -//! # let schema = json!({"maxLength": 5}); -//! let compiled_schema = JSONSchema::options() -//! .with_draft(Draft::Draft7) -//! .compile(&schema) -//! .expect("A valid schema"); -//! # } -//! ``` -//! -//! ## Example (CLI tool to highlight print errors) -//! ```rust -//! use jsonschema::{Draft, JSONSchema}; -//! use serde_json::json; -//! -//! let schema = json!({"maxLength": 5}); -//! let instance = json!("foo"); -//! let compiled = JSONSchema::options() -//! .with_draft(Draft::Draft7) -//! .compile(&schema) -//! .expect("A valid schema"); -//! let result = compiled.validate(&instance); -//! if let Err(errors) = result { -//! for error in errors { -//! println!("Validation error: {}", error); -//! println!("Instance path: {}", error.instance_path); -//! } -//! } -//! ``` -//! Each error has an `instance_path` attribute that indicates the path to the erroneous part within the validated instance. -//! It could be transformed to JSON Pointer via `.to_string()` or to `Vec` via `.into_vec()`. -#![warn( - clippy::cast_possible_truncation, - clippy::doc_markdown, - clippy::explicit_iter_loop, - clippy::map_unwrap_or, - clippy::match_same_arms, - clippy::needless_borrow, - clippy::needless_pass_by_value, - clippy::print_stdout, - clippy::redundant_closure, - clippy::trivially_copy_pass_by_ref, - clippy::missing_const_for_fn, - clippy::unseparated_literal_suffix, - missing_debug_implementations, - missing_docs, - trivial_casts, - trivial_numeric_casts, - unused_extern_crates, - unused_import_braces, - unused_qualifications, - unreachable_pub, - variant_size_differences -)] -#![allow( - clippy::unnecessary_wraps, - clippy::upper_case_acronyms, - clippy::needless_collect -)] -#![cfg_attr(not(test), allow(clippy::arithmetic_side_effects, clippy::unwrap_used))] -mod compilation; -mod content_encoding; -mod content_media_type; -pub mod error; -mod keywords; -pub mod output; -pub mod paths; -pub mod primitive_type; -pub(crate) mod properties; -mod resolver; -mod schema_node; -mod schemas; -mod validator; - -pub use compilation::{options::CompilationOptions, JSONSchema}; -pub use error::{ErrorIterator, ValidationError}; -pub use keywords::custom::Keyword; -pub use resolver::{SchemaResolver, SchemaResolverError}; -pub use schemas::Draft; - -use serde_json::Value; - -/// A shortcut for validating `instance` against `schema`. Draft version is detected automatically. -/// ```rust -/// use jsonschema::is_valid; -/// use serde_json::json; -/// -/// let schema = json!({"maxLength": 5}); -/// let instance = json!("foo"); -/// assert!(is_valid(&schema, &instance)); -/// ``` -/// -/// This function panics if an invalid schema is passed. -#[must_use] -#[inline] -pub fn is_valid(schema: &Value, instance: &Value) -> bool { - let compiled = JSONSchema::compile(schema).expect("Invalid schema"); - compiled.is_valid(instance) -} - -#[cfg(test)] -pub(crate) mod tests_util { - use super::JSONSchema; - use crate::ValidationError; - use serde_json::Value; - - pub(crate) fn is_not_valid_with(compiled: &JSONSchema, instance: &Value) { - assert!( - !compiled.is_valid(instance), - "{} should not be valid (via is_valid)", - instance - ); - assert!( - compiled.validate(instance).is_err(), - "{} should not be valid (via validate)", - instance - ); - assert!( - !compiled.apply(instance).basic().is_valid(), - "{} should not be valid (via apply)", - instance - ); - } - - pub(crate) fn is_not_valid(schema: &Value, instance: &Value) { - let compiled = JSONSchema::compile(schema).unwrap(); - is_not_valid_with(&compiled, instance) - } - - #[cfg(any(feature = "draft201909", feature = "draft202012"))] - pub(crate) fn is_not_valid_with_draft(draft: crate::Draft, schema: &Value, instance: &Value) { - let compiled = JSONSchema::options() - .with_draft(draft) - .compile(schema) - .unwrap(); - is_not_valid_with(&compiled, instance) - } - - pub(crate) fn expect_errors(schema: &Value, instance: &Value, errors: &[&str]) { - assert_eq!( - JSONSchema::compile(schema) - .expect("Should be a valid schema") - .validate(instance) - .expect_err(format!("{} should not be valid", instance).as_str()) - .map(|e| e.to_string()) - .collect::>(), - errors - ) - } - - pub(crate) fn is_valid_with(compiled: &JSONSchema, instance: &Value) { - if let Err(mut errors) = compiled.validate(instance) { - let first = errors.next().expect("Errors iterator is empty"); - panic!( - "{} should be valid (via validate). Error: {} at {}", - instance, first, first.instance_path - ); - } - assert!( - compiled.is_valid(instance), - "{} should be valid (via is_valid)", - instance - ); - assert!( - compiled.apply(instance).basic().is_valid(), - "{} should be valid (via apply)", - instance - ); - } - - pub(crate) fn is_valid(schema: &Value, instance: &Value) { - let compiled = JSONSchema::compile(schema).unwrap(); - is_valid_with(&compiled, instance); - } - - #[cfg(any(feature = "draft201909", feature = "draft202012"))] - pub(crate) fn is_valid_with_draft(draft: crate::Draft, schema: &Value, instance: &Value) { - let compiled = JSONSchema::options() - .with_draft(draft) - .compile(schema) - .unwrap(); - is_valid_with(&compiled, instance) - } - - pub(crate) fn validate(schema: &Value, instance: &Value) -> ValidationError<'static> { - let compiled = JSONSchema::compile(schema).unwrap(); - let err = compiled - .validate(instance) - .expect_err("Should be an error") - .next() - .expect("Should be an error") - .into_owned(); - err - } - - pub(crate) fn assert_schema_path(schema: &Value, instance: &Value, expected: &str) { - let error = validate(schema, instance); - assert_eq!(error.schema_path.to_string(), expected) - } - - pub(crate) fn assert_schema_paths(schema: &Value, instance: &Value, expected: &[&str]) { - let compiled = JSONSchema::compile(schema).unwrap(); - let errors = compiled.validate(instance).expect_err("Should be an error"); - for (error, schema_path) in errors.zip(expected) { - assert_eq!(error.schema_path.to_string(), *schema_path) - } - } -} - -#[cfg(test)] -mod tests { - use super::{is_valid, Draft, JSONSchema}; - use serde_json::json; - use test_case::test_case; - - #[test] - fn test_is_valid() { - let schema = json!({"minLength": 5}); - let valid = json!("foobar"); - let invalid = json!("foo"); - assert!(is_valid(&schema, &valid)); - assert!(!is_valid(&schema, &invalid)); - } - - #[test_case(Draft::Draft4)] - #[test_case(Draft::Draft6)] - #[test_case(Draft::Draft7)] - fn meta_schemas(draft: Draft) { - // See GH-258 - for schema in [json!({"enum": [0, 0.0]}), json!({"enum": []})] { - assert!(JSONSchema::options() - .with_draft(draft) - .compile(&schema) - .is_ok()) - } - } - - #[test] - fn incomplete_escape_in_pattern() { - // See GH-253 - let schema = json!({"pattern": "\\u"}); - assert!(JSONSchema::compile(&schema).is_err()) - } -} diff --git a/jsonschema/src/main.rs b/jsonschema/src/main.rs deleted file mode 100644 index a6a482dd..00000000 --- a/jsonschema/src/main.rs +++ /dev/null @@ -1,93 +0,0 @@ -#[cfg(not(feature = "cli"))] -fn main() { - eprintln!("`jsonschema` CLI is only available with the `cli` feature"); - std::process::exit(1); -} - -#[cfg(feature = "cli")] -fn main() -> Result<(), Box> { - use std::{ - fs::File, - io::BufReader, - path::{Path, PathBuf}, - process, - }; - - use clap::Parser; - use jsonschema::JSONSchema; - - #[derive(Parser)] - #[command(name = "jsonschema")] - struct Cli { - /// A path to a JSON instance (i.e. filename.json) to validate (may be specified multiple times). - #[arg(short = 'i', long = "instance")] - instances: Option>, - - /// The JSON Schema to validate with (i.e. schema.json). - #[arg(value_parser, required_unless_present("version"))] - schema: Option, - - /// Show program's version number and exit. - #[arg(short = 'v', long = "version")] - version: bool, - } - - fn read_json(path: &Path) -> serde_json::Result { - let file = File::open(path).expect("Failed to open file"); - let reader = BufReader::new(file); - serde_json::from_reader(reader) - } - - fn validate_instances( - instances: &[PathBuf], - schema_path: PathBuf, - ) -> Result> { - let mut success = true; - - let schema_json = read_json(&schema_path)?; - match JSONSchema::compile(&schema_json) { - Ok(schema) => { - for instance in instances { - let instance_json = read_json(instance)?; - let validation = schema.validate(&instance_json); - let filename = instance.to_string_lossy(); - match validation { - Ok(_) => println!("{} - VALID", filename), - Err(errors) => { - success = false; - - println!("{} - INVALID. Errors:", filename); - for (i, e) in errors.enumerate() { - println!("{}. {}", i + 1, e); - } - } - } - } - } - Err(error) => { - println!("Schema is invalid. Error: {}", error); - success = false; - } - } - Ok(success) - } - let config = Cli::parse(); - - if config.version { - println!(concat!("Version: ", env!("CARGO_PKG_VERSION"))); - return Ok(()); - } - - let mut success = true; - if let Some(schema) = config.schema { - if let Some(instances) = config.instances { - success = validate_instances(&instances, schema)?; - } - } - - if !success { - process::exit(1); - } - - Ok(()) -} diff --git a/jsonschema/tests/draft7_instance_paths.json b/jsonschema/tests/draft7_instance_paths.json deleted file mode 100644 index e773257a..00000000 --- a/jsonschema/tests/draft7_instance_paths.json +++ /dev/null @@ -1,2866 +0,0 @@ -{ - "additionalItems.json": [ - { - "suite_id": 0, - "tests": [ - { - "id": 1, - "instance_path": [ - "3" - ] - } - ] - }, - { - "suite_id": 2, - "tests": [ - { - "id": 4, - "instance_path": [] - } - ] - }, - { - "suite_id": 6, - "tests": [ - { - "id": 0, - "instance_path": [ - "1" - ] - } - ] - }, - { - "suite_id": 7, - "tests": [ - { - "id": 1, - "instance_path": [ - "1" - ] - } - ] - } - ], - "additionalProperties.json": [ - { - "suite_id": 0, - "tests": [ - { - "id": 1, - "instance_path": [] - } - ] - }, - { - "suite_id": 1, - "tests": [ - { - "id": 1, - "instance_path": [] - } - ] - }, - { - "suite_id": 2, - "tests": [ - { - "id": 2, - "instance_path": [ - "quux" - ] - } - ] - }, - { - "suite_id": 3, - "tests": [ - { - "id": 1, - "instance_path": [ - "foo" - ] - } - ] - }, - { - "suite_id": 5, - "tests": [ - { - "id": 0, - "instance_path": [ - "foo" - ] - } - ] - } - ], - "allOf.json": [ - { - "suite_id": 0, - "tests": [ - { - "id": 1, - "instance_path": [] - }, - { - "id": 2, - "instance_path": [] - }, - { - "id": 3, - "instance_path": [ - "bar" - ] - } - ] - }, - { - "suite_id": 1, - "tests": [ - { - "id": 1, - "instance_path": [] - }, - { - "id": 2, - "instance_path": [] - }, - { - "id": 3, - "instance_path": [] - }, - { - "id": 4, - "instance_path": [] - } - ] - }, - { - "suite_id": 2, - "tests": [ - { - "id": 1, - "instance_path": [] - } - ] - }, - { - "suite_id": 4, - "tests": [ - { - "id": 0, - "instance_path": [] - } - ] - }, - { - "suite_id": 5, - "tests": [ - { - "id": 0, - "instance_path": [] - } - ] - }, - { - "suite_id": 8, - "tests": [ - { - "id": 1, - "instance_path": [] - } - ] - }, - { - "suite_id": 9, - "tests": [ - { - "id": 1, - "instance_path": [] - } - ] - }, - { - "suite_id": 10, - "tests": [ - { - "id": 1, - "instance_path": [] - } - ] - }, - { - "suite_id": 11, - "tests": [ - { - "id": 0, - "instance_path": [] - }, - { - "id": 1, - "instance_path": [] - }, - { - "id": 2, - "instance_path": [] - }, - { - "id": 3, - "instance_path": [] - }, - { - "id": 4, - "instance_path": [] - }, - { - "id": 5, - "instance_path": [] - }, - { - "id": 6, - "instance_path": [] - } - ] - } - ], - "anyOf.json": [ - { - "suite_id": 0, - "tests": [ - { - "id": 3, - "instance_path": [] - } - ] - }, - { - "suite_id": 1, - "tests": [ - { - "id": 0, - "instance_path": [] - }, - { - "id": 2, - "instance_path": [] - } - ] - }, - { - "suite_id": 4, - "tests": [ - { - "id": 0, - "instance_path": [] - } - ] - }, - { - "suite_id": 5, - "tests": [ - { - "id": 3, - "instance_path": [] - } - ] - }, - { - "suite_id": 7, - "tests": [ - { - "id": 1, - "instance_path": [] - } - ] - }, - { - "suite_id": 8, - "tests": [ - { - "id": 1, - "instance_path": [] - } - ] - } - ], - "boolean_schema.json": [ - { - "suite_id": 1, - "tests": [ - { - "id": 0, - "instance_path": [] - }, - { - "id": 1, - "instance_path": [] - }, - { - "id": 2, - "instance_path": [] - }, - { - "id": 3, - "instance_path": [] - }, - { - "id": 4, - "instance_path": [] - }, - { - "id": 5, - "instance_path": [] - }, - { - "id": 6, - "instance_path": [] - }, - { - "id": 7, - "instance_path": [] - }, - { - "id": 8, - "instance_path": [] - } - ] - } - ], - "const.json": [ - { - "suite_id": 0, - "tests": [ - { - "id": 1, - "instance_path": [] - }, - { - "id": 2, - "instance_path": [] - } - ] - }, - { - "suite_id": 1, - "tests": [ - { - "id": 2, - "instance_path": [] - }, - { - "id": 3, - "instance_path": [] - } - ] - }, - { - "suite_id": 2, - "tests": [ - { - "id": 1, - "instance_path": [] - }, - { - "id": 2, - "instance_path": [] - } - ] - }, - { - "suite_id": 3, - "tests": [ - { - "id": 1, - "instance_path": [] - } - ] - }, - { - "suite_id": 4, - "tests": [ - { - "id": 1, - "instance_path": [] - }, - { - "id": 2, - "instance_path": [] - } - ] - }, - { - "suite_id": 5, - "tests": [ - { - "id": 1, - "instance_path": [] - }, - { - "id": 2, - "instance_path": [] - } - ] - }, - { - "suite_id": 6, - "tests": [ - { - "id": 1, - "instance_path": [] - }, - { - "id": 2, - "instance_path": [] - } - ] - }, - { - "suite_id": 7, - "tests": [ - { - "id": 1, - "instance_path": [] - }, - { - "id": 2, - "instance_path": [] - } - ] - }, - { - "suite_id": 8, - "tests": [ - { - "id": 1, - "instance_path": [] - }, - { - "id": 2, - "instance_path": [] - } - ] - }, - { - "suite_id": 9, - "tests": [ - { - "id": 1, - "instance_path": [] - }, - { - "id": 2, - "instance_path": [] - } - ] - }, - { - "suite_id": 10, - "tests": [ - { - "id": 0, - "instance_path": [] - }, - { - "id": 3, - "instance_path": [] - }, - { - "id": 4, - "instance_path": [] - }, - { - "id": 5, - "instance_path": [] - } - ] - }, - { - "suite_id": 11, - "tests": [ - { - "id": 0, - "instance_path": [] - } - ] - }, - { - "suite_id": 12, - "tests": [ - { - "id": 1, - "instance_path": [] - }, - { - "id": 3, - "instance_path": [] - }, - { - "id": 4, - "instance_path": [] - } - ] - }, - { - "suite_id": 13, - "tests": [ - { - "id": 1, - "instance_path": [] - }, - { - "id": 3, - "instance_path": [] - } - ] - }, - { - "suite_id": 14, - "tests": [ - { - "id": 1, - "instance_path": [] - } - ] - } - ], - "contains.json": [ - { - "suite_id": 0, - "tests": [ - { - "id": 3, - "instance_path": [] - }, - { - "id": 4, - "instance_path": [] - } - ] - }, - { - "suite_id": 1, - "tests": [ - { - "id": 2, - "instance_path": [] - } - ] - }, - { - "suite_id": 2, - "tests": [ - { - "id": 1, - "instance_path": [] - } - ] - }, - { - "suite_id": 3, - "tests": [ - { - "id": 0, - "instance_path": [] - }, - { - "id": 1, - "instance_path": [] - } - ] - }, - { - "suite_id": 4, - "tests": [ - { - "id": 0, - "instance_path": [] - }, - { - "id": 1, - "instance_path": [ - "0" - ] - }, - { - "id": 3, - "instance_path": [] - } - ] - } - ], - "default.json": [ - { - "suite_id": 2, - "tests": [ - { - "id": 1, - "instance_path": [ - "alpha" - ] - } - ] - } - ], - "definitions.json": [ - { - "suite_id": 0, - "tests": [ - { - "id": 1, - "instance_path": [ - "definitions", - "foo", - "type" - ] - } - ] - } - ], - "dependencies.json": [ - { - "suite_id": 0, - "tests": [ - { - "id": 3, - "instance_path": [] - } - ] - }, - { - "suite_id": 2, - "tests": [ - { - "id": 3, - "instance_path": [] - }, - { - "id": 4, - "instance_path": [] - }, - { - "id": 5, - "instance_path": [] - } - ] - }, - { - "suite_id": 3, - "tests": [ - { - "id": 2, - "instance_path": [ - "foo" - ] - }, - { - "id": 3, - "instance_path": [ - "bar" - ] - }, - { - "id": 4, - "instance_path": [ - "bar" - ] - } - ] - }, - { - "suite_id": 4, - "tests": [ - { - "id": 1, - "instance_path": [] - }, - { - "id": 2, - "instance_path": [] - } - ] - }, - { - "suite_id": 5, - "tests": [ - { - "id": 3, - "instance_path": [] - }, - { - "id": 4, - "instance_path": [] - }, - { - "id": 5, - "instance_path": [] - }, - { - "id": 6, - "instance_path": [] - } - ] - } - ], - "enum.json": [ - { - "suite_id": 0, - "tests": [ - { - "id": 1, - "instance_path": [] - } - ] - }, - { - "suite_id": 1, - "tests": [ - { - "id": 1, - "instance_path": [] - }, - { - "id": 2, - "instance_path": [] - }, - { - "id": 4, - "instance_path": [] - } - ] - }, - { - "suite_id": 2, - "tests": [ - { - "id": 2, - "instance_path": [] - } - ] - }, - { - "suite_id": 3, - "tests": [ - { - "id": 1, - "instance_path": [ - "foo" - ] - }, - { - "id": 2, - "instance_path": [ - "bar" - ] - }, - { - "id": 4, - "instance_path": [] - }, - { - "id": 5, - "instance_path": [] - } - ] - }, - { - "suite_id": 4, - "tests": [ - { - "id": 2, - "instance_path": [] - } - ] - }, - { - "suite_id": 5, - "tests": [ - { - "id": 1, - "instance_path": [] - }, - { - "id": 2, - "instance_path": [] - } - ] - }, - { - "suite_id": 6, - "tests": [ - { - "id": 1, - "instance_path": [] - }, - { - "id": 2, - "instance_path": [] - } - ] - }, - { - "suite_id": 7, - "tests": [ - { - "id": 0, - "instance_path": [] - } - ] - }, - { - "suite_id": 8, - "tests": [ - { - "id": 0, - "instance_path": [] - } - ] - }, - { - "suite_id": 9, - "tests": [ - { - "id": 1, - "instance_path": [] - } - ] - } - ], - "exclusiveMaximum.json": [ - { - "suite_id": 0, - "tests": [ - { - "id": 1, - "instance_path": [] - }, - { - "id": 2, - "instance_path": [] - } - ] - } - ], - "exclusiveMinimum.json": [ - { - "suite_id": 0, - "tests": [ - { - "id": 1, - "instance_path": [] - }, - { - "id": 2, - "instance_path": [] - } - ] - } - ], - "id.json": [ - { - "suite_id": 0, - "tests": [ - { - "id": 2, - "instance_path": [] - } - ] - } - ], - "if-then-else.json": [ - { - "suite_id": 3, - "tests": [ - { - "id": 1, - "instance_path": [] - } - ] - }, - { - "suite_id": 4, - "tests": [ - { - "id": 2, - "instance_path": [] - } - ] - }, - { - "suite_id": 5, - "tests": [ - { - "id": 1, - "instance_path": [] - }, - { - "id": 3, - "instance_path": [] - } - ] - }, - { - "suite_id": 7, - "tests": [ - { - "id": 1, - "instance_path": [] - } - ] - }, - { - "suite_id": 8, - "tests": [ - { - "id": 0, - "instance_path": [] - } - ] - }, - { - "suite_id": 9, - "tests": [ - { - "id": 2, - "instance_path": [] - }, - { - "id": 3, - "instance_path": [] - } - ] - } - ], - "infinite-loop-detection.json": [ - { - "suite_id": 0, - "tests": [ - { - "id": 1, - "instance_path": [ - "foo" - ] - } - ] - } - ], - "items.json": [ - { - "suite_id": 0, - "tests": [ - { - "id": 1, - "instance_path": [ - "1" - ] - } - ] - }, - { - "suite_id": 1, - "tests": [ - { - "id": 1, - "instance_path": [ - "0" - ] - } - ] - }, - { - "suite_id": 3, - "tests": [ - { - "id": 0, - "instance_path": [ - "0" - ] - } - ] - }, - { - "suite_id": 4, - "tests": [ - { - "id": 1, - "instance_path": [ - "1" - ] - } - ] - }, - { - "suite_id": 5, - "tests": [ - { - "id": 1, - "instance_path": [] - }, - { - "id": 2, - "instance_path": [ - "0" - ] - }, - { - "id": 3, - "instance_path": [ - "0" - ] - }, - { - "id": 4, - "instance_path": [ - "0", "0" - ] - } - ] - }, - { - "suite_id": 6, - "tests": [ - { - "id": 1, - "instance_path": [ - "0", "0", "0", "0" - ] - }, - { - "id": 2, - "instance_path": [ - "0", "0", "0" - ] - } - ] - } - ], - "maxItems.json": [ - { - "suite_id": 0, - "tests": [ - { - "id": 2, - "instance_path": [] - } - ] - } - ], - "maxLength.json": [ - { - "suite_id": 0, - "tests": [ - { - "id": 2, - "instance_path": [] - } - ] - } - ], - "maxProperties.json": [ - { - "suite_id": 0, - "tests": [ - { - "id": 2, - "instance_path": [] - } - ] - }, - { - "suite_id": 1, - "tests": [ - { - "id": 1, - "instance_path": [] - } - ] - } - ], - "maximum.json": [ - { - "suite_id": 0, - "tests": [ - { - "id": 2, - "instance_path": [] - } - ] - }, - { - "suite_id": 1, - "tests": [ - { - "id": 3, - "instance_path": [] - } - ] - } - ], - "minItems.json": [ - { - "suite_id": 0, - "tests": [ - { - "id": 2, - "instance_path": [] - } - ] - } - ], - "minLength.json": [ - { - "suite_id": 0, - "tests": [ - { - "id": 2, - "instance_path": [] - }, - { - "id": 4, - "instance_path": [] - } - ] - } - ], - "minProperties.json": [ - { - "suite_id": 0, - "tests": [ - { - "id": 2, - "instance_path": [] - } - ] - } - ], - "minimum.json": [ - { - "suite_id": 0, - "tests": [ - { - "id": 2, - "instance_path": [] - } - ] - }, - { - "suite_id": 1, - "tests": [ - { - "id": 4, - "instance_path": [] - }, - { - "id": 5, - "instance_path": [] - } - ] - } - ], - "multipleOf.json": [ - { - "suite_id": 0, - "tests": [ - { - "id": 1, - "instance_path": [] - } - ] - }, - { - "suite_id": 1, - "tests": [ - { - "id": 2, - "instance_path": [] - } - ] - }, - { - "suite_id": 2, - "tests": [ - { - "id": 1, - "instance_path": [] - } - ] - }, - { - "suite_id": 3, - "tests": [ - { - "id": 0, - "instance_path": [] - } - ] - } - ], - "not.json": [ - { - "suite_id": 0, - "tests": [ - { - "id": 1, - "instance_path": [] - } - ] - }, - { - "suite_id": 1, - "tests": [ - { - "id": 1, - "instance_path": [] - }, - { - "id": 2, - "instance_path": [] - } - ] - }, - { - "suite_id": 2, - "tests": [ - { - "id": 2, - "instance_path": [] - } - ] - }, - { - "suite_id": 3, - "tests": [ - { - "id": 0, - "instance_path": [ - "foo" - ] - } - ] - }, - { - "suite_id": 4, - "tests": [ - { - "id": 0, - "instance_path": [] - } - ] - } - ], - "oneOf.json": [ - { - "suite_id": 0, - "tests": [ - { - "id": 2, - "instance_path": [] - }, - { - "id": 3, - "instance_path": [] - } - ] - }, - { - "suite_id": 1, - "tests": [ - { - "id": 0, - "instance_path": [] - }, - { - "id": 2, - "instance_path": [] - } - ] - }, - { - "suite_id": 2, - "tests": [ - { - "id": 0, - "instance_path": [] - } - ] - }, - { - "suite_id": 4, - "tests": [ - { - "id": 0, - "instance_path": [] - } - ] - }, - { - "suite_id": 5, - "tests": [ - { - "id": 0, - "instance_path": [] - } - ] - }, - { - "suite_id": 6, - "tests": [ - { - "id": 2, - "instance_path": [] - }, - { - "id": 3, - "instance_path": [] - } - ] - }, - { - "suite_id": 7, - "tests": [ - { - "id": 1, - "instance_path": [] - } - ] - }, - { - "suite_id": 8, - "tests": [ - { - "id": 0, - "instance_path": [] - }, - { - "id": 3, - "instance_path": [] - } - ] - }, - { - "suite_id": 9, - "tests": [ - { - "id": 2, - "instance_path": [] - }, - { - "id": 3, - "instance_path": [] - } - ] - }, - { - "suite_id": 10, - "tests": [ - { - "id": 1, - "instance_path": [] - } - ] - } - ], - "optional/bignum.json": [ - { - "suite_id": 4, - "tests": [ - { - "id": 0, - "instance_path": [] - } - ] - }, - { - "suite_id": 6, - "tests": [ - { - "id": 0, - "instance_path": [] - } - ] - }, - { - "suite_id": 8, - "tests": [ - { - "id": 0, - "instance_path": [] - } - ] - } - ], - "optional/content.json": [ - { - "suite_id": 0, - "tests": [ - { - "id": 1, - "instance_path": [] - } - ] - }, - { - "suite_id": 1, - "tests": [ - { - "id": 1, - "instance_path": [] - } - ] - }, - { - "suite_id": 2, - "tests": [ - { - "id": 1, - "instance_path": [] - }, - { - "id": 2, - "instance_path": [] - } - ] - } - ], - "optional/ecmascript-regex.json": [ - { - "suite_id": 0, - "tests": [ - { - "id": 0, - "instance_path": [] - } - ] - }, - { - "suite_id": 1, - "tests": [ - { - "id": 0, - "instance_path": [] - } - ] - }, - { - "suite_id": 2, - "tests": [ - { - "id": 0, - "instance_path": [] - } - ] - }, - { - "suite_id": 3, - "tests": [ - { - "id": 0, - "instance_path": [] - } - ] - }, - { - "suite_id": 4, - "tests": [ - { - "id": 1, - "instance_path": [] - }, - { - "id": 2, - "instance_path": [] - } - ] - }, - { - "suite_id": 5, - "tests": [ - { - "id": 0, - "instance_path": [] - } - ] - }, - { - "suite_id": 6, - "tests": [ - { - "id": 1, - "instance_path": [] - } - ] - }, - { - "suite_id": 7, - "tests": [ - { - "id": 0, - "instance_path": [] - } - ] - }, - { - "suite_id": 8, - "tests": [ - { - "id": 9, - "instance_path": [] - }, - { - "id": 10, - "instance_path": [] - } - ] - }, - { - "suite_id": 9, - "tests": [ - { - "id": 0, - "instance_path": [] - }, - { - "id": 1, - "instance_path": [] - }, - { - "id": 2, - "instance_path": [] - }, - { - "id": 3, - "instance_path": [] - }, - { - "id": 4, - "instance_path": [] - }, - { - "id": 5, - "instance_path": [] - }, - { - "id": 6, - "instance_path": [] - }, - { - "id": 7, - "instance_path": [] - }, - { - "id": 8, - "instance_path": [] - } - ] - } - ], - "optional/format/date-time.json": [ - { - "suite_id": 0, - "tests": [ - { - "id": 4, - "instance_path": [] - }, - { - "id": 5, - "instance_path": [] - }, - { - "id": 6, - "instance_path": [] - }, - { - "id": 8, - "instance_path": [] - }, - { - "id": 9, - "instance_path": [] - }, - { - "id": 10, - "instance_path": [] - } - ] - } - ], - "optional/format/date.json": [ - { - "suite_id": 0, - "tests": [ - { - "id": 1, - "instance_path": [] - }, - { - "id": 2, - "instance_path": [] - }, - { - "id": 3, - "instance_path": [] - }, - { - "id": 4, - "instance_path": [] - } - ] - } - ], - "optional/format/hostname.json": [ - { - "suite_id": 0, - "tests": [ - { - "id": 2, - "instance_path": [] - }, - { - "id": 3, - "instance_path": [] - }, - { - "id": 4, - "instance_path": [] - }, - { - "id": 5, - "instance_path": [] - }, - { - "id": 6, - "instance_path": [] - }, - { - "id": 7, - "instance_path": [] - }, - { - "id": 8, - "instance_path": [] - }, - { - "id": 9, - "instance_path": [] - }, - { - "id": 11, - "instance_path": [] - } - ] - } - ], - "optional/format/idn-email.json": [ - { - "suite_id": 0, - "tests": [ - { - "id": 1, - "instance_path": [] - }, - { - "id": 3, - "instance_path": [] - } - ] - } - ], - "optional/format/ipv4.json": [ - { - "suite_id": 0, - "tests": [ - { - "id": 1, - "instance_path": [] - }, - { - "id": 2, - "instance_path": [] - }, - { - "id": 3, - "instance_path": [] - }, - { - "id": 4, - "instance_path": [] - }, - { - "id": 5, - "instance_path": [] - }, - { - "id": 6, - "instance_path": [] - } - ] - } - ], - "optional/format/ipv6.json": [ - { - "suite_id": 0, - "tests": [ - { - "id": 1, - "instance_path": [] - }, - { - "id": 2, - "instance_path": [] - }, - { - "id": 3, - "instance_path": [] - }, - { - "id": 7, - "instance_path": [] - }, - { - "id": 8, - "instance_path": [] - }, - { - "id": 9, - "instance_path": [] - }, - { - "id": 10, - "instance_path": [] - }, - { - "id": 13, - "instance_path": [] - }, - { - "id": 14, - "instance_path": [] - }, - { - "id": 16, - "instance_path": [] - }, - { - "id": 18, - "instance_path": [] - }, - { - "id": 19, - "instance_path": [] - }, - { - "id": 20, - "instance_path": [] - }, - { - "id": 21, - "instance_path": [] - }, - { - "id": 22, - "instance_path": [] - }, - { - "id": 23, - "instance_path": [] - }, - { - "id": 24, - "instance_path": [] - }, - { - "id": 25, - "instance_path": [] - }, - { - "id": 27, - "instance_path": [] - }, - { - "id": 28, - "instance_path": [] - } - ] - } - ], - "optional/format/iri-reference.json": [ - { - "suite_id": 0, - "tests": [ - { - "id": 3, - "instance_path": [] - }, - { - "id": 6, - "instance_path": [] - } - ] - } - ], - "optional/format/iri.json": [ - { - "suite_id": 0, - "tests": [ - { - "id": 5, - "instance_path": [] - }, - { - "id": 6, - "instance_path": [] - }, - { - "id": 7, - "instance_path": [] - }, - { - "id": 8, - "instance_path": [] - } - ] - } - ], - "optional/format/json-pointer.json": [ - { - "suite_id": 0, - "tests": [ - { - "id": 1, - "instance_path": [] - }, - { - "id": 21, - "instance_path": [] - }, - { - "id": 22, - "instance_path": [] - }, - { - "id": 23, - "instance_path": [] - }, - { - "id": 24, - "instance_path": [] - }, - { - "id": 25, - "instance_path": [] - }, - { - "id": 26, - "instance_path": [] - }, - { - "id": 27, - "instance_path": [] - }, - { - "id": 28, - "instance_path": [] - }, - { - "id": 29, - "instance_path": [] - }, - { - "id": 30, - "instance_path": [] - }, - { - "id": 31, - "instance_path": [] - } - ] - } - ], - "optional/format/regex.json": [ - { - "suite_id": 0, - "tests": [ - { - "id": 1, - "instance_path": [] - } - ] - } - ], - "optional/format/relative-json-pointer.json": [ - { - "suite_id": 0, - "tests": [ - { - "id": 4, - "instance_path": [] - }, - { - "id": 5, - "instance_path": [] - } - ] - } - ], - "optional/format/time.json": [ - { - "suite_id": 0, - "tests": [ - { - "id": 1, - "instance_path": [] - }, - { - "id": 2, - "instance_path": [] - } - ] - } - ], - "optional/format/uri-reference.json": [ - { - "suite_id": 0, - "tests": [ - { - "id": 3, - "instance_path": [] - }, - { - "id": 6, - "instance_path": [] - } - ] - } - ], - "optional/format/uri-template.json": [ - { - "suite_id": 0, - "tests": [ - { - "id": 1, - "instance_path": [] - } - ] - } - ], - "optional/format/uri.json": [ - { - "suite_id": 0, - "tests": [ - { - "id": 13, - "instance_path": [] - }, - { - "id": 14, - "instance_path": [] - }, - { - "id": 15, - "instance_path": [] - }, - { - "id": 16, - "instance_path": [] - }, - { - "id": 17, - "instance_path": [] - }, - { - "id": 18, - "instance_path": [] - }, - { - "id": 19, - "instance_path": [] - } - ] - } - ], - "optional/non-bmp-regex.json": [ - { - "suite_id": 0, - "tests": [ - { - "id": 3, - "instance_path": [] - }, - { - "id": 4, - "instance_path": [] - }, - { - "id": 5, - "instance_path": [] - }, - { - "id": 6, - "instance_path": [] - } - ] - }, - { - "suite_id": 1, - "tests": [ - { - "id": 3, - "instance_path": [ - "\uD83D\uDC32" - ] - }, - { - "id": 4, - "instance_path": [ - "\uD83D\uDC32\uD83D\uDC32" - ] - } - ] - } - ], - "pattern.json": [ - { - "suite_id": 0, - "tests": [ - { - "id": 1, - "instance_path": [] - } - ] - } - ], - "patternProperties.json": [ - { - "suite_id": 0, - "tests": [ - { - "id": 2, - "instance_path": [ - "foo" - ] - }, - { - "id": 3, - "instance_path": [ - "foo" - ] - } - ] - }, - { - "suite_id": 1, - "tests": [ - { - "id": 3, - "instance_path": [ - "a" - ] - }, - { - "id": 4, - "instance_path": [ - "aaaa" - ] - }, - { - "id": 5, - "instance_path": [ - "aaa" - ] - } - ] - }, - { - "suite_id": 2, - "tests": [ - { - "id": 1, - "instance_path": [ - "a31b" - ] - }, - { - "id": 3, - "instance_path": [ - "a_X_3" - ] - } - ] - }, - { - "suite_id": 3, - "tests": [ - { - "id": 1, - "instance_path": [ - "bar" - ] - }, - { - "id": 2, - "instance_path": [ - "bar" - ] - }, - { - "id": 3, - "instance_path": [ - "foobar" - ] - } - ] - } - ], - "properties.json": [ - { - "suite_id": 0, - "tests": [ - { - "id": 1, - "instance_path": [ - "bar" - ] - }, - { - "id": 2, - "instance_path": [ - "bar" - ] - } - ] - }, - { - "suite_id": 1, - "tests": [ - { - "id": 1, - "instance_path": [ - "foo" - ] - }, - { - "id": 2, - "instance_path": [ - "foo" - ] - }, - { - "id": 4, - "instance_path": [ - "fxo" - ] - }, - { - "id": 7, - "instance_path": [ - "quux" - ] - } - ] - }, - { - "suite_id": 2, - "tests": [ - { - "id": 2, - "instance_path": [ - "bar" - ] - }, - { - "id": 3, - "instance_path": [ - "bar" - ] - } - ] - }, - { - "suite_id": 3, - "tests": [ - { - "id": 1, - "instance_path": [ - "foo\tbar" - ] - } - ] - } - ], - "propertyNames.json": [ - { - "suite_id": 0, - "tests": [ - { - "id": 1, - "instance_path": [] - } - ] - }, - { - "suite_id": 2, - "tests": [ - { - "id": 0, - "instance_path": [] - } - ] - } - ], - "ref.json": [ - { - "suite_id": 0, - "tests": [ - { - "id": 2, - "instance_path": [] - }, - { - "id": 3, - "instance_path": [ - "foo" - ] - } - ] - }, - { - "suite_id": 1, - "tests": [ - { - "id": 1, - "instance_path": [ - "bar" - ] - } - ] - }, - { - "suite_id": 2, - "tests": [ - { - "id": 1, - "instance_path": [ - "1" - ] - } - ] - }, - { - "suite_id": 3, - "tests": [ - { - "id": 0, - "instance_path": [ - "slash" - ] - }, - { - "id": 1, - "instance_path": [ - "tilde" - ] - }, - { - "id": 2, - "instance_path": [ - "percent" - ] - } - ] - }, - { - "suite_id": 4, - "tests": [ - { - "id": 1, - "instance_path": [] - } - ] - }, - { - "suite_id": 5, - "tests": [ - { - "id": 2, - "instance_path": [ - "foo" - ] - } - ] - }, - { - "suite_id": 6, - "tests": [ - { - "id": 1, - "instance_path": [ - "minLength" - ] - } - ] - }, - { - "suite_id": 7, - "tests": [ - { - "id": 1, - "instance_path": [ - "$ref" - ] - } - ] - }, - { - "suite_id": 8, - "tests": [ - { - "id": 1, - "instance_path": [ - "$ref" - ] - } - ] - }, - { - "suite_id": 10, - "tests": [ - { - "id": 0, - "instance_path": [] - } - ] - }, - { - "suite_id": 11, - "tests": [ - { - "id": 1, - "instance_path": [ - "nodes", "0", "subtree", "nodes", "0", "value" - ] - } - ] - }, - { - "suite_id": 12, - "tests": [ - { - "id": 1, - "instance_path": [ - "foo\"bar" - ] - } - ] - }, - { - "suite_id": 13, - "tests": [ - { - "id": 1, - "instance_path": [] - } - ] - }, - { - "suite_id": 14, - "tests": [ - { - "id": 1, - "instance_path": [] - } - ] - }, - { - "suite_id": 15, - "tests": [ - { - "id": 1, - "instance_path": [] - } - ] - }, - { - "suite_id": 16, - "tests": [ - { - "id": 0, - "instance_path": [] - }, - { - "id": 1, - "instance_path": [] - } - ] - } - ], - "refRemote.json": [ - { - "suite_id": 0, - "tests": [ - { - "id": 1, - "instance_path": [] - } - ] - }, - { - "suite_id": 1, - "tests": [ - { - "id": 1, - "instance_path": [] - } - ] - }, - { - "suite_id": 2, - "tests": [ - { - "id": 1, - "instance_path": [] - } - ] - }, - { - "suite_id": 3, - "tests": [ - { - "id": 1, - "instance_path": [ - "0", "0" - ] - } - ] - }, - { - "suite_id": 4, - "tests": [ - { - "id": 1, - "instance_path": [ - "list", "0" - ] - } - ] - }, - { - "suite_id": 5, - "tests": [ - { - "id": 1, - "instance_path": [ - "list", "0" - ] - } - ] - }, - { - "suite_id": 6, - "tests": [ - { - "id": 2, - "instance_path": [ - "name" - ] - } - ] - } - ], - "required.json": [ - { - "suite_id": 0, - "tests": [ - { - "id": 1, - "instance_path": [] - } - ] - }, - { - "suite_id": 3, - "tests": [ - { - "id": 1, - "instance_path": [] - } - ] - } - ], - "type.json": [ - { - "suite_id": 0, - "tests": [ - { - "id": 2, - "instance_path": [] - }, - { - "id": 3, - "instance_path": [] - }, - { - "id": 4, - "instance_path": [] - }, - { - "id": 5, - "instance_path": [] - }, - { - "id": 6, - "instance_path": [] - }, - { - "id": 7, - "instance_path": [] - }, - { - "id": 8, - "instance_path": [] - } - ] - }, - { - "suite_id": 1, - "tests": [ - { - "id": 3, - "instance_path": [] - }, - { - "id": 4, - "instance_path": [] - }, - { - "id": 5, - "instance_path": [] - }, - { - "id": 6, - "instance_path": [] - }, - { - "id": 7, - "instance_path": [] - }, - { - "id": 8, - "instance_path": [] - } - ] - }, - { - "suite_id": 2, - "tests": [ - { - "id": 0, - "instance_path": [] - }, - { - "id": 1, - "instance_path": [] - }, - { - "id": 5, - "instance_path": [] - }, - { - "id": 6, - "instance_path": [] - }, - { - "id": 7, - "instance_path": [] - }, - { - "id": 8, - "instance_path": [] - } - ] - }, - { - "suite_id": 3, - "tests": [ - { - "id": 0, - "instance_path": [] - }, - { - "id": 1, - "instance_path": [] - }, - { - "id": 2, - "instance_path": [] - }, - { - "id": 4, - "instance_path": [] - }, - { - "id": 5, - "instance_path": [] - }, - { - "id": 6, - "instance_path": [] - } - ] - }, - { - "suite_id": 4, - "tests": [ - { - "id": 0, - "instance_path": [] - }, - { - "id": 1, - "instance_path": [] - }, - { - "id": 2, - "instance_path": [] - }, - { - "id": 3, - "instance_path": [] - }, - { - "id": 5, - "instance_path": [] - }, - { - "id": 6, - "instance_path": [] - } - ] - }, - { - "suite_id": 5, - "tests": [ - { - "id": 0, - "instance_path": [] - }, - { - "id": 1, - "instance_path": [] - }, - { - "id": 2, - "instance_path": [] - }, - { - "id": 3, - "instance_path": [] - }, - { - "id": 4, - "instance_path": [] - }, - { - "id": 5, - "instance_path": [] - }, - { - "id": 6, - "instance_path": [] - }, - { - "id": 9, - "instance_path": [] - } - ] - }, - { - "suite_id": 6, - "tests": [ - { - "id": 0, - "instance_path": [] - }, - { - "id": 1, - "instance_path": [] - }, - { - "id": 2, - "instance_path": [] - }, - { - "id": 3, - "instance_path": [] - }, - { - "id": 4, - "instance_path": [] - }, - { - "id": 5, - "instance_path": [] - }, - { - "id": 6, - "instance_path": [] - }, - { - "id": 7, - "instance_path": [] - }, - { - "id": 8, - "instance_path": [] - } - ] - }, - { - "suite_id": 7, - "tests": [ - { - "id": 2, - "instance_path": [] - }, - { - "id": 3, - "instance_path": [] - }, - { - "id": 4, - "instance_path": [] - }, - { - "id": 5, - "instance_path": [] - }, - { - "id": 6, - "instance_path": [] - } - ] - }, - { - "suite_id": 8, - "tests": [ - { - "id": 1, - "instance_path": [] - } - ] - }, - { - "suite_id": 9, - "tests": [ - { - "id": 2, - "instance_path": [] - }, - { - "id": 3, - "instance_path": [] - }, - { - "id": 4, - "instance_path": [] - } - ] - }, - { - "suite_id": 10, - "tests": [ - { - "id": 3, - "instance_path": [] - }, - { - "id": 4, - "instance_path": [] - } - ] - } - ], - "uniqueItems.json": [ - { - "suite_id": 0, - "tests": [ - { - "id": 1, - "instance_path": [] - }, - { - "id": 2, - "instance_path": [] - }, - { - "id": 6, - "instance_path": [] - }, - { - "id": 8, - "instance_path": [] - }, - { - "id": 10, - "instance_path": [] - }, - { - "id": 18, - "instance_path": [] - }, - { - "id": 20, - "instance_path": [] - } - ] - }, - { - "suite_id": 1, - "tests": [ - { - "id": 2, - "instance_path": [] - }, - { - "id": 3, - "instance_path": [] - }, - { - "id": 6, - "instance_path": [] - }, - { - "id": 7, - "instance_path": [] - } - ] - }, - { - "suite_id": 2, - "tests": [ - { - "id": 2, - "instance_path": [] - }, - { - "id": 3, - "instance_path": [] - }, - { - "id": 4, - "instance_path": [] - } - ] - }, - { - "suite_id": 5, - "tests": [ - { - "id": 4, - "instance_path": [] - } - ] - } - ] -} diff --git a/jsonschema/tests/suite b/jsonschema/tests/suite deleted file mode 160000 index c2994271..00000000 --- a/jsonschema/tests/suite +++ /dev/null @@ -1 +0,0 @@ -Subproject commit c2994271f7ee764d91baa9e78f0ce95d98c49137 diff --git a/jsonschema/tests/test_suite.rs b/jsonschema/tests/test_suite.rs deleted file mode 100644 index 004cf041..00000000 --- a/jsonschema/tests/test_suite.rs +++ /dev/null @@ -1,212 +0,0 @@ -use json_schema_test_suite::{json_schema_test_suite, TestCase}; -use jsonschema::{Draft, JSONSchema}; -use std::fs; - -#[json_schema_test_suite("tests/suite", "draft4", {"optional_bignum_0_0", "optional_bignum_2_0"})] -#[json_schema_test_suite("tests/suite", "draft6")] -#[json_schema_test_suite("tests/suite", "draft7", { - r"optional_format_idn_hostname_0_\d+", // https://github.com/Stranger6667/jsonschema-rs/issues/101 -})] -#[cfg_attr(feature = "draft201909", json_schema_test_suite("tests/suite", "draft2019-09", { - r"optional_format_idn_hostname_0_\d+", // https://github.com/Stranger6667/jsonschema-rs/issues/101 - r"format_\d+_6", // https://github.com/Stranger6667/jsonschema-rs/issues/261 - // These depend on the new `$defs` keyword (which is renamed from `definitions`) - r"id_0_[0-6]", - // Various types of new behavior used in the `$ref` context - "refRemote_4_0", - "refRemote_4_1", - "recursiveRef_0_3", - "recursiveRef_1_2", - "recursiveRef_1_4", - "recursiveRef_3_2", - "recursiveRef_3_4", - "recursiveRef_4_2", - "recursiveRef_4_4", - "recursiveRef_5_2", - "recursiveRef_6_2", - "recursiveRef_7_0", - "recursiveRef_7_1", - // New keywords & formats. - // https://github.com/Stranger6667/jsonschema-rs/issues/100 - r"anchor_.+", - r"defs_.+", - r"optional_format_duration_.+", // https://github.com/Stranger6667/jsonschema-rs/issues/265 - r"optional_format_uuid_.+", // https://github.com/Stranger6667/jsonschema-rs/issues/266 - r"unevaluatedItems_.+", -}))] -#[cfg_attr(feature = "draft202012", json_schema_test_suite("tests/suite", "draft2020-12", { - r"optional_format_idn_hostname_0_\d+", // https://github.com/Stranger6667/jsonschema-rs/issues/101 - r"format_\d+_6", // https://github.com/Stranger6667/jsonschema-rs/issues/261 - // These depend on the new `$defs` keyword (which is renamed from `definitions`) - r"id_0_[0-6]", - // Various types of new behavior used in the `$ref` context - "refRemote_4_0", - "refRemote_4_1", - "recursiveRef_0_3", - "recursiveRef_1_2", - "recursiveRef_1_4", - "recursiveRef_3_2", - "recursiveRef_3_4", - "recursiveRef_4_2", - "recursiveRef_4_4", - "recursiveRef_5_2", - "recursiveRef_6_2", - "recursiveRef_7_0", - "recursiveRef_7_1", - // New keywords & formats. - // https://github.com/Stranger6667/jsonschema-rs/issues/100 - r"anchor_.+", - r"defs_.+", - r"dynamicRef_.+", - r"uniqueItems_.+", - r"optional_format_duration_.+", // https://github.com/Stranger6667/jsonschema-rs/issues/265 - r"optional_format_uuid_.+", // https://github.com/Stranger6667/jsonschema-rs/issues/266 - r"optional_format_iri_.+", - r"optional_format_json_pointer_.+", - r"optional_format_relative_json_pointer_.+", - r"optional_format_uri_reference_.+", - r"optional_format_uri_template_.+", - r"unevaluatedItems_.+", -}))] -fn test_draft(_server_address: &str, test_case: TestCase) { - let draft_version = match test_case.draft_version.as_ref() { - "draft4" => Draft::Draft4, - "draft6" => Draft::Draft6, - "draft7" => Draft::Draft7, - #[cfg(feature = "draft201909")] - "draft2019-09" => Draft::Draft201909, - #[cfg(feature = "draft202012")] - "draft2020-12" => Draft::Draft202012, - _ => panic!("Unsupported draft"), - }; - - let compiled = JSONSchema::options() - .with_draft(draft_version) - .should_validate_formats(true) - .compile(&test_case.schema) - .expect("should not fail to compile schema"); - - let result = compiled.validate(&test_case.instance); - - if test_case.is_valid { - if let Err(mut errors_iterator) = result { - let first_error = errors_iterator.next(); - assert!( - first_error.is_none(), - "Test case should not have validation errors:\nGroup: {}\nTest case: {}\nSchema: {}\nInstance: {}\nError: {:?}", - test_case.group_description, - test_case.test_case_description, - test_case.schema, - test_case.instance, - first_error, - ); - } - assert!( - compiled.is_valid(&test_case.instance), - "Test case should be valid:\nGroup: {}\nTest case: {}\nSchema: {}\nInstance: {}", - test_case.group_description, - test_case.test_case_description, - test_case.schema, - test_case.instance, - ); - let output = compiled.apply(&test_case.instance).basic(); - assert!( - output.is_valid(), - "Test case should be valid via basic output:\nGroup: {}\nTest case: {}\nSchema: {}\nInstance: {}\nError: {:?}", - test_case.group_description, - test_case.test_case_description, - test_case.schema, - test_case.instance, - output - ); - } else { - assert!( - result.is_err(), - "Test case should have validation errors:\nGroup: {}\nTest case: {}\nSchema: {}\nInstance: {}", - test_case.group_description, - test_case.test_case_description, - test_case.schema, - test_case.instance, - ); - let errors = result.unwrap_err(); - for error in errors { - let pointer = error.instance_path.to_string(); - assert_eq!( - test_case.instance.pointer(&pointer), Some(&*error.instance), - "Expected error instance did not match actual error instance:\nGroup: {}\nTest case: {}\nSchema: {}\nInstance: {}\nExpected pointer: {:#?}\nActual pointer: {:#?}", - test_case.group_description, - test_case.test_case_description, - test_case.schema, - test_case.instance, - &*error.instance, - &pointer, - ); - } - assert!( - !compiled.is_valid(&test_case.instance), - "Test case should be invalid:\nGroup: {}\nTest case: {}\nSchema: {}\nInstance: {}", - test_case.group_description, - test_case.test_case_description, - test_case.schema, - test_case.instance, - ); - let output = compiled.apply(&test_case.instance).basic(); - assert!( - !output.is_valid(), - "Test case should be invalid via basic output:\nGroup: {}\nTest case: {}\nSchema: {}\nInstance: {}", - test_case.group_description, - test_case.test_case_description, - test_case.schema, - test_case.instance, - ); - } -} - -#[test] -fn test_instance_path() { - let expectations: serde_json::Value = - serde_json::from_str(include_str!("draft7_instance_paths.json")).expect("Valid JSON"); - for (filename, expected) in expectations.as_object().expect("Is object") { - let test_file = fs::read_to_string(format!("tests/suite/tests/draft7/{}", filename)) - .unwrap_or_else(|_| panic!("Valid file: {}", filename)); - let data: serde_json::Value = serde_json::from_str(&test_file).expect("Valid JSON"); - for item in expected.as_array().expect("Is array") { - let suite_id = item["suite_id"].as_u64().expect("Is integer") as usize; - let raw_schema = &data[suite_id]["schema"]; - let schema = JSONSchema::options() - .compile(raw_schema) - .unwrap_or_else(|_| { - panic!( - "Valid schema. File: {}; Suite ID: {}; Schema: {}", - filename, suite_id, raw_schema - ) - }); - for test_data in item["tests"].as_array().expect("Valid array") { - let test_id = test_data["id"].as_u64().expect("Is integer") as usize; - let instance_path: Vec<&str> = test_data["instance_path"] - .as_array() - .expect("Valid array") - .iter() - .map(|value| value.as_str().expect("A string")) - .collect(); - let instance = &data[suite_id]["tests"][test_id]["data"]; - let error = schema - .validate(instance) - .expect_err(&format!( - "File: {}; Suite ID: {}; Test ID: {}", - filename, suite_id, test_id - )) - .next() - .expect("Validation error"); - assert_eq!( - error.instance_path.into_vec(), - instance_path, - "File: {}; Suite ID: {}; Test ID: {}", - filename, - suite_id, - test_id - ) - } - } - } -} diff --git a/perf-helpers/.gitignore b/perf-helpers/.gitignore deleted file mode 100644 index 3605c3e0..00000000 --- a/perf-helpers/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -/Cargo.lock -/target -/trace.svg diff --git a/perf-helpers/Cargo.toml b/perf-helpers/Cargo.toml deleted file mode 100644 index 3ac8f779..00000000 --- a/perf-helpers/Cargo.toml +++ /dev/null @@ -1,14 +0,0 @@ -[package] -name = "perf_helpers" -version = "0.1.0" -authors = ["Samuele Maci "] -edition = "2021" -publish = false - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html -[dependencies] -jsonschema = { path = "../jsonschema" } -serde_json = "1" - -[profile.release] -debug = true diff --git a/perf-helpers/run b/perf-helpers/run deleted file mode 100755 index fc2f69f8..00000000 --- a/perf-helpers/run +++ /dev/null @@ -1,31 +0,0 @@ -#!/bin/sh -set -euo pipefail -CURDIR=$(pwd) -DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" - -if [ $# -lt 2 ]; then - echo "Usage: $0 []" > /dev/stderr - exit 1 -fi - -export SCHEMA_PATH="$(realpath $1)" -export INSTANCE_PATH="$(realpath $2)" -export NUMBER_OF_ITERATIONS="${3:-1234}" - -# Ensure that flamegraph is installed -cargo flamegraph -h > /dev/null || cargo install flamegraph - -# Clear files before starting -rm -rf trace.svg - -# Move into perf-helpers package -pushd ${DIR} - -# Ensure that a new build is re-issued as the content of the -# files and/or CLI arguments (env variables) might change -cargo clean --release --package perf_helpers - -cargo flamegraph --root --output ${CURDIR}/trace.svg - -# Exits perf-helpers -popd diff --git a/perf-helpers/rustfmt.toml b/perf-helpers/rustfmt.toml deleted file mode 100644 index bf772232..00000000 --- a/perf-helpers/rustfmt.toml +++ /dev/null @@ -1,2 +0,0 @@ -imports_granularity = "Crate" -edition = "2021" diff --git a/perf-helpers/src/main.rs b/perf-helpers/src/main.rs deleted file mode 100644 index c61b59ab..00000000 --- a/perf-helpers/src/main.rs +++ /dev/null @@ -1,24 +0,0 @@ -use jsonschema::JSONSchema; -use serde_json::{from_str, Error, Value}; - -/// This executable is supposed to be used via ../run sh scrpt -/// The script does trigger the build of the binary and is responsible -/// injects for injecting the needed environmental variables -fn main() -> Result<(), Error> { - let schema: Value = from_str(include_str!(env!("SCHEMA_PATH")))?; - let instance: Value = from_str(include_str!(env!("INSTANCE_PATH")))?; - let number_of_iterations: usize = env!("NUMBER_OF_ITERATIONS") - .parse() - .expect("NUMBER_OF_ITERATIONS is expected to be a positive integer"); - - eprintln!("Schema {}", schema); - eprintln!("Instance {}", instance); - eprintln!("Number of Iterations {}", number_of_iterations); - - let compiled = JSONSchema::compile(&schema).unwrap(); - for _ in 0..number_of_iterations { - compiled.is_valid(&instance); - } - - Ok(()) -} diff --git a/bench_helpers/rustfmt.toml b/rustfmt.toml similarity index 100% rename from bench_helpers/rustfmt.toml rename to rustfmt.toml