Compare commits

...

39 Commits

Author SHA1 Message Date
Dmitry Dygalo 8adae12108
docs: Update README.md 2024-05-15 17:32:39 +02:00
Dmitry Dygalo de4fb4a16c build: Add boon to benchmarks
Signed-off-by: Dmitry Dygalo <dmitry@dygalo.dev>
2024-05-15 17:19:30 +02:00
Dmitry Dygalo dbe6c90d78 build: Add CodSpeed
Signed-off-by: Dmitry Dygalo <dmitry@dygalo.dev>
2024-05-10 18:21:05 +02:00
Dmitry Dygalo bcb6d393b5
chore(python): Release 0.18.0
Signed-off-by: Dmitry Dygalo <dmitry@dygalo.dev>
2024-05-07 21:01:53 +02:00
Dmitry Dygalo ffdbcc98d5
chore(rust): Release 0.18.0
Signed-off-by: Dmitry Dygalo <dmitry@dygalo.dev>
2024-05-07 18:35:07 +02:00
Dmitry Dygalo 2cbc86fd6a fix: incorrect schema_path in errors from $ref
Signed-off-by: Dmitry Dygalo <dmitry@dygalo.dev>
2024-05-07 17:22:21 +02:00
Dmitry Dygalo cfab6027a1 perf: Avoid allocations while constructing path segments
Signed-off-by: Dmitry Dygalo <dmitry@dygalo.dev>
2024-05-03 20:46:44 +02:00
Dmitry Dygalo 21df1810bd chore(python): Update types
Signed-off-by: Dmitry Dygalo <dmitry@dygalo.dev>
2024-05-02 00:22:44 +02:00
Dmitry Dygalo e564888da5 feat(python): Custom format checkers
Signed-off-by: Dmitry Dygalo <dmitry@dygalo.dev>
2024-05-02 00:02:22 +02:00
Dmitry Dygalo 0a00839482 docs(python): Update README
Signed-off-by: Dmitry Dygalo <dmitry@dygalo.dev>
2024-05-01 19:13:06 +02:00
Dmitry Dygalo 146f6cde42
docs: Update README
Signed-off-by: Dmitry Dygalo <dmitry@dygalo.dev>
2024-05-01 18:34:41 +02:00
Sam Roberts aa94a4b24a
feat: Custom keyword validation (#473)
Signed-off-by: Dmitry Dygalo <dmitry@dygalo.dev>
Co-authored-by: Benjamin Tobler <ben@tobler.nz>
Co-authored-by: Benjamin Tobler <benjamin.tobler@stedi.com>
Co-authored-by: Dmitry Dygalo <dmitry@dygalo.dev>
2024-05-01 18:16:08 +02:00
Dmitry Dygalo 7946e978b5 chore(python): Update PyO3 to 0.21.0
Signed-off-by: Dmitry Dygalo <dmitry@dygalo.dev>
2024-05-01 18:08:54 +02:00
Dmitry Dygalo 092b573163 build: Disable build cache for Python tests
Signed-off-by: Dmitry Dygalo <dmitry@dygalo.dev>
2024-05-01 14:19:45 +02:00
Dmitry Dygalo cd0b70a49a chore: Expose JsonPointerNode
Signed-off-by: Dmitry Dygalo <dmitry@dygalo.dev>
2024-05-01 14:03:03 +02:00
Joel Natividad 6cf82328e3 chore: bump reqwest from 0.11 to 0.12 2024-04-14 21:01:58 +02:00
Dmitry Dygalo 9aae87e573 build: Update builds
Signed-off-by: Dmitry Dygalo <dmitry@dygalo.dev>
2024-04-14 18:24:03 +02:00
Dmitry Dygalo 79e35a2012
chore(python): Release 0.17.3
Signed-off-by: Dmitry Dygalo <dmitry@dygalo.dev>
2024-03-22 19:02:25 +01:00
Dmitry Dygalo b31e4ffb18 chore: fix import order
Signed-off-by: Dmitry Dygalo <dmitry@dygalo.dev>
2024-03-22 19:00:09 +01:00
Florian Braun 829ca19963 feat(python): support validation of dict subclasses
Currently, attemping to validate an instance of a dict subclass will raise a `ValueError`. This should not be happening, since the instance is still dict.
Achieve compatibility by checking the subclasses' inheritance tree, and treat the instance like a dict if that check passes.
2024-03-22 18:44:42 +01:00
Dmitry Dygalo 9771bc227c
chore: Clarify error on missing test suite
Signed-off-by: Dmitry Dygalo <dmitry@dygalo.dev>
2024-03-03 21:26:10 +01:00
Dmitry Dygalo 8eacf2d9da
chore(python): Release 0.17.2
Signed-off-by: Dmitry Dygalo <dmitry@dygalo.dev>
2024-03-03 20:32:38 +01:00
Dmitry Dygalo 8193c2cb91
chore(python): Update classifiers
Signed-off-by: Dmitry Dygalo <dmitry@dygalo.dev>
2024-03-03 19:50:49 +01:00
Dmitry Dygalo 4f99c8f8be feat: Support Python 3.12
Signed-off-by: Dmitry Dygalo <dmitry@dygalo.dev>
2024-03-03 19:48:40 +01:00
Dmitry Dygalo f0828cba01 fix: Missing features from the main crate
Signed-off-by: Dmitry Dygalo <dmitry@dygalo.dev>
2024-03-03 01:43:16 +01:00
Stephan Lanfermann 6ae63dc564
feat: Expose drafts 2019-09 and 2020-12 to Python (#457)
Co-authored-by: Dmitry Dygalo <dmitry@dygalo.dev>
2024-03-03 01:24:07 +01:00
Dmitry Dygalo e15d4dd342 chore: Update dependencies
Signed-off-by: Dmitry Dygalo <dmitry@dygalo.dev>
2024-03-03 01:18:27 +01:00
Karl Gutwin 584a07a410 ci: build aarch64 Python wheels 2024-03-03 01:15:12 +01:00
Dmitry Dygalo e2e06895e4 chore: Update more dependencies
Signed-off-by: Dmitry Dygalo <dmitry@dygalo.dev>
2024-03-03 01:03:09 +01:00
dependabot[bot] cd1ed98e6d
build(deps): bump rustix from 0.38.2 to 0.38.31 in /bench_helpers (#459)
Bumps [rustix](https://github.com/bytecodealliance/rustix) from 0.38.2 to 0.38.31.
- [Release notes](https://github.com/bytecodealliance/rustix/releases)
- [Commits](https://github.com/bytecodealliance/rustix/compare/v0.38.2...v0.38.31)

---
updated-dependencies:
- dependency-name: rustix
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-03 00:25:22 +01:00
Joel Natividad 710f7700f0
chore: Bump dependencies 2024-03-03 00:21:12 +01:00
Dmitry Dygalo c64e87eebd
docs: Update README
Signed-off-by: Dmitry Dygalo <dmitry@dygalo.dev>
2024-03-03 00:20:09 +01:00
OrangeTux 847fb05251 Correct link 2024-03-03 00:13:02 +01:00
OrangeTux 88c4b7503f Document how to run tests
`cargo tests` depends on an external, undocumented dependency: the JSON
Schema Test Suite.

This commit introduces instructions how to get test suite.

Without the test suite, `cargo test` fails with:

```
$ cargo test
error: custom attribute panicked
 --> tests/test_suite.rs:5:1
  |
5 | #[json_schema_test_suite("tests/suite", "draft4", {"optional_bignum_0_0", "optional_bignum_2_0"})]
  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  |
  = help: message: Tests directory not found: tests/suite/tests/draft4
```
2024-03-03 00:13:02 +01:00
Dmitry Dygalo 558d13db35 test: fix python tests
Signed-off-by: Dmitry Dygalo <dmitry@dygalo.dev>
2024-03-03 00:11:33 +01:00
Dmitry Dygalo c7ca4119ba chore: fix clippy
Signed-off-by: Dmitry Dygalo <dmitry@dygalo.dev>
2024-03-02 23:54:00 +01:00
OrangeTux 52cf5683d6 fix: unresolved import `"syn::ItemFn"` when running tests
`cargo test` fails with:

```
$ cargo test                                                                                                                                     master ✭
   Compiling json_schema_test_suite_proc_macro v0.3.0 (/home/auke/projects/jsonschema-rs/jsonschema-test-suite/proc_macro)
   Compiling jsonschema v0.17.1 (/home/auke/projects/jsonschema-rs/jsonschema)
error[E0432]: unresolved import `syn::ItemFn`
   --> /home/auke/projects/jsonschema-rs/jsonschema-test-suite/proc_macro/src/lib.rs:41:37
    |
41  | use syn::{parse_macro_input, Ident, ItemFn};
    |                                     ^^^^^^ no `ItemFn` in the root
    |
note: found an item that was configured out
   --> /home/auke/.cargo/registry/src/index.crates.io-6f17d22bba15001f/syn-1.0.109/src/lib.rs:365:32
    |
365 |     ItemEnum, ItemExternCrate, ItemFn, ItemForeignMod, ItemImpl, ItemMacro, ItemMacro2, ItemMod,
    |                                ^^^^^^
    = note: the item is gated behind the `full` feature

For more information about this error, try `rustc --explain E0432`.
```

This commit fixes the issue by applying the suggestion of the compiler.
2024-03-02 23:02:09 +01:00
John Vandenberg 8dc33ea41a
chore: Fix typos (#447) 2024-01-16 08:00:33 +01:00
Dmitry Dygalo 4461d0ff7f
chore(python): Release 0.17.1
Signed-off-by: Dmitry Dygalo <dmitry@dygalo.dev>
2023-07-05 11:58:49 +02:00
92 changed files with 1964 additions and 2731 deletions

View File

@ -10,26 +10,24 @@ jobs:
commitsar:
name: Verify commit messages
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
steps:
- name: Check out code
uses: actions/checkout@v3.0.0
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Run commitsar
uses: aevea/commitsar@v0.18.0
- uses: aevea/commitsar@v0.20.2
pre-commit:
name: Generic pre-commit checks
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
fetch-depth: 1
- uses: actions/setup-python@v4
- uses: actions/setup-python@v5
with:
python-version: 3.9
python-version: 3.11
- run: pip install pre-commit
- run: pre-commit run --all-files
@ -39,30 +37,23 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [ubuntu-20.04, macos-12, windows-2022]
os: [ubuntu-22.04, macos-12, windows-2022]
draft: [draft201909, draft202012]
name: Test ${{ matrix.draft }} (stable) on ${{ matrix.os}}
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
submodules: true
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
- uses: dtolnay/rust-toolchain@stable
- name: Cache cargo
uses: actions/cache@v3
- uses: Swatinem/rust-cache@v2
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: stable-${{ matrix.os }}-${{ matrix.draft }}-cargo-cache
workspaces: jsonschema
cache-all-crates: "true"
key: ${{ matrix.os }}-${{ matrix.draft }}
- run: cargo test --no-fail-fast --features ${{ matrix.draft }}
working-directory: ./jsonschema
@ -74,78 +65,72 @@ jobs:
target: ['wasm32-unknown-unknown']
name: Build on ${{ matrix.target }}
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
submodules: true
- uses: actions-rs/toolchain@v1
- uses: dtolnay/rust-toolchain@stable
with:
profile: minimal
toolchain: stable
target: ${{ matrix.target }}
override: true
targets: ${{ matrix.target }}
- name: Cache cargo
uses: actions/cache@v3
- uses: Swatinem/rust-cache@v2
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: stable-${{ matrix.target }}-cargo-cache
workspaces: jsonschema
cache-all-crates: "true"
- run: cargo build --target ${{ matrix.target }} --no-default-features --features=cli
working-directory: ./jsonschema
coverage:
name: Run test coverage
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
submodules: true
- name: Toolchain setup
uses: actions-rs/toolchain@v1
with:
toolchain: nightly
override: true
- name: Install grcov
run: cargo install cargo-tarpaulin
- name: Cache cargo
uses: actions/cache@v3
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: coverage-cargo-cache
workspaces: jsonschema
cache-all-crates: "true"
- name: Install cargo-llvm-cov
uses: taiki-e/install-action@cargo-llvm-cov
- name: Run tests
run: cargo +nightly tarpaulin --verbose --all-features --out Xml
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@v3
if: ${{ env.GITHUB_REPOSITORY }} == 'Stranger6667/jsonschema-rs'
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
name: coverage
files: lcov.info
fail_ci_if_error: true
test-python:
strategy:
fail-fast: false
matrix:
os: [ubuntu-20.04, macos-12, windows-2022]
python-version: ['3.7', '3.8', '3.9', '3.10', '3.11']
os: [ubuntu-22.04, macos-12, windows-2022]
python-version: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12']
name: Python ${{ matrix.python-version }} on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
submodules: true
- uses: actions/setup-python@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
architecture: x64
@ -153,19 +138,7 @@ jobs:
- run: python -m pip install tox
working-directory: ./bindings/python
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true
- name: Cache cargo
uses: actions/cache@v3
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: python-${{ matrix.python-version }}-${{ matrix.os }}-cargo-cache
- uses: dtolnay/rust-toolchain@stable
- name: Run ${{ matrix.python }} tox job
run: tox -e py
@ -173,46 +146,69 @@ jobs:
fmt:
name: Rustfmt
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
- uses: actions-rs/toolchain@v1
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@nightly
with:
profile: minimal
toolchain: stable
override: true
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
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
submodules: true
- uses: actions-rs/toolchain@v1
- uses: dtolnay/rust-toolchain@stable
with:
profile: minimal
toolchain: stable
override: true
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
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
submodules: true
- uses: actions-rs/toolchain@v1
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
profile: minimal
toolchain: stable
override: true
workspaces: jsonschema
cache-all-crates: "true"
- uses: taiki-e/install-action@cargo-hack
- run: cargo hack check --feature-powerset --lib
working-directory: ./jsonschema

31
.github/workflows/codspeed.yml vendored Normal file
View File

@ -0,0 +1,31 @@
name: Benchmarks
on:
push:
branches:
- "master"
pull_request:
workflow_dispatch:
jobs:
rust:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- 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
- uses: CodSpeedHQ/action@v2
with:
run: cargo codspeed run jsonschema
token: ${{ secrets.CODSPEED_TOKEN }}
working-directory: ./jsonschema

View File

@ -49,7 +49,7 @@ jobs:
runs-on: macos-12
strategy:
matrix:
python-version: ['3.7', '3.8', '3.9', '3.10', '3.11']
python-version: [ '3.7', '3.8', '3.9', '3.10', '3.11', '3.12' ]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
@ -80,7 +80,7 @@ jobs:
runs-on: macos-12
strategy:
matrix:
python-version: [ '3.7', '3.8', '3.9', '3.10', '3.11' ]
python-version: [ '3.7', '3.8', '3.9', '3.10', '3.11', '3.12' ]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
@ -96,7 +96,7 @@ jobs:
- name: Build wheels - universal2
uses: messense/maturin-action@v1
with:
args: --release -m bindings/python/Cargo.toml --universal2 --out dist --interpreter ${{ matrix.python-version }}
args: --release -m bindings/python/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 +110,7 @@ jobs:
runs-on: windows-2022
strategy:
matrix:
python-version: [ '3.7', '3.8', '3.9', '3.10', '3.11' ]
python-version: [ '3.7', '3.8', '3.9', '3.10', '3.11', '3.12' ]
target: [ x64, x86 ]
steps:
- uses: actions/checkout@v3
@ -143,24 +143,42 @@ jobs:
runs-on: ubuntu-20.04
strategy:
matrix:
python-version: [ '3.7', '3.8', '3.9', '3.10', '3.11' ]
target: [ x86_64, i686 ]
python-version: [ '3.7', '3.8', '3.9', '3.10', '3.11', '3.12' ]
target: [ x86_64, i686, aarch64 ]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
architecture: x64
- name: Build wheels
uses: messense/maturin-action@v1
with:
target: ${{ matrix.target }}
manylinux: auto
args: --release -m bindings/python/Cargo.toml --out dist --interpreter ${{ matrix.python-version }}
- name: Install built wheel
- name: Install built wheel on native architecture
if: matrix.target == 'x86_64'
run: |
pip install dist/${{ env.PACKAGE_NAME }}-*.whl --force-reinstall
- uses: uraimo/run-on-arch-action@v2
if: matrix.target == 'aarch64'
name: Install built wheel on ARM architecture
with:
arch: ${{ matrix.target }}
distro: ubuntu22.04
githubToken: ${{ github.token }}
install: |
export TZ=UTC
export DEBIAN_FRONTEND=noninteractive
apt-get update
apt-get install -y --no-install-recommends software-properties-common gpg gpg-agent curl
add-apt-repository ppa:deadsnakes/ppa
apt-get update
apt-get install -y python${{ matrix.python-version }}-dev python${{ matrix.python-version }}-venv
run: |
python${{ matrix.python-version }} -m venv venv
venv/bin/pip install -U pip wheel
venv/bin/pip install ${{ env.PACKAGE_NAME }} --no-index --find-links dist/ --force-reinstall
- name: Upload wheels
uses: actions/upload-artifact@v3
with:

3
.gitignore vendored
View File

@ -3,7 +3,6 @@
/bindings/*/target
/bench_helpers/target
/jsonschema-test-suite/target
/jsonschema-csr/target
.hypothesis
.benchmarks
/jsonschema/Cargo.lock
@ -12,3 +11,5 @@
# IDEs
/.idea
/.vscode
Cargo.lock

View File

@ -1,9 +1,9 @@
default_language_version:
python: python3.9
python: python3.11
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.1.0
rev: v4.6.0
hooks:
- id: check-yaml
- id: end-of-file-fixer
@ -15,23 +15,21 @@ repos:
- id: check-merge-conflict
- repo: https://github.com/jorisroovers/gitlint
rev: v0.17.0
rev: v0.19.1
hooks:
- id: gitlint
- repo: https://github.com/adrienverge/yamllint
rev: v1.26.3
rev: v1.35.1
hooks:
- id: yamllint
- repo: https://github.com/ambv/black
rev: 22.3.0
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.3.7
hooks:
- id: black
types: [python]
- id: ruff-format
- repo: https://github.com/pre-commit/mirrors-isort
rev: v5.10.1
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.3.7
hooks:
- id: isort
additional_dependencies: ["isort[pyproject]"]
- id: ruff

View File

@ -2,6 +2,38 @@
## [Unreleased]
## [0.18.0] - 2024-05-07
### Added
- Custom keywords support. [#379](https://github.com/Stranger6667/jsonschema-rs/issues/379)
- Expose `JsonPointerNode` that can be converted into `JSONPointer`.
This is needed for the upcoming custom validators support.
### Changed
- Bump `base64` to `0.22`.
- Bump `clap` to `4.5`.
- Bump `fancy-regex` to `0.13`.
- Bump `fraction` to `0.15`.
- Bump `memchr` to `2.7`.
- Bump `once_cell` to `1.19`.
- Bump `percent-encoding` to `2.3`.
- Bump `regex` to `1.10`.
- Bump `url` to `2.5`.
- Build CLI only if the `cli` feature is enabled.
- **BREAKING**: Extend `CompilationOptions` to support more ways to define custom format checkers (for example in Python bindings).
In turn it changes `ValidationErrorKind::Format` to contain a `String` instead of a `&'static str`.
### Fixed
- Incorrect `schema_path` when multiple errors coming from the `$ref` keyword [#426](https://github.com/Stranger6667/jsonschema-rs/issues/426)
### Performance
- Optimize building `JSONPointer` for validation errors by allocating the exact amount of memory needed.
- Avoid cloning path segments during validation.
## [0.17.1] - 2023-07-05
### Changed
@ -419,7 +451,8 @@
- Initial public release
[Unreleased]: https://github.com/Stranger6667/jsonschema-rs/compare/rust-v0.17.1...HEAD
[Unreleased]: https://github.com/Stranger6667/jsonschema-rs/compare/rust-v0.18.0...HEAD
[0.18.0]: https://github.com/Stranger6667/jsonschema-rs/compare/rust-v0.17.1...rust-v0.18.0
[0.17.1]: https://github.com/Stranger6667/jsonschema-rs/compare/rust-v0.17.0...rust-v0.17.1
[0.17.0]: https://github.com/Stranger6667/jsonschema-rs/compare/rust-v0.16.1...rust-v0.17.0
[0.16.1]: https://github.com/Stranger6667/jsonschema-rs/compare/rust-v0.16.0...rust-v0.16.1

View File

@ -55,7 +55,7 @@ further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at dadygalo@gmail.com. All
reported by contacting the project team at dmitry@dygalo.dev. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.

View File

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2020-2022 Dmitry Dygalo
Copyright (c) 2020-2024 Dmitry Dygalo
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

106
README.md
View File

@ -20,7 +20,7 @@ Partially supported drafts (some keywords are not implemented):
```toml
# Cargo.toml
jsonschema = "0.17"
jsonschema = "0.18"
```
To validate documents against some schema and get validation errors (if any):
@ -118,6 +118,73 @@ fn main() {
}
```
## 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())
}
}
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<String, Value>,
// Your keyword value
value: &'a Value,
// JSON Pointer to your keyword within the schema
path: JSONPointer,
) -> Result<Box<dyn Keyword>, ValidationError<'a>> {
// You may return validation error if the keyword is misused for some reason
Ok(Box::new(MyCustomValidator))
}
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));
}
```
## Reference resolving and TLS
By default, `jsonschema` resolves HTTP references via `reqwest` without TLS support.
@ -139,9 +206,26 @@ This library is functional and ready for use, but its API is still evolving to t
- Ruby - a [crate](https://github.com/driv3r/rusty_json_schema) by @driv3r
- NodeJS - a [package](https://github.com/ahungrynoob/jsonschema) by @ahungrynoob
## Running tests
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:
```bash
$ git submodule init
$ git submodule update
```
These commands clone the suite to [jsonschema/tests/suite/](jsonschema/tests/suite/).
Now, enter the `jsonschema` directory and run `cargo test`.
```bash
$ cd jsonschema
$ cargo test
```
## Performance
There is a comparison with other JSON Schema validators written in Rust - `jsonschema_valid==0.4.0` and `valico==3.6.0`.
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`.
Test machine i8700K (12 cores), 32GB RAM.
@ -164,14 +248,14 @@ Input values and schemas:
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:
| Case | jsonschema_valid | valico | jsonschema (validate) | jsonschema (is_valid) |
| -------------- | ----------------------- | ----------------------- | --------------------- | ---------------------- |
| OpenAPI | - (1) | - (1) | 4.717 ms | 4.279 ms (**x0.90**) |
| Swagger | - (2) | 83.357 ms (**x12.47**) | 6.681 ms | 4.533 ms (**x0.67**) |
| Canada | 32.987 ms (**x31.38**) | 141.41 ms (**x134.54**) | 1.051 ms | 1.046 ms (**x0.99**) |
| CITM catalog | 4.735 ms (**x2.00**) | 13.222 ms (**x5.58**) | 2.367 ms | 535.07 us (**x0.22**) |
| Fast (valid) | 2.00 us (**x3.85**) | 3.18 us (**x6.13**) | 518.39 ns | 97.91 ns (**x0.18**) |
| Fast (invalid) | 339.28 ns (**x0.50**) | 3.34 us (**x5.00**) | 667.55 ns | 5.41ns (**x0.01**) |
| 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**) |
Notes:
@ -179,7 +263,7 @@ Notes:
2. `jsonschema_valid` fails to resolve local references (e.g. `#/definitions/definitions`).
You can find benchmark code in `benches/jsonschema.rs`, Rust version is `1.57`.
You can find benchmark code in `benches/jsonschema.rs`, Rust version is `1.78`.
## Support

458
bench_helpers/Cargo.lock generated
View File

@ -1,458 +0,0 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "anes"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
[[package]]
name = "anstyle"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a30da5c5f2d5e72842e00bcb57657162cdabef0931f40e2deb9b4140440cecd"
[[package]]
name = "autocfg"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "bench_helpers"
version = "0.1.0"
dependencies = [
"criterion",
"serde",
"serde_json",
]
[[package]]
name = "bitflags"
version = "2.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42"
[[package]]
name = "cast"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
[[package]]
name = "cc"
version = "1.0.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f"
[[package]]
name = "ciborium"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "effd91f6c78e5a4ace8a5d3c0b6bfaec9e2baaef55f3efc00e45fb2e477ee926"
dependencies = [
"ciborium-io",
"ciborium-ll",
"serde",
]
[[package]]
name = "ciborium-io"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdf919175532b369853f5d5e20b26b43112613fd6fe7aee757e35f7a44642656"
[[package]]
name = "ciborium-ll"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "defaa24ecc093c77630e6c15e17c51f5e187bf35ee514f4e2d67baaa96dae22b"
dependencies = [
"ciborium-io",
"half",
]
[[package]]
name = "clap"
version = "4.3.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384e169cc618c613d5e3ca6404dda77a8685a63e08660dcc64abaf7da7cb0c7a"
dependencies = [
"clap_builder",
]
[[package]]
name = "clap_builder"
version = "4.3.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef137bbe35aab78bdb468ccfba75a5f4d8321ae011d34063770780545176af2d"
dependencies = [
"anstyle",
"clap_lex",
]
[[package]]
name = "clap_lex"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b"
[[package]]
name = "criterion"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f"
dependencies = [
"anes",
"cast",
"ciborium",
"clap",
"criterion-plot",
"is-terminal",
"itertools",
"num-traits",
"once_cell",
"oorandom",
"regex",
"serde",
"serde_derive",
"serde_json",
"tinytemplate",
"walkdir",
]
[[package]]
name = "criterion-plot"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1"
dependencies = [
"cast",
"itertools",
]
[[package]]
name = "either"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91"
[[package]]
name = "errno"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a"
dependencies = [
"errno-dragonfly",
"libc",
"windows-sys",
]
[[package]]
name = "errno-dragonfly"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf"
dependencies = [
"cc",
"libc",
]
[[package]]
name = "half"
version = "1.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7"
[[package]]
name = "hermit-abi"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b"
[[package]]
name = "is-terminal"
version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24fddda5af7e54bf7da53067d6e802dbcc381d0a8eef629df528e3ebf68755cb"
dependencies = [
"hermit-abi",
"rustix",
"windows-sys",
]
[[package]]
name = "itertools"
version = "0.10.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62b02a5381cc465bd3041d84623d0fa3b66738b52b8e2fc3bab8ad63ab032f4a"
[[package]]
name = "libc"
version = "0.2.147"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3"
[[package]]
name = "linux-raw-sys"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09fc20d2ca12cb9f044c93e3bd6d32d523e6e2ec3db4f7b2939cd99026ecd3f0"
[[package]]
name = "num-traits"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
dependencies = [
"autocfg",
]
[[package]]
name = "once_cell"
version = "1.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
[[package]]
name = "oorandom"
version = "11.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575"
[[package]]
name = "proc-macro2"
version = "1.0.63"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b368fba921b0dce7e60f5e04ec15e565b3303972b42bcfde1d0713b881959eb"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "573015e8ab27661678357f27dc26460738fd2b6c86e46f386fde94cb5d913105"
dependencies = [
"proc-macro2",
]
[[package]]
name = "regex"
version = "1.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0ab3ca65655bb1e41f2a8c8cd662eb4fb035e67c3f78da1d61dffe89d07300f"
dependencies = [
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78"
[[package]]
name = "rustix"
version = "0.38.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aabcb0461ebd01d6b79945797c27f8529082226cb630a9865a71870ff63532a4"
dependencies = [
"bitflags",
"errno",
"libc",
"linux-raw-sys",
"windows-sys",
]
[[package]]
name = "ryu"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe232bdf6be8c8de797b22184ee71118d63780ea42ac85b61d1baa6d3b782ae9"
[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]]
name = "serde"
version = "1.0.166"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d01b7404f9d441d3ad40e6a636a7782c377d2abdbe4fa2440e2edcc2f4f10db8"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.166"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5dd83d6dde2b6b2d466e14d9d1acce8816dedee94f735eac6395808b3483c6d6"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f1e14e89be7aa4c4b78bdbdc9eb5bf8517829a600ae8eaa39a6e1d960b5185c"
dependencies = [
"itoa",
"ryu",
"serde",
]
[[package]]
name = "syn"
version = "2.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59fb7d6d8281a51045d62b8eb3a7d1ce347b76f312af50cd3dc0af39c87c1737"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "tinytemplate"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc"
dependencies = [
"serde",
"serde_json",
]
[[package]]
name = "unicode-ident"
version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22049a19f4a68748a168c0fc439f9516686aa045927ff767eca0a85101fb6e73"
[[package]]
name = "walkdir"
version = "2.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698"
dependencies = [
"same-file",
"winapi-util",
]
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
dependencies = [
"winapi",
]
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-sys"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.48.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3"
[[package]]
name = "windows_i686_gnu"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241"
[[package]]
name = "windows_i686_msvc"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"

View File

@ -1,7 +1,7 @@
[package]
name = "bench_helpers"
version = "0.1.0"
authors = ["dmitry.dygalo <dadygalo@gmail.com>"]
authors = ["Dmitry Dygalo <dmitry@dygalo.dev>"]
edition = "2021"
license = "MIT"
@ -9,3 +9,4 @@ license = "MIT"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
criterion = { version = "0.5.1", features = [], default-features = false }
codspeed-criterion-compat = "2.6.0"

View File

@ -0,0 +1,2 @@
imports_granularity = "Crate"
edition = "2021"

View File

@ -1,4 +1,4 @@
use criterion::Criterion;
use codspeed_criterion_compat::Criterion;
use serde::Deserialize;
use serde_json::{from_reader, Value};
use std::{

View File

@ -2,6 +2,38 @@
## [Unreleased]
## [0.18.0] - 2024-05-07
### Added
- Defining custom format checkers. [#245](https://github.com/Stranger6667/jsonschema-rs/issues/245)
### Changed
- Update `pyo3` to `0.21`.
### Fixed
- Incorrect `schema_path` when multiple errors coming from the `$ref` keyword [#426](https://github.com/Stranger6667/jsonschema-rs/issues/426)
## [0.17.3] - 2024-03-22
### Added
- Support subclasses of Python `dict`s [#427](https://github.com/Stranger6667/jsonschema-rs/issues/427)
## [0.17.2] - 2024-03-03
### Added
- Support for Python 3.12 [#439](https://github.com/Stranger6667/jsonschema-rs/issues/439)
### Changed
- Expose drafts 2019-09 and 2020-12 to Python
- Update `pyo3` to `0.20`.
## [0.17.1] - 2023-07-05
### Changed
- Update `pyo3` to `0.19`.
@ -301,7 +333,11 @@
## 0.1.0 - 2020-06-09
- Initial public release
[Unreleased]: https://github.com/Stranger6667/jsonschema-rs/compare/python-v0.16.3...HEAD
[Unreleased]: https://github.com/Stranger6667/jsonschema-rs/compare/python-v0.18.0...HEAD
[0.18.0]: https://github.com/Stranger6667/jsonschema-rs/compare/python-v0.17.3...python-v0.18.0
[0.17.3]: https://github.com/Stranger6667/jsonschema-rs/compare/python-v0.17.2...python-v0.17.3
[0.17.2]: https://github.com/Stranger6667/jsonschema-rs/compare/python-v0.17.1...python-v0.17.2
[0.17.1]: https://github.com/Stranger6667/jsonschema-rs/compare/python-v0.16.3...python-v0.17.1
[0.16.3]: https://github.com/Stranger6667/jsonschema-rs/compare/python-v0.16.2...python-v0.16.3
[0.16.2]: https://github.com/Stranger6667/jsonschema-rs/compare/python-v0.16.1...python-v0.16.2
[0.16.1]: https://github.com/Stranger6667/jsonschema-rs/compare/python-v0.16.0...python-v0.16.1

File diff suppressed because it is too large Load Diff

View File

@ -1,10 +1,10 @@
[package]
name = "jsonschema-python"
version = "0.16.3"
authors = ["Dmitry Dygalo <dadygalo@gmail.com>"]
version = "0.18.0"
authors = ["Dmitry Dygalo <dmitry@dygalo.dev>"]
edition = "2021"
license = "MIT"
readme = "README.rst"
readme = "README.md"
description = "JSON schema validaton library"
repository = "https://github.com/Stranger6667/jsonschema-rs"
keywords = ["jsonschema", "validation"]
@ -16,19 +16,20 @@ name = "jsonschema_rs"
crate-type = ["cdylib"]
[build-dependencies]
built = { version = "0.6.1", features = ["chrono"] }
built = { version = "0.7.1", features = ["cargo-lock", "chrono"] }
pyo3-build-config = { version = "0.21.2", features = ["resolve-config"] }
[dependencies.jsonschema]
path = "../../jsonschema"
version = "*"
default-features = false
features = ["resolve-http", "resolve-file"]
features = ["resolve-http", "resolve-file", "draft201909", "draft202012"]
[dependencies]
serde_json = "1.0.91"
serde = "1.0.152"
pyo3 = { version = "0.19.1", features = ["extension-module"] }
pyo3-built = "0.4.7"
pyo3 = { version = "0.21.2", features = ["extension-module"] }
pyo3-built = "0.5"
[profile.release]
codegen-units = 1

156
bindings/python/README.md Normal file
View File

@ -0,0 +1,156 @@
# jsonschema-rs
[![Build](https://github.com/Stranger6667/jsonschema-rs/workflows/ci/badge.svg)](https://github.com/Stranger6667/jsonschema-rs/actions)
[![Version](https://img.shields.io/pypi/v/jsonschema-rs.svg)](https://pypi.org/project/jsonschema-rs/)
[![Python versions](https://img.shields.io/pypi/pyversions/jsonschema-rs.svg)](https://pypi.org/project/jsonschema-rs/)
[![License](https://img.shields.io/pypi/l/jsonschema-rs.svg)](https://opensource.org/licenses/MIT)
Fast JSON Schema validation for Python implemented in Rust.
Supported drafts:
- Draft 7
- Draft 6
- Draft 4
There are some notable restrictions at the moment:
- The underlying Rust crate doesn't support arbitrary precision integers yet, which may lead to `SystemError` when such value is used;
- Unicode surrogates are not supported;
## Installation
To install `jsonschema-rs` via `pip` run the following command:
```bash
pip install jsonschema-rs
```
## Usage
To check if the input document is valid:
```python
import jsonschema_rs
validator = jsonschema_rs.JSONSchema({"minimum": 42})
validator.is_valid(45) # True
```
or:
```python
import jsonschema_rs
validator = jsonschema_rs.JSONSchema({"minimum": 42})
validator.validate(41) # raises ValidationError
```
If you have a schema as a JSON string, then you could use
`jsonschema_rs.JSONSchema.from_str` to avoid parsing on the
Python side:
```python
import jsonschema_rs
validator = jsonschema_rs.JSONSchema.from_str('{"minimum": 42}')
...
```
You can define custom format checkers:
```python
import jsonschema_rs
def is_currency(value):
# The input value is always a string
return len(value) == 3 and value.isascii()
validator = jsonschema_rs.JSONSchema(
{"type": "string", "format": "currency"},
formats={"currency": is_currency}
)
validator.is_valid("USD") # True
validator.is_valid("invalid") # False
```
## Performance
According to our benchmarks, `jsonschema-rs` is usually faster than
existing alternatives in real-life scenarios.
However, for small schemas & inputs it might be slower than
`fastjsonschema` or `jsonschema` on PyPy.
### Input values and schemas
- [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`).
| 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 |
Compiled validators (when the input schema is compiled once and reused
later). `jsonschema-rs` comes in three variants in the tables below:
- `validate`. This method raises `ValidationError` on errors or returns `None` on their absence.
- `is_valid`. A faster method that returns a boolean result whether the instance is valid.
- `overhead`. Only transforms data to underlying Rust types and do not perform any validation. Shows the Python -> Rust data conversion cost.
Ratios are given against the `validate` variant.
Small schemas:
| library | `true` | `{"minimum": 10}` | `Fast (valid)` | `Fast (invalid)` |
|---------------------------|-----------------------|------------------------|------------------------|------------------------|
| jsonschema-rs\[validate\] | 93.84 ns | 94.83 ns | 1.2 us | 1.84 us |
| jsonschema-rs\[is_valid\] | 70.22 ns (**x0.74**) | 68.26 ns (**x0.71**) | 688.70 ns (**x0.57**) | 1.26 us (**x0.68**) |
| jsonschema-rs\[overhead\] | 65.27 ns (**x0.69**) | 66.90 ns (**x0.70**) | 461.53 ns (**x0.38**) | 925.16 ns (**x0.50**) |
| fastjsonschema\[CPython\] | 58.19 ns (**x0.62**) | 105.77 ns (**x1.11**) | 3.98 us (**x3.31**) | 4.57 us (**x2.48**) |
| fastjsonschema\[PyPy\] | 10.39 ns (**x0.11**) | 34.96 ns (**x0.36**) | 866 ns (**x0.72**) | 916 ns (**x0.49**) |
| jsonschema\[CPython\] | 235.06 ns (**x2.50**)| 1.86 us (**x19.6**) | 56.26 us (**x46.88**) | 59.39 us (**x32.27**) |
| jsonschema\[PyPy\] | 40.83 ns (**x0.43**) | 232.41 ns (**x2.45**) | 21.82 us (**x18.18**) | 22.23 us (**x12.08**) |
Large schemas:
| library | `Zuora (OpenAPI)` | `Kubernetes (Swagger)` | `Canada (GeoJSON)` | `CITM catalog` |
|---------------------------|------------------------|------------------------|------------------------|------------------------|
| jsonschema-rs\[validate\] | 17.311 ms | 15.194 ms | 5.018 ms | 4.765 ms |
| jsonschema-rs\[is_valid\] | 16.605 ms (**x0.95**) | 12.610 ms (**x0.82**) | 4.954 ms (**x0.98**) | 2.792 ms (**x0.58**) |
| jsonschema-rs\[overhead\] | 12.017 ms (**x0.69**) | 8.005 ms (**x0.52**) | 3.702 ms (**x0.73**) | 2.303 ms (**x0.48**) |
| fastjsonschema\[CPython\] | -- (1) | 90.305 ms (**x5.94**) | 32.389 ms (**6.45**) | 12.020 ms (**x2.52**) |
| fastjsonschema\[PyPy\] | -- (1) | 37.204 ms (**x2.44**) | 8.450 ms (**x1.68**) | 4.888 ms (**x1.02**) |
| jsonschema\[CPython\] | 764.172 ms (**x44.14**)| 1.063 s (**x69.96**) | 1.301 s (**x259.26**) | 115.362 ms (**x24.21**)|
| jsonschema\[PyPy\] | 604.557 ms (**x34.92**)| 619.744 ms (**x40.78**)| 524.275 ms (**x104.47**)| 25.275 ms (**x5.30**) |
Notes:
1. `fastjsonschema` fails to compile the Open API spec due to the presence of the `uri-reference` format (that is not defined in Draft 4). However, unknown formats are [explicitly supported](https://tools.ietf.org/html/draft-fge-json-schema-validation-00#section-7.1) by the spec.
The bigger the input is the bigger is performance win. You can take a look at benchmarks in `benches/bench.py`.
Package versions:
- `jsonschema-rs` - latest version from the repository
- `jsonschema` - `3.2.0`
- `fastjsonschema` - `2.15.1`
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.
## License
The code in this project is licensed under [MIT license](https://opensource.org/licenses/MIT). By contributing to `jsonschema-rs`, you agree that your contributions will be licensed under its MIT license.

View File

@ -1,173 +0,0 @@
jsonschema-rs
=============
|Build| |Version| |Python versions| |License|
Fast JSON Schema validation for Python implemented in Rust.
Supported drafts:
- Draft 7
- Draft 6
- Draft 4
There are some notable restrictions at the moment:
- The underlying crate doesn't support arbitrary precision integers yet, which may lead to ``SystemError`` when such value is used;
- Unicode surrogates are not supported;
Installation
------------
To install ``jsonschema-rs`` via ``pip`` run the following command:
.. code:: bash
pip install jsonschema-rs
Usage
-----
To check if the input document is valid:
.. code:: python
import jsonschema_rs
validator = jsonschema_rs.JSONSchema({"minimum": 42})
validator.is_valid(45) # True
or:
.. code:: python
import jsonschema_rs
validator = jsonschema_rs.JSONSchema({"minimum": 42})
validator.validate(41) # raises ValidationError
If you have a schema as a JSON string, then you could use `jsonschema_rs.JSONSchema.from_str` to avoid parsing on the Python side:
.. code:: python
import jsonschema_rs
validator = jsonschema_rs.JSONSchema.from_str('{"minimum": 42}')
...
Performance
-----------
According to our benchmarks, ``jsonschema-rs`` is usually faster than existing alternatives in real-life scenarios.
However, for small schemas & inputs it might be slower than ``fastjsonschema`` or ``jsonschema`` on PyPy.
Input values and schemas
~~~~~~~~~~~~~~~~~~~~~~~~
- `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``, `f`ast_valid.json`` and ``fast_invalid.json``).
+----------------+-------------+---------------+
| 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 |
+----------------+-------------+---------------+
Compiled validators (when the input schema is compiled once and reused later). ``jsonschema-rs`` comes in three variants in the tables below:
- ``validate``. This method raises ``ValidationError`` on errors or returns ``None`` on their absence.
- ``is_valid``. A faster method that returns a boolean result whether the instance is valid.
- ``overhead``. Only transforms data to underlying Rust types and do not perform any validation. Shows the Python -> Rust data conversion cost.
Ratios are given against the ``validate`` variant.
Small schemas:
+-------------------------+------------------------+-----------------------+----------------------------+----------------------------+
| library | ``true`` | ``{"minimum": 10}`` | ``Fast (valid)`` | ``Fast (invalid)`` |
+=========================+========================+=======================+============================+============================+
| jsonschema-rs[validate] | 93.84 ns | 94.83 ns | 1.2 us | 1.84 us |
+-------------------------+------------------------+-----------------------+----------------------------+----------------------------+
| jsonschema-rs[is_valid] | 70.22 ns (**x0.74**) | 68.26 ns (**x0.71**) | 688.70 ns (**x0.57**) | 1.26 us (**x0.68**) |
+-------------------------+------------------------+-----------------------+----------------------------+----------------------------+
| jsonschema-rs[overhead] | 65.27 ns (**x0.69**) | 66.90 ns (**x0.70**) | 461.53 ns (**x0.38**) | 925.16 ns (**x0.50**) |
+-------------------------+------------------------+-----------------------+----------------------------+----------------------------+
| fastjsonschema[CPython] | 58.19 ns (**x0.62**) | 105.77 ns (**x1.11**) | 3.98 us (**x3.31**) | 4.57 us (**x2.48**) |
+-------------------------+------------------------+-----------------------+----------------------------+----------------------------+
| fastjsonschema[PyPy] | 10.39 ns (**x0.11**) | 34.96 ns (**x0.36**) | 866 ns (**x0.72**) | 916 ns (**x0.49**) |
+-------------------------+------------------------+-----------------------+----------------------------+----------------------------+
| jsonschema[CPython] | 235.06 ns (**x2.50**) | 1.86 us (**x19.6**) | 56.26 us (**x46.88**) | 59.39 us (**x32.27**) |
+-------------------------+------------------------+-----------------------+----------------------------+----------------------------+
| jsonschema[PyPy] | 40.83 ns (**x0.43**) | 232.41 ns (**x2.45**) | 21.82 us (**x18.18**) | 22.23 us (**x12.08**) |
+-------------------------+------------------------+-----------------------+----------------------------+----------------------------+
Large schemas:
+-------------------------+-------------------------+--------------------------+----------------------------+---------------------------+
| library | ``Zuora (OpenAPI)`` | ``Kubernetes (Swagger)`` | ``Canada (GeoJSON)`` | ``CITM catalog`` |
+=========================+=========================+==========================+============================+===========================+
| jsonschema-rs[validate] | 17.311 ms | 15.194 ms | 5.018 ms | 4.765 ms |
+-------------------------+-------------------------+--------------------------+----------------------------+---------------------------+
| jsonschema-rs[is_valid] | 16.605 ms (**x0.95**) | 12.610 ms (**x0.82**) | 4.954 ms (**x0.98**) | 2.792 ms (**x0.58**) |
+-------------------------+-------------------------+--------------------------+----------------------------+---------------------------+
| jsonschema-rs[overhead] | 12.017 ms (**x0.69**) | 8.005 ms (**x0.52**) | 3.702 ms (**x0.73**) | 2.303 ms (**x0.48**) |
+-------------------------+-------------------------+--------------------------+----------------------------+---------------------------+
| fastjsonschema[CPython] | -- (1) | 90.305 ms (**x5.94**) | 32.389 ms (**6.45**) | 12.020 ms (**x2.52**) |
+-------------------------+-------------------------+--------------------------+----------------------------+---------------------------+
| fastjsonschema[PyPy] | -- (1) | 37.204 ms (**x2.44**) | 8.450 ms (**x1.68**) | 4.888 ms (**x1.02**) |
+-------------------------+-------------------------+--------------------------+----------------------------+---------------------------+
| jsonschema[CPython] | 764.172 ms (**x44.14**) | 1.063 s (**x69.96**) | 1.301 s (**x259.26**) | 115.362 ms (**x24.21**) |
+-------------------------+-------------------------+--------------------------+----------------------------+---------------------------+
| jsonschema[PyPy] | 604.557 ms (**x34.92**) | 619.744 ms (**x40.78**) | 524.275 ms (**x104.47**) | 25.275 ms (**x5.30**) |
+-------------------------+-------------------------+--------------------------+----------------------------+---------------------------+
Notes:
1. ``fastjsonschema`` fails to compile the Open API spec due to the presence of the ``uri-reference`` format (that is not defined in Draft 4). However, unknown formats are `explicitly supported <https://tools.ietf.org/html/draft-fge-json-schema-validation-00#section-7.1>`_ by the spec.
The bigger the input is the bigger is performance win. You can take a look at benchmarks in ``benches/bench.py``.
Package versions:
- ``jsonschema-rs`` - latest version from the repository
- ``jsonschema`` - ``3.2.0``
- ``fastjsonschema`` - ``2.15.1``
Measured with stable Rust 1.56, CPython 3.9.7 / PyPy3 7.3.6 on i8700K (12 cores), 32GB RAM, Arch Linux.
Python support
--------------
``jsonschema-rs`` supports CPython 3.7, 3.8, 3.9, 3.10, and 3.11.
License
-------
The code in this project is licensed under `MIT license`_.
By contributing to ``jsonschema-rs``, you agree that your contributions
will be licensed under its MIT license.
.. |Build| image:: https://github.com/Stranger6667/jsonschema-rs/workflows/ci/badge.svg
:target: https://github.com/Stranger6667/jsonschema-rs/actions
.. |Version| image:: https://img.shields.io/pypi/v/jsonschema-rs.svg
:target: https://pypi.org/project/jsonschema-rs/
.. |Python versions| image:: https://img.shields.io/pypi/pyversions/jsonschema-rs.svg
:target: https://pypi.org/project/jsonschema-rs/
.. |License| image:: https://img.shields.io/pypi/l/jsonschema-rs.svg
:target: https://opensource.org/licenses/MIT
.. _MIT license: https://opensource.org/licenses/MIT

View File

@ -36,8 +36,22 @@ CANADA = load_from_benches("canada.json")
CITM_CATALOG_SCHEMA = load_from_benches("citm_catalog_schema.json")
CITM_CATALOG = load_from_benches("citm_catalog.json")
FAST_SCHEMA = load_from_benches("fast_schema.json")
FAST_INSTANCE_VALID = [9, "hello", [1, "a", True], {"a": "a", "b": "b", "d": "d"}, 42, 3]
FAST_INSTANCE_INVALID = [10, "world", [1, "a", True], {"a": "a", "b": "b", "c": "xy"}, "str", 5]
FAST_INSTANCE_VALID = [
9,
"hello",
[1, "a", True],
{"a": "a", "b": "b", "d": "d"},
42,
3,
]
FAST_INSTANCE_INVALID = [
10,
"world",
[1, "a", True],
{"a": "a", "b": "b", "c": "xy"},
"str",
5,
]
@pytest.fixture(params=[True, False], ids=("compiled", "raw"))
@ -46,7 +60,12 @@ def is_compiled(request):
if jsonschema_rs is not None:
variants = ["jsonschema-rs-is-valid", "jsonschema-rs-validate", "jsonschema", "fastjsonschema"]
variants = [
"jsonschema-rs-is-valid",
"jsonschema-rs-validate",
"jsonschema",
"fastjsonschema",
]
else:
variants = ["jsonschema", "fastjsonschema"]
@ -66,12 +85,20 @@ def args(request, variant, is_compiled):
if is_compiled:
return jsonschema_rs.JSONSchema(schema, with_meta_schemas=True).is_valid, instance
else:
return partial(jsonschema_rs.is_valid, with_meta_schemas=True), schema, instance
return (
partial(jsonschema_rs.is_valid, with_meta_schemas=True),
schema,
instance,
)
if variant == "jsonschema-rs-validate":
if is_compiled:
return jsonschema_rs.JSONSchema(schema, with_meta_schemas=True).validate, instance
else:
return partial(jsonschema_rs.validate, with_meta_schemas=True), schema, instance
return (
partial(jsonschema_rs.validate, with_meta_schemas=True),
schema,
instance,
)
if variant == "jsonschema":
if is_compiled:
return jsonschema.validators.validator_for(schema)(schema).is_valid, instance
@ -85,7 +112,14 @@ def args(request, variant, is_compiled):
@pytest.mark.parametrize(
"name", ("openapi.json", "swagger.json", "geojson.json", "citm_catalog_schema.json", "fast_schema.json")
"name",
(
"openapi.json",
"swagger.json",
"geojson.json",
"citm_catalog_schema.json",
"fast_schema.json",
),
)
@pytest.mark.parametrize(
"func",

View File

@ -1,8 +1,4 @@
fn main() {
let src = std::env::var("CARGO_MANIFEST_DIR").unwrap();
let dst = std::path::Path::new(&std::env::var("OUT_DIR").unwrap()).join("built.rs");
let mut opts = built::Options::default();
opts.set_dependencies(true).set_compiler(true).set_env(true);
built::write_built_file_with_opts(&opts, std::path::Path::new(&src), &dst)
.expect("Failed to acquire build-time information");
built::write_built_file().expect("Failed to acquire build-time information");
pyo3_build_config::use_pyo3_cfgs();
}

View File

@ -3,12 +3,12 @@ name = "jsonschema_rs"
description = "Fast JSON Schema validation for Python implemented in Rust"
keywords = ["jsonschema", "validation", "rust"]
authors = [
{name = "Dmitry Dygalo", email = "dadygalo@gmail.com"}
{name = "Dmitry Dygalo", email = "dmitry@dygalo.dev"}
]
maintainers = [
{name = "Dmitry Dygalo", email = "dadygalo@gmail.com"}
{name = "Dmitry Dygalo", email = "dmitry@dygalo.dev"}
]
readme = "README.rst"
readme = "README.md"
license = { text = "MIT" }
classifiers = [
"Development Status :: 3 - Alpha",
@ -22,6 +22,7 @@ classifiers = [
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Rust",
]
@ -35,23 +36,21 @@ Changelog = "https://github.com/Stranger6667/jsonschema-rs/blob/master/bindings/
Source = "https://github.com/Stranger6667/jsonschema-rs"
Funding = 'https://github.com/sponsors/Stranger6667'
[tool.black]
[tool.ruff]
line-length = 120
target_version = ["py37"]
target-version = "py37"
[tool.isort]
# config compatible with Black
line_length = 120
multi_line_output = 3
default_section = "THIRDPARTY"
include_trailing_comma = true
known_first_party = "jsonschema_rs"
known_third_party = []
[tool.ruff.lint.isort]
known-first-party = ["jsonschema_rs"]
known-third-party = ["hypothesis", "pytest"]
[tool.ruff.format]
skip-magic-trailing-comma = false
[tool.maturin]
python-source = "python"
strip = true
[build-system]
requires = ["maturin>=0.14.11,<0.15"]
requires = ["maturin>=1.1"]
build-backend = "maturin"

View File

@ -1 +1 @@
from .jsonschema_rs import *
from .jsonschema_rs import * # noqa: F403

View File

@ -1,51 +1,54 @@
from typing import Any, TypeVar
from typing import Any, Callable, TypeVar
from collections.abc import Iterator
_SchemaT = TypeVar('_SchemaT', bool, dict[str, Any])
_SchemaT = TypeVar("_SchemaT", bool, dict[str, Any])
_FormatFunc = TypeVar("_FormatFunc", bound=Callable[[str], bool])
def is_valid(
schema: _SchemaT,
instance: Any,
draft: int | None = None,
with_meta_schemas: bool | None = None
schema: _SchemaT,
instance: Any,
draft: int | None = None,
with_meta_schemas: bool | None = None,
formats: dict[str, _FormatFunc] | None = None,
) -> bool:
pass
def validate(
schema: _SchemaT,
instance: Any,
draft: int | None = None,
with_meta_schemas: bool | None = None
schema: _SchemaT,
instance: Any,
draft: int | None = None,
with_meta_schemas: bool | None = None,
formats: dict[str, _FormatFunc] | None = None,
) -> None:
pass
def iter_errors(
schema: _SchemaT,
instance: Any,
draft: int | None = None,
with_meta_schemas: bool | None = None
schema: _SchemaT,
instance: Any,
draft: int | None = None,
with_meta_schemas: bool | None = None,
formats: dict[str, _FormatFunc] | None = None,
) -> Iterator[ValidationError]:
pass
class JSONSchema:
def __init__(
self,
schema: _SchemaT,
draft: int | None = None,
with_meta_schemas: bool | None = None
self,
schema: _SchemaT,
draft: int | None = None,
with_meta_schemas: bool | None = None,
formats: dict[str, _FormatFunc] | None = None,
) -> None:
pass
@classmethod
def from_str(
cls,
schema: str,
draft: int | None = None,
with_meta_schemas: bool | None = None
) -> 'JSONSchema':
cls,
schema: str,
draft: int | None = None,
with_meta_schemas: bool | None = None,
formats: dict[str, _FormatFunc] | None = None,
) -> "JSONSchema":
pass
def is_valid(self, instance: Any) -> bool:
@ -57,13 +60,13 @@ class JSONSchema:
def iter_errors(self, instance: Any) -> Iterator[ValidationError]:
pass
class ValidationError(ValueError):
message: str
schema_path: list[str | int]
instance_path: list[str | int]
Draft4: int
Draft6: int
Draft7: int
Draft201909: int
Draft202012: int

View File

@ -0,0 +1,2 @@
imports_granularity = "Crate"
edition = "2021"

View File

@ -5,7 +5,7 @@
clippy::match_same_arms,
clippy::needless_borrow,
clippy::print_stdout,
clippy::integer_arithmetic,
clippy::arithmetic_side_effects,
clippy::cast_possible_truncation,
clippy::map_unwrap_or,
clippy::unseparated_literal_suffix,
@ -16,24 +16,32 @@
)]
#![allow(clippy::upper_case_acronyms)]
use std::{
any::Any,
cell::RefCell,
panic::{self, AssertUnwindSafe},
};
use jsonschema::{paths::JSONPointer, Draft};
use pyo3::{
exceptions::{self, PyValueError},
ffi::PyUnicode_AsUTF8AndSize,
prelude::*,
types::{PyAny, PyList, PyType},
wrap_pyfunction, AsPyPointer,
types::{PyAny, PyDict, PyList, PyString, PyType},
wrap_pyfunction,
};
#[macro_use]
extern crate pyo3_built;
mod ffi;
mod ser;
mod string;
mod types;
const DRAFT7: u8 = 7;
const DRAFT6: u8 = 6;
const DRAFT4: u8 = 4;
const DRAFT201909: u8 = 19;
const DRAFT202012: u8 = 20;
/// An instance is invalid under a provided schema.
#[pyclass(extends=exceptions::PyValueError, module="jsonschema_rs")]
@ -87,19 +95,19 @@ impl ValidationErrorIter {
}
fn into_py_err(py: Python<'_>, error: jsonschema::ValidationError<'_>) -> PyResult<PyErr> {
let pyerror_type = PyType::new::<ValidationError>(py);
let pyerror_type = PyType::new_bound::<ValidationError>(py);
let message = error.to_string();
let verbose_message = to_error_message(&error);
let schema_path = into_path(py, error.schema_path)?;
let instance_path = into_path(py, error.instance_path)?;
Ok(PyErr::from_type(
Ok(PyErr::from_type_bound(
pyerror_type,
(message, verbose_message, schema_path, instance_path),
))
}
fn into_path(py: Python<'_>, pointer: JSONPointer) -> PyResult<Py<PyList>> {
let path = PyList::empty(py);
let path = PyList::empty_bound(py);
for chunk in pointer {
match chunk {
jsonschema::paths::PathChunk::Property(property) => {
@ -109,7 +117,7 @@ fn into_path(py: Python<'_>, pointer: JSONPointer) -> PyResult<Py<PyList>> {
jsonschema::paths::PathChunk::Keyword(keyword) => path.append(keyword)?,
};
}
Ok(path.into_py(py))
Ok(path.unbind())
}
fn get_draft(draft: u8) -> PyResult<Draft> {
@ -117,6 +125,8 @@ fn get_draft(draft: u8) -> PyResult<Draft> {
DRAFT4 => Ok(jsonschema::Draft::Draft4),
DRAFT6 => Ok(jsonschema::Draft::Draft6),
DRAFT7 => Ok(jsonschema::Draft::Draft7),
DRAFT201909 => Ok(jsonschema::Draft::Draft201909),
DRAFT202012 => Ok(jsonschema::Draft::Draft202012),
_ => Err(exceptions::PyValueError::new_err(format!(
"Unknown draft: {}",
draft
@ -124,9 +134,14 @@ fn get_draft(draft: u8) -> PyResult<Draft> {
}
}
thread_local! {
static LAST_FORMAT_ERROR: RefCell<Option<PyErr>> = const { RefCell::new(None) };
}
fn make_options(
draft: Option<u8>,
with_meta_schemas: Option<bool>,
formats: Option<&Bound<'_, PyDict>>,
) -> PyResult<jsonschema::CompilationOptions> {
let mut options = jsonschema::JSONSchema::options();
if let Some(raw_draft_version) = draft {
@ -135,22 +150,57 @@ fn make_options(
if with_meta_schemas == Some(true) {
options.with_meta_schemas();
}
if let Some(formats) = formats {
for (name, callback) in formats.iter() {
if !callback.is_callable() {
return Err(exceptions::PyValueError::new_err(format!(
"Format checker for '{}' must be a callable",
name
)));
}
let callback: Py<PyAny> = callback.clone().unbind();
let call_py_callback = move |value: &str| {
Python::with_gil(|py| {
let value = PyString::new_bound(py, value);
callback.call_bound(py, (value,), None)?.is_truthy(py)
})
};
options.with_format(
name.to_string(),
move |value: &str| match call_py_callback(value) {
Ok(r) => r,
Err(e) => {
LAST_FORMAT_ERROR.with(|last| {
*last.borrow_mut() = Some(e);
});
std::panic::set_hook(Box::new(|_| {}));
// Should be caught
panic!("Format checker failed")
}
},
);
}
}
Ok(options)
}
fn iter_on_error(
py: Python<'_>,
compiled: &jsonschema::JSONSchema,
instance: &PyAny,
instance: &Bound<'_, PyAny>,
) -> PyResult<ValidationErrorIter> {
let instance = ser::to_value(instance)?;
let mut pyerrors = vec![];
if let Err(errors) = compiled.validate(&instance) {
for error in errors {
pyerrors.push(into_py_err(py, error)?);
}
};
panic::catch_unwind(AssertUnwindSafe(|| {
if let Err(errors) = compiled.validate(&instance) {
for error in errors {
pyerrors.push(into_py_err(py, error)?);
}
};
PyResult::Ok(())
}))
.map_err(handle_format_checked_panic)??;
Ok(ValidationErrorIter {
iter: pyerrors.into_iter(),
})
@ -159,10 +209,11 @@ fn iter_on_error(
fn raise_on_error(
py: Python<'_>,
compiled: &jsonschema::JSONSchema,
instance: &PyAny,
instance: &Bound<'_, PyAny>,
) -> PyResult<()> {
let instance = ser::to_value(instance)?;
let result = compiled.validate(&instance);
let result = panic::catch_unwind(AssertUnwindSafe(|| compiled.validate(&instance)))
.map_err(handle_format_checked_panic)?;
let error = result
.err()
.map(|mut errors| errors.next().expect("Iterator should not be empty"));
@ -223,7 +274,7 @@ fn to_error_message(error: &jsonschema::ValidationError<'_>) -> String {
message
}
/// is_valid(schema, instance, draft=None, with_meta_schemas=False)
/// is_valid(schema, instance, draft=None, with_meta_schemas=False, formats=None)
///
/// A shortcut for validating the input instance against the schema.
///
@ -233,26 +284,28 @@ fn to_error_message(error: &jsonschema::ValidationError<'_>) -> String {
/// If your workflow implies validating against the same schema, consider using `JSONSchema.is_valid`
/// instead.
#[pyfunction]
#[pyo3(text_signature = "(schema, instance, draft=None, with_meta_schemas=False)")]
#[pyo3(text_signature = "(schema, instance, draft=None, with_meta_schemas=False, formats=None)")]
fn is_valid(
py: Python<'_>,
schema: &PyAny,
instance: &PyAny,
schema: &Bound<'_, PyAny>,
instance: &Bound<'_, PyAny>,
draft: Option<u8>,
with_meta_schemas: Option<bool>,
formats: Option<&Bound<'_, PyDict>>,
) -> PyResult<bool> {
let options = make_options(draft, with_meta_schemas)?;
let options = make_options(draft, with_meta_schemas, formats)?;
let schema = ser::to_value(schema)?;
match options.compile(&schema) {
Ok(compiled) => {
let instance = ser::to_value(instance)?;
Ok(compiled.is_valid(&instance))
panic::catch_unwind(AssertUnwindSafe(|| Ok(compiled.is_valid(&instance))))
.map_err(handle_format_checked_panic)?
}
Err(error) => Err(into_py_err(py, error)?),
}
}
/// validate(schema, instance, draft=None, with_meta_schemas=False)
/// validate(schema, instance, draft=None, with_meta_schemas=False, formats=None)
///
/// Validate the input instance and raise `ValidationError` in the error case
///
@ -264,15 +317,16 @@ fn is_valid(
/// If your workflow implies validating against the same schema, consider using `JSONSchema.validate`
/// instead.
#[pyfunction]
#[pyo3(text_signature = "(schema, instance, draft=None, with_meta_schemas=False)")]
#[pyo3(text_signature = "(schema, instance, draft=None, with_meta_schemas=False, formats=None)")]
fn validate(
py: Python<'_>,
schema: &PyAny,
instance: &PyAny,
schema: &Bound<'_, PyAny>,
instance: &Bound<'_, PyAny>,
draft: Option<u8>,
with_meta_schemas: Option<bool>,
formats: Option<&Bound<'_, PyDict>>,
) -> PyResult<()> {
let options = make_options(draft, with_meta_schemas)?;
let options = make_options(draft, with_meta_schemas, formats)?;
let schema = ser::to_value(schema)?;
match options.compile(&schema) {
Ok(compiled) => raise_on_error(py, &compiled, instance),
@ -280,7 +334,7 @@ fn validate(
}
}
/// iter_errors(schema, instance, draft=None, with_meta_schemas=False)
/// iter_errors(schema, instance, draft=None, with_meta_schemas=False, formats=None)
///
/// Iterate the validation errors of the input instance
///
@ -291,15 +345,16 @@ fn validate(
/// If your workflow implies validating against the same schema, consider using `JSONSchema.iter_errors`
/// instead.
#[pyfunction]
#[pyo3(text_signature = "(schema, instance, draft=None, with_meta_schemas=False)")]
#[pyo3(text_signature = "(schema, instance, draft=None, with_meta_schemas=False, formats=None)")]
fn iter_errors(
py: Python<'_>,
schema: &PyAny,
instance: &PyAny,
schema: &Bound<'_, PyAny>,
instance: &Bound<'_, PyAny>,
draft: Option<u8>,
with_meta_schemas: Option<bool>,
formats: Option<&Bound<'_, PyDict>>,
) -> PyResult<ValidationErrorIter> {
let options = make_options(draft, with_meta_schemas)?;
let options = make_options(draft, with_meta_schemas, formats)?;
let schema = ser::to_value(schema)?;
match options.compile(&schema) {
Ok(compiled) => iter_on_error(py, &compiled, instance),
@ -317,7 +372,6 @@ fn iter_errors(
///
/// By default Draft 7 will be used for compilation.
#[pyclass(module = "jsonschema_rs")]
#[pyo3(text_signature = "(schema, draft=None, with_meta_schemas=False)")]
struct JSONSchema {
schema: jsonschema::JSONSchema,
repr: String,
@ -335,16 +389,29 @@ fn get_schema_repr(schema: &serde_json::Value) -> String {
repr
}
fn handle_format_checked_panic(err: Box<dyn Any + Send>) -> PyErr {
LAST_FORMAT_ERROR.with(|last| {
if let Some(err) = last.borrow_mut().take() {
let _ = panic::take_hook();
err
} else {
exceptions::PyRuntimeError::new_err(format!("Validation panicked: {:?}", err))
}
})
}
#[pymethods]
impl JSONSchema {
#[new]
#[pyo3(text_signature = "(schema, draft=None, with_meta_schemas=False, formats=None)")]
fn new(
py: Python<'_>,
pyschema: &PyAny,
pyschema: &Bound<'_, PyAny>,
draft: Option<u8>,
with_meta_schemas: Option<bool>,
formats: Option<&Bound<'_, PyDict>>,
) -> PyResult<Self> {
let options = make_options(draft, with_meta_schemas)?;
let options = make_options(draft, with_meta_schemas, formats)?;
let raw_schema = ser::to_value(pyschema)?;
match options.compile(&raw_schema) {
Ok(schema) => Ok(JSONSchema {
@ -354,7 +421,7 @@ impl JSONSchema {
Err(error) => Err(into_py_err(py, error)?),
}
}
/// from_str(string, draft=None, with_meta_schemas=False)
/// from_str(string, draft=None, with_meta_schemas=False, formats=None)
///
/// Create `JSONSchema` from a serialized JSON string.
///
@ -362,13 +429,14 @@ impl JSONSchema {
///
/// Use it if you have your schema as a string and want to utilize Rust JSON parsing.
#[classmethod]
#[pyo3(text_signature = "(string, draft=None, with_meta_schemas=False)")]
#[pyo3(text_signature = "(string, draft=None, with_meta_schemas=False, formats=None)")]
fn from_str(
_: &PyType,
_: &Bound<'_, PyType>,
py: Python<'_>,
pyschema: &PyAny,
pyschema: &Bound<'_, PyAny>,
draft: Option<u8>,
with_meta_schemas: Option<bool>,
formats: Option<&Bound<'_, PyDict>>,
) -> PyResult<Self> {
let obj_ptr = pyschema.as_ptr();
let object_type = unsafe { pyo3::ffi::Py_TYPE(obj_ptr) };
@ -381,11 +449,11 @@ impl JSONSchema {
)))
} else {
let mut str_size: pyo3::ffi::Py_ssize_t = 0;
let uni = unsafe { string::read_utf8_from_str(obj_ptr, &mut str_size) };
let slice = unsafe { std::slice::from_raw_parts(uni, str_size as usize) };
let ptr = unsafe { PyUnicode_AsUTF8AndSize(obj_ptr, &mut str_size) };
let slice = unsafe { std::slice::from_raw_parts(ptr.cast::<u8>(), str_size as usize) };
let raw_schema = serde_json::from_slice(slice)
.map_err(|error| PyValueError::new_err(format!("Invalid string: {}", error)))?;
let options = make_options(draft, with_meta_schemas)?;
let options = make_options(draft, with_meta_schemas, formats)?;
match options.compile(&raw_schema) {
Ok(schema) => Ok(JSONSchema {
schema,
@ -406,9 +474,10 @@ impl JSONSchema {
///
/// The output is a boolean value, that indicates whether the instance is valid or not.
#[pyo3(text_signature = "(instance)")]
fn is_valid(&self, instance: &PyAny) -> PyResult<bool> {
fn is_valid(&self, instance: &Bound<'_, PyAny>) -> PyResult<bool> {
let instance = ser::to_value(instance)?;
Ok(self.schema.is_valid(&instance))
panic::catch_unwind(AssertUnwindSafe(|| Ok(self.schema.is_valid(&instance))))
.map_err(handle_format_checked_panic)?
}
/// validate(instance)
@ -422,7 +491,7 @@ impl JSONSchema {
///
/// If the input instance is invalid, only the first occurred error is raised.
#[pyo3(text_signature = "(instance)")]
fn validate(&self, py: Python<'_>, instance: &PyAny) -> PyResult<()> {
fn validate(&self, py: Python<'_>, instance: &Bound<'_, PyAny>) -> PyResult<()> {
raise_on_error(py, &self.schema, instance)
}
@ -435,7 +504,11 @@ impl JSONSchema {
/// ...
/// ValidationError: 3 is less than the minimum of 5
#[pyo3(text_signature = "(instance)")]
fn iter_errors(&self, py: Python<'_>, instance: &PyAny) -> PyResult<ValidationErrorIter> {
fn iter_errors(
&self,
py: Python<'_>,
instance: &Bound<'_, PyAny>,
) -> PyResult<ValidationErrorIter> {
iter_on_error(py, &self.schema, instance)
}
fn __repr__(&self) -> String {
@ -450,7 +523,7 @@ mod build {
/// JSON Schema validation for Python written in Rust.
#[pymodule]
fn jsonschema_rs(py: Python<'_>, module: &PyModule) -> PyResult<()> {
fn jsonschema_rs(py: Python<'_>, module: &Bound<'_, PyModule>) -> PyResult<()> {
// To provide proper signatures for PyCharm, all the functions have their signatures as the
// first line in docstrings. The idea is taken from NumPy.
types::init();
@ -458,10 +531,12 @@ fn jsonschema_rs(py: Python<'_>, module: &PyModule) -> PyResult<()> {
module.add_wrapped(wrap_pyfunction!(validate))?;
module.add_wrapped(wrap_pyfunction!(iter_errors))?;
module.add_class::<JSONSchema>()?;
module.add("ValidationError", py.get_type::<ValidationError>())?;
module.add("ValidationError", py.get_type_bound::<ValidationError>())?;
module.add("Draft4", DRAFT4)?;
module.add("Draft6", DRAFT6)?;
module.add("Draft7", DRAFT7)?;
module.add("Draft201909", DRAFT201909)?;
module.add("Draft202012", DRAFT202012)?;
// Add build metadata to ease triaging incoming issues
module.add("__build__", pyo3_built::pyo3_built!(py, build))?;

View File

@ -2,18 +2,18 @@ use pyo3::{
exceptions,
ffi::{
PyDictObject, PyFloat_AS_DOUBLE, PyList_GET_ITEM, PyList_GET_SIZE, PyLong_AsLongLong,
PyObject_GetAttr, PyTuple_GET_ITEM, PyTuple_GET_SIZE, Py_TYPE,
PyObject_GetAttr, PyTuple_GET_ITEM, PyTuple_GET_SIZE, PyUnicode_AsUTF8AndSize, Py_DECREF,
Py_TYPE,
},
prelude::*,
types::PyAny,
AsPyPointer,
};
use serde::{
ser::{self, Serialize, SerializeMap, SerializeSeq},
Serializer,
};
use crate::{ffi, string, types};
use crate::{ffi, types};
use std::ffi::CStr;
pub const RECURSION_LIMIT: u8 = 255;
@ -67,6 +67,19 @@ fn is_enum_subclass(object_type: *mut pyo3::ffi::PyTypeObject) -> bool {
unsafe { (*(object_type.cast::<ffi::PyTypeObject>())).ob_type == types::ENUM_TYPE }
}
#[inline]
fn is_dict_subclass(object_type: *mut pyo3::ffi::PyTypeObject) -> bool {
// traverse the object's inheritance chain to check if it's a dict subclass
let mut current_type = object_type;
while !current_type.is_null() {
if current_type == unsafe { types::DICT_TYPE } {
return true;
}
current_type = unsafe { (*current_type).tp_base }
}
false
}
fn get_object_type_from_object(object: *mut pyo3::ffi::PyObject) -> ObjectType {
unsafe {
let object_type = Py_TYPE(object);
@ -110,11 +123,38 @@ pub fn get_object_type(object_type: *mut pyo3::ffi::PyTypeObject) -> ObjectType
ObjectType::Dict
} else if is_enum_subclass(object_type) {
ObjectType::Enum
} else if is_dict_subclass(object_type) {
ObjectType::Dict
} else {
ObjectType::Unknown(get_type_name(object_type).to_string())
}
}
macro_rules! bail_on_integer_conversion_error {
($value:expr) => {
if !$value.is_null() {
let repr = unsafe { pyo3::ffi::PyObject_Str($value) };
let mut size = 0;
let ptr = unsafe { PyUnicode_AsUTF8AndSize(repr, &mut size) };
return if !ptr.is_null() {
let slice = unsafe {
std::str::from_utf8_unchecked(std::slice::from_raw_parts(
ptr.cast::<u8>(),
size as usize,
))
};
let message = String::from(slice);
unsafe { Py_DECREF(repr) };
Err(ser::Error::custom(message))
} else {
Err(ser::Error::custom(
"Internal Error: Failed to convert exception to string",
))
};
}
};
}
/// Convert a Python value to `serde_json::Value`
impl Serialize for SerializePyObject {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
@ -124,16 +164,34 @@ impl Serialize for SerializePyObject {
match self.object_type {
ObjectType::Str => {
let mut str_size: pyo3::ffi::Py_ssize_t = 0;
let uni = unsafe { string::read_utf8_from_str(self.object, &mut str_size) };
let ptr = unsafe { PyUnicode_AsUTF8AndSize(self.object, &mut str_size) };
let slice = unsafe {
std::str::from_utf8_unchecked(std::slice::from_raw_parts(
uni,
ptr.cast::<u8>(),
str_size as usize,
))
};
serializer.serialize_str(slice)
}
ObjectType::Int => serializer.serialize_i64(unsafe { PyLong_AsLongLong(self.object) }),
ObjectType::Int => {
let value = unsafe { PyLong_AsLongLong(self.object) };
if value == -1 {
#[cfg(Py_3_12)]
{
let exception = unsafe { pyo3::ffi::PyErr_GetRaisedException() };
bail_on_integer_conversion_error!(exception);
};
#[cfg(not(Py_3_12))]
{
let mut ptype: *mut pyo3::ffi::PyObject = std::ptr::null_mut();
let mut pvalue: *mut pyo3::ffi::PyObject = std::ptr::null_mut();
let mut ptraceback: *mut pyo3::ffi::PyObject = std::ptr::null_mut();
unsafe { pyo3::ffi::PyErr_Fetch(&mut ptype, &mut pvalue, &mut ptraceback) };
bail_on_integer_conversion_error!(pvalue);
};
}
serializer.serialize_i64(value)
}
ObjectType::Float => {
serializer.serialize_f64(unsafe { PyFloat_AS_DOUBLE(self.object) })
}
@ -157,14 +215,14 @@ impl Serialize for SerializePyObject {
pyo3::ffi::PyDict_Next(self.object, &mut pos, &mut key, &mut value);
}
check_type_is_str(key)?;
let uni = unsafe { string::read_utf8_from_str(key, &mut str_size) };
let ptr = unsafe { PyUnicode_AsUTF8AndSize(key, &mut str_size) };
let slice = unsafe {
std::str::from_utf8_unchecked(std::slice::from_raw_parts(
uni,
ptr.cast::<u8>(),
str_size as usize,
))
};
#[allow(clippy::integer_arithmetic)]
#[allow(clippy::arithmetic_side_effects)]
map.serialize_entry(
slice,
&SerializePyObject::new(value, self.recursion_depth + 1),
@ -191,7 +249,7 @@ impl Serialize for SerializePyObject {
type_ptr = current_ob_type;
ob_type = get_object_type(current_ob_type);
}
#[allow(clippy::integer_arithmetic)]
#[allow(clippy::arithmetic_side_effects)]
sequence.serialize_element(&SerializePyObject::with_obtype(
elem,
ob_type.clone(),
@ -219,7 +277,7 @@ impl Serialize for SerializePyObject {
type_ptr = current_ob_type;
ob_type = get_object_type(current_ob_type);
}
#[allow(clippy::integer_arithmetic)]
#[allow(clippy::arithmetic_side_effects)]
sequence.serialize_element(&SerializePyObject::with_obtype(
elem,
ob_type.clone(),
@ -231,7 +289,7 @@ impl Serialize for SerializePyObject {
}
ObjectType::Enum => {
let value = unsafe { PyObject_GetAttr(self.object, types::VALUE_STR) };
#[allow(clippy::integer_arithmetic)]
#[allow(clippy::arithmetic_side_effects)]
SerializePyObject::new(value, self.recursion_depth + 1).serialize(serializer)
}
ObjectType::Unknown(ref type_name) => Err(ser::Error::custom(format!(
@ -243,7 +301,7 @@ impl Serialize for SerializePyObject {
}
#[inline]
pub(crate) fn to_value(object: &PyAny) -> PyResult<serde_json::Value> {
pub(crate) fn to_value(object: &Bound<'_, PyAny>) -> PyResult<serde_json::Value> {
serde_json::to_value(SerializePyObject::new(object.as_ptr(), 0))
.map_err(|err| exceptions::PyValueError::new_err(err.to_string()))
}

View File

@ -1,48 +0,0 @@
use pyo3::ffi::{PyTypeObject, PyUnicode_AsUTF8AndSize, Py_UNICODE, Py_hash_t, Py_ssize_t};
use std::os::raw::c_char;
#[repr(C)]
struct PyAsciiObject {
pub ob_refcnt: Py_ssize_t,
pub ob_type: *mut PyTypeObject,
pub length: Py_ssize_t,
pub hash: Py_hash_t,
pub state: u32,
pub wstr: *mut c_char,
}
#[repr(C)]
struct PyCompactUnicodeObject {
pub ob_refcnt: Py_ssize_t,
pub ob_type: *mut PyTypeObject,
pub length: Py_ssize_t,
pub hash: Py_hash_t,
pub state: u32,
pub wstr: *mut Py_UNICODE,
pub utf8_length: Py_ssize_t,
pub utf8: *mut c_char,
pub wstr_length: Py_ssize_t,
}
const STATE_ASCII: u32 = 0b0000_0000_0000_0000_0000_0000_0100_0000;
const STATE_COMPACT: u32 = 0b0000_0000_0000_0000_0000_0000_0010_0000;
/// Read a UTF-8 string from a pointer and change the given size if needed.
pub unsafe fn read_utf8_from_str(
object_pointer: *mut pyo3::ffi::PyObject,
size: &mut Py_ssize_t,
) -> *const u8 {
if (*object_pointer.cast::<PyAsciiObject>()).state & STATE_ASCII == STATE_ASCII {
*size = (*object_pointer.cast::<PyAsciiObject>()).length;
object_pointer.cast::<PyAsciiObject>().offset(1) as *const u8
} else if (*object_pointer.cast::<PyAsciiObject>()).state & STATE_COMPACT == STATE_COMPACT
&& !(*object_pointer.cast::<PyCompactUnicodeObject>())
.utf8
.is_null()
{
*size = (*object_pointer.cast::<PyCompactUnicodeObject>()).utf8_length;
(*object_pointer.cast::<PyCompactUnicodeObject>()).utf8 as *const u8
} else {
PyUnicode_AsUTF8AndSize(object_pointer, size).cast::<u8>()
}
}

View File

@ -1,6 +1,6 @@
import sys
import uuid
from collections import namedtuple
from collections import OrderedDict, namedtuple
from contextlib import suppress
from enum import Enum
from functools import partial
@ -141,14 +141,14 @@ def test_initialization_errors(schema, draft, error):
@given(minimum=st.integers().map(abs))
def test_minimum(minimum):
with suppress(SystemError):
with suppress(SystemError, ValueError):
assert is_valid({"minimum": minimum}, minimum)
assert is_valid({"minimum": minimum}, minimum - 1) is False
@given(maximum=st.integers().map(abs))
def test_maximum(maximum):
with suppress(SystemError):
with suppress(SystemError, ValueError):
assert is_valid({"maximum": maximum}, maximum)
assert is_valid({"maximum": maximum}, maximum + 1) is False
@ -248,3 +248,68 @@ def test_dict_with_non_str_keys():
with pytest.raises(ValueError) as exec_info:
validate(schema, instance)
assert exec_info.value.args[0] == "Dict key must be str. Got 'UUID'"
class MyDict(dict):
pass
class MyDict2(MyDict):
pass
@pytest.mark.parametrize(
"type_, value, expected",
(
(dict, 1, True),
(dict, "bar", False),
(OrderedDict, 1, True),
(OrderedDict, "bar", False),
(MyDict, 1, True),
(MyDict, "bar", False),
(MyDict2, 1, True),
(MyDict2, "bar", False),
),
)
def test_dict_subclasses(type_, value, expected):
schema = {"type": "object", "properties": {"foo": {"type": "integer"}}}
document = type_({"foo": value})
assert is_valid(schema, document) is expected
def test_custom_format():
def is_currency(value):
return len(value) == 3 and value.isascii()
validator = JSONSchema({"type": "string", "format": "currency"}, formats={"currency": is_currency})
assert validator.is_valid("USD")
assert not validator.is_valid(42)
assert not validator.is_valid("invalid")
def test_custom_format_invalid_callback():
with pytest.raises(ValueError, match="Format checker for 'currency' must be a callable"):
JSONSchema({"type": "string", "format": "currency"}, formats={"currency": 42})
def test_custom_format_with_exception():
def is_currency(_):
raise ValueError("Invalid currency")
schema = {"type": "string", "format": "currency"}
formats = {"currency": is_currency}
validator = JSONSchema(schema, formats=formats)
with pytest.raises(ValueError, match="Invalid currency"):
validator.is_valid("USD")
with pytest.raises(ValueError, match="Invalid currency"):
validator.validate("USD")
with pytest.raises(ValueError, match="Invalid currency"):
for _ in validator.iter_errors("USD"):
pass
with pytest.raises(ValueError, match="Invalid currency"):
is_valid(schema, "USD", formats=formats)
with pytest.raises(ValueError, match="Invalid currency"):
validate(schema, "USD", formats=formats)
with pytest.raises(ValueError, match="Invalid currency"):
for _ in iter_errors(schema, "USD", formats=formats):
pass

View File

@ -72,9 +72,16 @@ def maybe_optional(draft, schema, instance, expected, description, filename):
def pytest_generate_tests(metafunc):
cases = [
maybe_optional(draft, block["schema"], test["data"], test["valid"], test["description"], filename)
maybe_optional(
draft,
block["schema"],
test["data"],
test["valid"],
test["description"],
filename,
)
for draft in SUPPORTED_DRAFTS
for root, dirs, files in os.walk(f"{TEST_SUITE_PATH}/tests/draft{draft}/")
for root, _, files in os.walk(f"{TEST_SUITE_PATH}/tests/draft{draft}/")
for filename in files
for block in load_file(os.path.join(root, filename))
for test in block["tests"]
@ -85,7 +92,7 @@ def pytest_generate_tests(metafunc):
def test_draft(filename, draft, schema, instance, expected, description):
error_message = f"[{filename}] {description}: {schema} | {instance}"
try:
result = jsonschema_rs.is_valid(schema, instance, int(draft))
result = jsonschema_rs.is_valid(schema, instance, int(draft), with_meta_schemas=True)
assert result is expected, error_message
except ValueError:
pytest.fail(error_message)

View File

@ -1,6 +1,6 @@
[tox]
skipsdist = True
envlist = py{37,38,39,310,311}
envlist = py{37,38,39,310,311,312}
[testenv]
deps =

View File

@ -28,4 +28,4 @@ regex = "1"
quote = "1"
serde = { version = "1", features = [ "derive" ] }
serde_json = "1"
syn = "1"
syn = { version = "1", features = ["full"] }

View File

@ -41,7 +41,7 @@ pub(crate) fn setup(json_schema_test_suite_path: &Path) -> Vec<TokenStream> {
let path = remote_path
.trim_start_matches(base_path)
.replace(std::path::MAIN_SEPARATOR, "/");
if let Ok(file_content) = std::fs::read_to_string(remote_path) {
if let Ok(file_content) = fs::read_to_string(remote_path) {
Some(quote! {
mockito::mock("GET", #path)
.with_body(

View File

@ -39,9 +39,15 @@ fn draft_version(json_schema_test_suite_path: &Path, file_path: &Path) -> String
fn load_inner(json_schema_test_suite_path: &Path, dir: &Path, prefix: &str) -> Vec<TestCase> {
let mut tests = vec![];
for result_entry in
fs::read_dir(dir).unwrap_or_else(|_| panic!("Tests directory not found: {}", dir.display()))
{
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) {

View File

@ -0,0 +1,2 @@
imports_granularity = "Crate"
edition = "2021"

View File

@ -1,5 +1,5 @@
[package]
authors = ["dmitry.dygalo <dadygalo@gmail.com>"]
authors = ["Dmitry Dygalo <dmitry@dygalo.dev>"]
description = "A crate for performing JSON schema validation"
edition = "2021"
exclude = [
@ -18,7 +18,7 @@ license = "MIT"
name = "jsonschema"
readme = "../README.md"
repository = "https://github.com/Stranger6667/jsonschema-rs"
version = "0.17.1"
version = "0.18.0"
rust-version = "1.56.1"
categories = ["web-programming"]
@ -38,29 +38,29 @@ resolve-file = []
[dependencies]
ahash = { version = "0.8", features = ["serde"] }
anyhow = "1.0"
base64 = "0.21"
base64 = "0.22"
bytecount = { version = "0.6", features = ["runtime-dispatch-simd"] }
clap = { version = "4.0", features = ["derive"], optional = true }
fancy-regex = "0.11"
fraction = { version = "0.13", default-features = false, features = [
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.5"
memchr = "2.7"
num-cmp = "0.1"
once_cell = "1.17"
once_cell = "1.19"
parking_lot = "0.12"
percent-encoding = "2.1"
regex = "1.6"
reqwest = { version = "0.11", features = [
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.2"
url = "2.5"
uuid = "1"
[target.'cfg(target_arch = "wasm32")'.dependencies]
@ -68,14 +68,16 @@ getrandom = { version = "0.2", features = ["js"] }
[dev-dependencies]
bench_helpers = { path = "../bench_helpers" }
boon = "0.5"
codspeed-criterion-compat = "2.6.0"
criterion = { version = "0.5.1", features = [], default-features = false }
lazy_static = "1.4" # Needed for json schema test suite
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 = "3.6"
valico = "4"
# Benchmarks for `jsonschema`
[[bench]]
@ -92,6 +94,11 @@ name = "valico"
harness = false
name = "jsonschema_valid"
# Benchmarks for `boon`
[[bench]]
harness = false
name = "boon"
[profile.release]
codegen-units = 1
lto = "fat"

View File

@ -0,0 +1,53 @@
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);

View File

@ -1,11 +1,11 @@
use bench_helpers::{
bench_citm, bench_fast, bench_geojson, bench_keywords, bench_openapi, bench_swagger,
};
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
use jsonschema::JSONSchema;
use codspeed_criterion_compat::{criterion_group, criterion_main, Bencher, BenchmarkId, Criterion};
use jsonschema::{paths::JsonPointerNode, JSONSchema};
use serde_json::Value;
macro_rules! jsonschema_rs_bench {
macro_rules! jsonschema_bench {
($c:tt, $name:expr, $schema:ident, $instance:ident) => {{
let compiled = JSONSchema::options()
.with_meta_schemas()
@ -13,13 +13,13 @@ macro_rules! jsonschema_rs_bench {
.expect("Invalid schema");
assert!(compiled.is_valid(&$instance), "Invalid instance");
assert!(compiled.validate(&$instance).is_ok(), "Invalid instance");
$c.bench_function(&format!("{} jsonschema_rs/compile", $name), |b| {
$c.bench_function(&format!("{} jsonschema/compile", $name), |b| {
b.iter(|| JSONSchema::options().with_meta_schemas().compile(&$schema))
});
$c.bench_function(&format!("{} jsonschema_rs/is_valid", $name), |b| {
$c.bench_function(&format!("{} jsonschema/is_valid", $name), |b| {
b.iter(|| compiled.is_valid(&$instance))
});
$c.bench_function(&format!("{} jsonschema_rs/validate", $name), |b| {
$c.bench_function(&format!("{} jsonschema/validate", $name), |b| {
b.iter(|| compiled.validate(&$instance).ok())
});
}};
@ -28,13 +28,13 @@ macro_rules! jsonschema_rs_bench {
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_rs_bench!(c, name, schema, instance));
bench_openapi(&mut |name, schema, instance| jsonschema_bench!(c, name, schema, instance));
// Swagger JSON Schema
bench_swagger(&mut |name, schema, instance| jsonschema_rs_bench!(c, name, schema, instance));
bench_swagger(&mut |name, schema, instance| jsonschema_bench!(c, name, schema, instance));
// Canada borders in GeoJSON
bench_geojson(&mut |name, schema, instance| jsonschema_rs_bench!(c, name, schema, instance));
bench_geojson(&mut |name, schema, instance| jsonschema_bench!(c, name, schema, instance));
// CITM catalog
bench_citm(&mut |name, schema, instance| jsonschema_rs_bench!(c, name, schema, instance));
bench_citm(&mut |name, schema, instance| jsonschema_bench!(c, name, schema, instance));
}
fn fast_schema(c: &mut Criterion) {
@ -42,19 +42,19 @@ fn fast_schema(c: &mut Criterion) {
let compiled = JSONSchema::compile(&schema).expect("Valid schema");
assert!(compiled.is_valid(&valid));
assert!(!compiled.is_valid(&invalid));
c.bench_function(&format!("{} jsonschema_rs/compile", name), |b| {
c.bench_function(&format!("{} jsonschema/compile", name), |b| {
b.iter(|| JSONSchema::compile(&schema).expect("Valid schema"))
});
c.bench_function(&format!("{} jsonschema_rs/is_valid/valid", name), |b| {
c.bench_function(&format!("{} jsonschema/is_valid/valid", name), |b| {
b.iter(|| compiled.is_valid(&valid))
});
c.bench_function(&format!("{} jsonschema_rs/validate/valid", name), |b| {
c.bench_function(&format!("{} jsonschema/validate/valid", name), |b| {
b.iter(|| compiled.validate(&valid).ok())
});
c.bench_function(&format!("{} jsonschema_rs/is_valid/invalid", name), |b| {
c.bench_function(&format!("{} jsonschema/is_valid/invalid", name), |b| {
b.iter(|| compiled.is_valid(&invalid))
});
c.bench_function(&format!("{} jsonschema_rs/validate/invalid", name), |b| {
c.bench_function(&format!("{} jsonschema/validate/invalid", name), |b| {
b.iter(|| {
let _: Vec<_> = compiled
.validate(&invalid)
@ -75,7 +75,7 @@ fn keywords(c: &mut Criterion) {
},
&mut |c: &mut Criterion, name: &str, schema: &Value| {
c.bench_with_input(
BenchmarkId::new(name, "jsonschema_rs/compile"),
BenchmarkId::new(name, "jsonschema/compile"),
schema,
|b, schema| {
b.iter(|| {
@ -92,7 +92,7 @@ fn keywords(c: &mut Criterion) {
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_rs/is_valid/valid"),
BenchmarkId::new(name, "jsonschema/is_valid/valid"),
instance,
|b, instance| {
b.iter(|| {
@ -101,7 +101,7 @@ fn validate_valid(c: &mut Criterion, name: &str, schema: &Value, instance: &Valu
},
);
c.bench_with_input(
BenchmarkId::new(name, "jsonschema_rs/validate/valid"),
BenchmarkId::new(name, "jsonschema/validate/valid"),
instance,
|b, instance| {
b.iter(|| {
@ -114,7 +114,7 @@ fn validate_valid(c: &mut Criterion, name: &str, schema: &Value, instance: &Valu
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_rs/is_valid/invalid"),
BenchmarkId::new(name, "jsonschema/is_valid/invalid"),
instance,
|b, instance| {
b.iter(|| {
@ -123,7 +123,7 @@ fn validate_invalid(c: &mut Criterion, name: &str, schema: &Value, instance: &Va
},
);
c.bench_with_input(
BenchmarkId::new(name, "jsonschema_rs/validate/invalid"),
BenchmarkId::new(name, "jsonschema/validate/invalid"),
instance,
|b, instance| {
b.iter(|| {
@ -136,5 +136,32 @@ fn validate_invalid(c: &mut Criterion, name: &str, schema: &Value, instance: &Va
);
}
criterion_group!(arbitrary, large_schemas, fast_schema, keywords);
criterion_main!(arbitrary);
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);

View File

@ -1,5 +1,5 @@
use bench_helpers::{bench_citm, bench_fast, bench_geojson, bench_keywords};
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
use codspeed_criterion_compat::{criterion_group, criterion_main, BenchmarkId, Criterion};
use jsonschema_valid::schemas;
use serde_json::Value;

View File

@ -1,5 +1,5 @@
use bench_helpers::{bench_citm, bench_fast, bench_geojson, bench_keywords, bench_swagger};
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
use codspeed_criterion_compat::{criterion_group, criterion_main, BenchmarkId, Criterion};
use serde_json::Value;
use valico::json_schema;

2
jsonschema/rustfmt.toml Normal file
View File

@ -0,0 +1,2 @@
imports_granularity = "Crate"
edition = "2021"

View File

@ -1,7 +1,7 @@
use super::options::CompilationOptions;
use crate::{
compilation::DEFAULT_SCOPE,
paths::{InstancePath, JSONPointer, PathChunk},
paths::{JSONPointer, JsonPointerNode, PathChunkRef},
resolver::Resolver,
schemas,
};
@ -17,7 +17,7 @@ pub(crate) struct CompilationContext<'a> {
base_uri: BaseUri<'a>,
pub(crate) config: Arc<CompilationOptions>,
pub(crate) resolver: Arc<Resolver>,
pub(crate) schema_path: InstancePath<'a>,
pub(crate) schema_path: JsonPointerNode<'a, 'a>,
}
#[derive(Debug, Clone)]
@ -84,7 +84,7 @@ impl<'a> CompilationContext<'a> {
base_uri: scope,
config,
resolver,
schema_path: InstancePath::new(),
schema_path: JsonPointerNode::new(),
}
}
@ -118,7 +118,7 @@ impl<'a> CompilationContext<'a> {
}
#[inline]
pub(crate) fn with_path(&'a self, chunk: impl Into<PathChunk>) -> Self {
pub(crate) fn with_path(&'a self, chunk: impl Into<PathChunkRef<'a>>) -> Self {
let schema_path = self.schema_path.push(chunk);
CompilationContext {
base_uri: self.base_uri.clone(),
@ -136,7 +136,7 @@ impl<'a> CompilationContext<'a> {
/// Create a JSON Pointer from the current `schema_path` & a new chunk.
#[inline]
pub(crate) fn as_pointer_with(&self, chunk: impl Into<PathChunk>) -> JSONPointer {
pub(crate) fn as_pointer_with(&'a self, chunk: impl Into<PathChunkRef<'a>>) -> JSONPointer {
self.schema_path.push(chunk).into()
}

View File

@ -6,9 +6,9 @@ pub(crate) mod options;
use crate::{
error::ErrorIterator,
keywords,
keywords::{self, custom::CustomKeyword, BoxedValidator},
output::Output,
paths::{InstancePath, JSONPointer},
paths::{JSONPointer, JsonPointerNode},
primitive_type::{PrimitiveType, PrimitiveTypesBitMap},
schema_node::SchemaNode,
validator::Validate,
@ -32,11 +32,11 @@ pub struct JSONSchema {
}
pub(crate) static DEFAULT_SCOPE: Lazy<Url> =
Lazy::new(|| url::Url::parse(DEFAULT_ROOT_URL).expect("Is a valid URL"));
Lazy::new(|| Url::parse(DEFAULT_ROOT_URL).expect("Is a valid URL"));
impl JSONSchema {
/// Return a default `CompilationOptions` that can configure
/// `JSONSchema` compilaton flow.
/// `JSONSchema` compilation flow.
///
/// Using options you will be able to configure the draft version
/// to use during `JSONSchema` compilation
@ -67,7 +67,7 @@ impl JSONSchema {
&'instance self,
instance: &'instance Value,
) -> Result<(), ErrorIterator<'instance>> {
let instance_path = InstancePath::new();
let instance_path = JsonPointerNode::new();
let mut errors = self.node.validate(instance, &instance_path).peekable();
if errors.peek().is_none() {
Ok(())
@ -198,7 +198,13 @@ pub(crate) fn compile_validators<'a>(
{
is_props = true;
}
if let Some(validator) = context
// Check if this keyword is overridden, then check the standard definitions
if let Some(factory) = context.config.get_keyword_factory(keyword) {
let path = context.as_pointer_with(keyword.as_str());
let validator = CustomKeyword::new(factory.init(object, subschema, path)?);
let validator: BoxedValidator = Box::new(validator);
validators.push((keyword.clone(), validator));
} else if let Some(validator) = context
.config
.draft()
.get_validator(keyword)
@ -244,8 +250,17 @@ pub(crate) fn compile_validators<'a>(
#[cfg(test)]
mod tests {
use super::JSONSchema;
use crate::error::ValidationError;
use serde_json::{from_str, json, Value};
use crate::{
error::{self, no_error, ValidationError},
keywords::custom::Keyword,
paths::{JSONPointer, JsonPointerNode},
primitive_type::PrimitiveType,
ErrorIterator,
};
use num_cmp::NumCmp;
use once_cell::sync::Lazy;
use regex::Regex;
use serde_json::{from_str, json, Map, Value};
use std::{fs::File, io::Read, path::Path};
fn load(path: &str, idx: usize) -> Value {
@ -302,4 +317,240 @@ mod tests {
);
assert_eq!(errors[1].to_string(), r#""a" is shorter than 3 characters"#);
}
#[test]
fn custom_keyword_definition() {
/// Define a custom validator that verifies the object's keys consist of
/// only ASCII representable characters.
/// NOTE: This could be done with `propertyNames` + `pattern` but will be slower due to
/// regex usage.
struct CustomObjectValidator;
impl Keyword for CustomObjectValidator {
fn validate<'instance>(
&self,
instance: &'instance Value,
instance_path: &JsonPointerNode,
) -> ErrorIterator<'instance> {
let mut errors = vec![];
for key in instance.as_object().unwrap().keys() {
if !key.is_ascii() {
let error = ValidationError::custom(
JSONPointer::default(),
instance_path.into(),
instance,
"Key is not ASCII",
);
errors.push(error);
}
}
Box::new(errors.into_iter())
}
fn is_valid(&self, instance: &Value) -> bool {
for (key, _value) in instance.as_object().unwrap() {
if !key.is_ascii() {
return false;
}
}
true
}
}
fn custom_object_type_factory<'a>(
_: &'a Map<String, Value>,
schema: &'a Value,
path: JSONPointer,
) -> Result<Box<dyn Keyword>, ValidationError<'a>> {
const EXPECTED: &str = "ascii-keys";
if schema.as_str().map_or(true, |key| key != EXPECTED) {
Err(ValidationError::constant_string(
JSONPointer::default(),
path,
schema,
EXPECTED,
))
} else {
Ok(Box::new(CustomObjectValidator))
}
}
// Define a JSON schema that enforces the top level object has ASCII keys and has at least 1 property
let schema =
json!({ "custom-object-type": "ascii-keys", "type": "object", "minProperties": 1 });
let compiled = JSONSchema::options()
.with_keyword("custom-object-type", custom_object_type_factory)
.compile(&schema)
.unwrap();
// Verify schema validation detects object with too few properties
let instance = json!({});
assert!(compiled.validate(&instance).is_err());
assert!(!compiled.is_valid(&instance));
// Verify validator succeeds on a valid custom-object-type
let instance = json!({ "a" : 1 });
assert!(compiled.validate(&instance).is_ok());
assert!(compiled.is_valid(&instance));
// Verify validator detects invalid custom-object-type
let instance = json!({ "å" : 1 });
let error = compiled
.validate(&instance)
.expect_err("Should fail")
.next()
.expect("Not empty");
assert_eq!(error.to_string(), "Key is not ASCII");
assert!(!compiled.is_valid(&instance));
}
#[test]
fn custom_format_and_override_keyword() {
/// Check that a string has some number of digits followed by a dot followed by exactly 2 digits.
fn currency_format_checker(s: &str) -> bool {
static CURRENCY_RE: Lazy<Regex> = Lazy::new(|| {
Regex::new("^(0|([1-9]+[0-9]*))(\\.[0-9]{2})$").expect("Invalid regex")
});
CURRENCY_RE.is_match(s)
}
/// A custom keyword validator that overrides "minimum"
/// so that "minimum" may apply to "currency"-formatted strings as well.
struct CustomMinimumValidator {
limit: f64,
limit_val: Value,
with_currency_format: bool,
schema_path: JSONPointer,
}
impl Keyword for CustomMinimumValidator {
fn validate<'instance>(
&self,
instance: &'instance Value,
instance_path: &JsonPointerNode,
) -> ErrorIterator<'instance> {
if self.is_valid(instance) {
no_error()
} else {
error::error(ValidationError::minimum(
self.schema_path.clone(),
instance_path.into(),
instance,
self.limit_val.clone(),
))
}
}
fn is_valid(&self, instance: &Value) -> bool {
match instance {
// Numeric comparison should happen just like original behavior
Value::Number(instance) => {
if let Some(item) = instance.as_u64() {
!NumCmp::num_lt(item, self.limit)
} else if let Some(item) = instance.as_i64() {
!NumCmp::num_lt(item, self.limit)
} else {
let item = instance.as_f64().expect("Always valid");
!NumCmp::num_lt(item, self.limit)
}
}
// String comparison should cast currency-formatted
Value::String(instance) => {
if self.with_currency_format && currency_format_checker(instance) {
// all preconditions for minimum applying are met
let value = instance
.parse::<f64>()
.expect("format validated by regex checker");
!NumCmp::num_lt(value, self.limit)
} else {
true
}
}
// In all other cases, the "minimum" keyword should not apply
_ => true,
}
}
}
/// Build a validator that overrides the standard `minimum` keyword
fn custom_minimum_factory<'a>(
parent: &'a Map<String, Value>,
schema: &'a Value,
schema_path: JSONPointer,
) -> Result<Box<dyn Keyword>, ValidationError<'a>> {
let limit = if let Value::Number(limit) = schema {
limit.as_f64().expect("Always valid")
} else {
return Err(ValidationError::single_type_error(
// There is no metaschema definition for a custom keyword, hence empty `schema` pointer
JSONPointer::default(),
schema_path,
schema,
PrimitiveType::Number,
));
};
let with_currency_format = parent
.get("format")
.map_or(false, |format| format == "currency");
Ok(Box::new(CustomMinimumValidator {
limit,
limit_val: schema.clone(),
with_currency_format,
schema_path,
}))
}
// Schema includes both the custom format and the overridden keyword
let schema = json!({ "minimum": 2, "type": "string", "format": "currency" });
let compiled = JSONSchema::options()
.with_format("currency", currency_format_checker)
.with_keyword("minimum", custom_minimum_factory)
.with_keyword("minimum-2", |_, _, _| todo!())
.compile(&schema)
.expect("Invalid schema");
// Control: verify schema validation rejects non-string types
let instance = json!(15);
assert!(compiled.validate(&instance).is_err());
assert!(!compiled.is_valid(&instance));
// Control: verify validator rejects ill-formatted strings
let instance = json!("not a currency");
assert!(compiled.validate(&instance).is_err());
assert!(!compiled.is_valid(&instance));
// Verify validator allows properly formatted strings that conform to custom keyword
let instance = json!("3.00");
assert!(compiled.validate(&instance).is_ok());
assert!(compiled.is_valid(&instance));
// Verify validator rejects properly formatted strings that do not conform to custom keyword
let instance = json!("1.99");
assert!(compiled.validate(&instance).is_err());
assert!(!compiled.is_valid(&instance));
// Define another schema that applies "minimum" to an integer to ensure original behavior
let schema = json!({ "minimum": 2, "type": "integer" });
let compiled = JSONSchema::options()
.with_format("currency", currency_format_checker)
.with_keyword("minimum", custom_minimum_factory)
.compile(&schema)
.expect("Invalid schema");
// Verify schema allows integers greater than 2
let instance = json!(3);
assert!(compiled.validate(&instance).is_ok());
assert!(compiled.is_valid(&instance));
// Verify schema rejects integers less than 2
let instance = json!(1);
assert!(compiled.validate(&instance).is_err());
assert!(!compiled.is_valid(&instance));
// Invalid `minimum` value
let schema = json!({ "minimum": "foo" });
let error = JSONSchema::options()
.with_keyword("minimum", custom_minimum_factory)
.compile(&schema)
.expect_err("Should fail");
assert_eq!(error.to_string(), "\"foo\" is not of type \"number\"");
}
}

View File

@ -5,8 +5,10 @@ use crate::{
DEFAULT_CONTENT_ENCODING_CHECKS_AND_CONVERTERS,
},
content_media_type::{ContentMediaTypeCheckType, DEFAULT_CONTENT_MEDIA_TYPE_CHECKS},
keywords::{custom::KeywordFactory, format::Format},
paths::JSONPointer,
resolver::{DefaultResolver, Resolver, SchemaResolver},
schemas, ValidationError,
schemas, Keyword, ValidationError,
};
use ahash::AHashMap;
use once_cell::sync::Lazy;
@ -271,10 +273,11 @@ pub struct CompilationOptions {
content_encoding_checks_and_converters:
AHashMap<&'static str, Option<(ContentEncodingCheckType, ContentEncodingConverterType)>>,
store: AHashMap<String, Arc<serde_json::Value>>,
formats: AHashMap<&'static str, fn(&str) -> bool>,
formats: AHashMap<String, Arc<dyn Format>>,
validate_formats: Option<bool>,
validate_schema: bool,
ignore_unknown_formats: bool,
keywords: AHashMap<String, Arc<dyn KeywordFactory>>,
}
impl Default for CompilationOptions {
@ -289,6 +292,7 @@ impl Default for CompilationOptions {
formats: AHashMap::default(),
validate_formats: None,
ignore_unknown_formats: true,
keywords: AHashMap::default(),
}
}
}
@ -558,7 +562,7 @@ impl CompilationOptions {
/// The example above is taken from the Swagger 2.0 JSON schema.
#[inline]
pub fn with_meta_schemas(&mut self) -> &mut Self {
self.store.extend(META_SCHEMAS.clone().into_iter());
self.store.extend(META_SCHEMAS.clone());
self
}
@ -594,11 +598,15 @@ impl CompilationOptions {
/// ```
///
/// The format check function should receive `&str` and return `bool`.
pub fn with_format(&mut self, name: &'static str, format: fn(&str) -> bool) -> &mut Self {
self.formats.insert(name, format);
pub fn with_format<N, F>(&mut self, name: N, format: F) -> &mut Self
where
N: Into<String>,
F: Fn(&str) -> bool + Send + Sync + 'static,
{
self.formats.insert(name.into(), Arc::new(format));
self
}
pub(crate) fn format(&self, format: &str) -> FormatKV<'_> {
pub(crate) fn get_format(&self, format: &str) -> Option<(&String, &Arc<dyn Format>)> {
self.formats.get_key_value(format)
}
/// Do not perform schema validation during compilation.
@ -637,9 +645,82 @@ impl CompilationOptions {
pub(crate) const fn are_unknown_formats_ignored(&self) -> bool {
self.ignore_unknown_formats
}
/// Register a custom keyword definition.
///
/// ## Example
///
/// ```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())
/// }
/// }
/// 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: &'a Map<String, Value>,
/// value: &'a Value,
/// path: JSONPointer,
/// ) -> Result<Box<dyn Keyword>, ValidationError<'a>> {
/// Ok(Box::new(MyCustomValidator))
/// }
///
/// assert!(JSONSchema::options()
/// .with_keyword("my-type", custom_validator_factory)
/// .with_keyword("my-type-with-closure", |_, _, _| Ok(Box::new(MyCustomValidator)))
/// .compile(&json!({ "my-type": "my-schema"}))
/// .expect("A valid schema")
/// .is_valid(&json!({ "a": "b"})));
/// ```
pub fn with_keyword<N, F>(&mut self, name: N, factory: F) -> &mut Self
where
N: Into<String>,
F: for<'a> Fn(
&'a serde_json::Map<String, serde_json::Value>,
&'a serde_json::Value,
JSONPointer,
) -> Result<Box<dyn Keyword>, ValidationError<'a>>
+ Send
+ Sync
+ 'static,
{
self.keywords.insert(name.into(), Arc::new(factory));
self
}
pub(crate) fn get_keyword_factory(&self, name: &str) -> Option<&Arc<dyn KeywordFactory>> {
self.keywords.get(name)
}
}
// format name & a pointer to a check function
type FormatKV<'a> = Option<(&'a &'static str, &'a fn(&str) -> bool)>;
impl fmt::Debug for CompilationOptions {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {

View File

@ -7,8 +7,8 @@ use crate::{
use serde_json::{Map, Number, Value};
use std::{
borrow::Cow,
error, fmt,
fmt::Formatter,
error,
fmt::{self, Formatter},
io,
iter::{empty, once},
str::Utf8Error,
@ -80,6 +80,8 @@ pub enum ValidationErrorKind {
ContentEncoding { content_encoding: String },
/// The input value does not respect the defined contentMediaType
ContentMediaType { content_media_type: String },
/// Custom error message for user-defined validation.
Custom { message: String },
/// The input value doesn't match any of specified options.
Enum { options: Value },
/// Value is too large.
@ -91,7 +93,7 @@ pub enum ValidationErrorKind {
/// If the referenced file is not found during ref resolution.
FileNotFound { error: io::Error },
/// When the input doesn't match to the specified format.
Format { format: &'static str },
Format { format: String },
/// May happen in `contentEncoding` validation if `base64` encoded data is invalid.
FromUtf8 { error: FromUtf8Error },
/// Invalid UTF-8 string during percent encoding when resolving happens
@ -414,16 +416,18 @@ impl<'a> ValidationError<'a> {
schema_path: JSONPointer::default(),
}
}
pub(crate) const fn format(
pub(crate) fn format(
schema_path: JSONPointer,
instance_path: JSONPointer,
instance: &'a Value,
format: &'static str,
format: impl Into<String>,
) -> ValidationError<'a> {
ValidationError {
instance_path,
instance: Cow::Borrowed(instance),
kind: ValidationErrorKind::Format { format },
kind: ValidationErrorKind::Format {
format: format.into(),
},
schema_path,
}
}
@ -735,6 +739,22 @@ impl<'a> ValidationError<'a> {
schema_path: JSONPointer::default(),
}
}
/// Create a new custom validation error.
pub fn custom(
schema_path: JSONPointer,
instance_path: JSONPointer,
instance: &'a Value,
message: impl Into<String>,
) -> ValidationError<'a> {
ValidationError {
instance_path,
instance: Cow::Borrowed(instance),
kind: ValidationErrorKind::Custom {
message: message.into(),
},
schema_path,
}
}
}
impl error::Error for ValidationError<'_> {}
@ -994,6 +1014,7 @@ impl fmt::Display for ValidationError<'_> {
.collect::<Vec<String>>()
.join(", ")
),
ValidationErrorKind::Custom { message } => f.write_str(message),
}
}
}

View File

@ -2,7 +2,7 @@ use crate::{
compilation::{compile_validators, context::CompilationContext},
error::{error, no_error, ErrorIterator, ValidationError},
keywords::{boolean::FalseValidator, CompilationResult},
paths::{InstancePath, JSONPointer},
paths::{JSONPointer, JsonPointerNode},
primitive_type::{PrimitiveType, PrimitiveTypesBitMap},
schema_node::SchemaNode,
validator::{format_validators, Validate},
@ -43,7 +43,7 @@ impl Validate for AdditionalItemsObjectValidator {
fn validate<'instance>(
&self,
instance: &'instance Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> ErrorIterator<'instance> {
if let Value::Array(items) = instance {
let errors: Vec<_> = items
@ -98,7 +98,7 @@ impl Validate for AdditionalItemsBooleanValidator {
fn validate<'instance>(
&self,
instance: &'instance Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> ErrorIterator<'instance> {
if let Value::Array(items) = instance {
if items.len() > self.items_count {

View File

@ -11,7 +11,7 @@ use crate::{
error::{error, no_error, ErrorIterator, ValidationError},
keywords::CompilationResult,
output::{Annotations, BasicOutput, OutputUnit},
paths::{AbsolutePath, InstancePath, JSONPointer},
paths::{AbsolutePath, JSONPointer, JsonPointerNode},
properties::*,
schema_node::SchemaNode,
validator::{format_validators, PartialApplication, Validate},
@ -55,7 +55,7 @@ macro_rules! is_valid_patterns {
macro_rules! validate {
($node:expr, $value:ident, $instance_path:expr, $property_name:expr) => {{
let instance_path = $instance_path.push($property_name.clone());
let instance_path = $instance_path.push($property_name.as_str());
$node.validate($value, &instance_path)
}};
}
@ -103,7 +103,7 @@ impl Validate for AdditionalPropertiesValidator {
fn validate<'instance>(
&self,
instance: &'instance Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> ErrorIterator<'instance> {
if let Value::Object(item) = instance {
let errors: Vec<_> = item
@ -119,18 +119,18 @@ impl Validate for AdditionalPropertiesValidator {
fn apply<'a>(
&'a self,
instance: &Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> PartialApplication<'a> {
if let Value::Object(item) = instance {
let mut matched_props = Vec::with_capacity(item.len());
let mut output = BasicOutput::default();
for (name, value) in item.iter() {
let path = instance_path.push(name.to_string());
for (name, value) in item {
let path = instance_path.push(name.as_str());
output += self.node.apply_rooted(value, &path);
matched_props.push(name.clone());
}
let mut result: PartialApplication = output.into();
result.annotate(serde_json::Value::from(matched_props).into());
result.annotate(Value::from(matched_props).into());
result
} else {
PartialApplication::valid_empty()
@ -182,7 +182,7 @@ impl Validate for AdditionalPropertiesFalseValidator {
fn validate<'instance>(
&self,
instance: &'instance Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> ErrorIterator<'instance> {
if let Value::Object(item) = instance {
if let Some((_, value)) = item.iter().next() {
@ -260,7 +260,7 @@ impl<M: PropertiesValidatorsMap> Validate for AdditionalPropertiesNotEmptyFalseV
fn validate<'instance>(
&self,
instance: &'instance Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> ErrorIterator<'instance> {
if let Value::Object(item) = instance {
let mut errors = vec![];
@ -291,14 +291,14 @@ impl<M: PropertiesValidatorsMap> Validate for AdditionalPropertiesNotEmptyFalseV
fn apply<'a>(
&'a self,
instance: &Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> PartialApplication<'a> {
if let Value::Object(item) = instance {
let mut unexpected = Vec::with_capacity(item.len());
let mut output = BasicOutput::default();
for (property, value) in item {
if let Some((_name, node)) = self.properties.get_key_validator(property) {
let path = instance_path.push(property.clone());
let path = instance_path.push(property.as_str());
output += node.apply_rooted(value, &path);
} else {
unexpected.push(property.clone())
@ -396,7 +396,7 @@ impl<M: PropertiesValidatorsMap> Validate for AdditionalPropertiesNotEmptyValida
fn validate<'instance>(
&self,
instance: &'instance Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> ErrorIterator<'instance> {
if let Value::Object(map) = instance {
let mut errors = vec![];
@ -418,13 +418,13 @@ impl<M: PropertiesValidatorsMap> Validate for AdditionalPropertiesNotEmptyValida
fn apply<'a>(
&'a self,
instance: &Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> PartialApplication<'a> {
if let Value::Object(map) = instance {
let mut matched_propnames = Vec::with_capacity(map.len());
let mut output = BasicOutput::default();
for (property, value) in map {
let path = instance_path.push(property.clone());
let path = instance_path.push(property.as_str());
if let Some((_name, property_validators)) =
self.properties.get_key_validator(property)
{
@ -436,7 +436,7 @@ impl<M: PropertiesValidatorsMap> Validate for AdditionalPropertiesNotEmptyValida
}
let mut result: PartialApplication = output.into();
if !matched_propnames.is_empty() {
result.annotate(serde_json::Value::from(matched_propnames).into());
result.annotate(Value::from(matched_propnames).into());
}
result
} else {
@ -507,7 +507,7 @@ impl AdditionalPropertiesWithPatternsValidator {
impl Validate for AdditionalPropertiesWithPatternsValidator {
fn is_valid(&self, instance: &Value) -> bool {
if let Value::Object(item) = instance {
for (property, value) in item.iter() {
for (property, value) in item {
let mut has_match = false;
for (re, node) in &self.patterns {
if re.is_match(property).unwrap_or(false) {
@ -526,11 +526,11 @@ impl Validate for AdditionalPropertiesWithPatternsValidator {
fn validate<'instance>(
&self,
instance: &'instance Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> ErrorIterator<'instance> {
if let Value::Object(item) = instance {
let mut errors = vec![];
for (property, value) in item.iter() {
for (property, value) in item {
let mut has_match = false;
errors.extend(
self.patterns
@ -554,14 +554,14 @@ impl Validate for AdditionalPropertiesWithPatternsValidator {
fn apply<'a>(
&'a self,
instance: &Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> PartialApplication<'a> {
if let Value::Object(item) = instance {
let mut output = BasicOutput::default();
let mut pattern_matched_propnames = Vec::with_capacity(item.len());
let mut additional_matched_propnames = Vec::with_capacity(item.len());
for (property, value) in item {
let path = instance_path.push(property.clone());
let path = instance_path.push(property.as_str());
let mut has_match = false;
for (pattern, node) in &self.patterns {
if pattern.is_match(property).unwrap_or(false) {
@ -580,13 +580,13 @@ impl Validate for AdditionalPropertiesWithPatternsValidator {
self.pattern_keyword_path.clone(),
instance_path.into(),
self.pattern_keyword_absolute_path.clone(),
serde_json::Value::from(pattern_matched_propnames).into(),
Value::from(pattern_matched_propnames).into(),
)
.into();
}
let mut result: PartialApplication = output.into();
if !additional_matched_propnames.is_empty() {
result.annotate(serde_json::Value::from(additional_matched_propnames).into())
result.annotate(Value::from(additional_matched_propnames).into())
}
result
} else {
@ -663,7 +663,7 @@ impl Validate for AdditionalPropertiesWithPatternsFalseValidator {
fn validate<'instance>(
&self,
instance: &'instance Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> ErrorIterator<'instance> {
if let Value::Object(item) = instance {
let mut errors = vec![];
@ -700,14 +700,14 @@ impl Validate for AdditionalPropertiesWithPatternsFalseValidator {
fn apply<'a>(
&'a self,
instance: &Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> PartialApplication<'a> {
if let Value::Object(item) = instance {
let mut output = BasicOutput::default();
let mut unexpected = Vec::with_capacity(item.len());
let mut pattern_matched_props = Vec::with_capacity(item.len());
for (property, value) in item {
let path = instance_path.push(property.clone());
let path = instance_path.push(property.as_str());
let mut has_match = false;
for (pattern, node) in &self.patterns {
if pattern.is_match(property).unwrap_or(false) {
@ -725,7 +725,7 @@ impl Validate for AdditionalPropertiesWithPatternsFalseValidator {
self.pattern_keyword_path.clone(),
instance_path.into(),
self.pattern_keyword_absolute_path.clone(),
serde_json::Value::from(pattern_matched_props).into(),
Value::from(pattern_matched_props).into(),
)
.into();
}
@ -824,7 +824,7 @@ impl AdditionalPropertiesWithPatternsNotEmptyValidator<BigValidatorsMap> {
impl<M: PropertiesValidatorsMap> Validate for AdditionalPropertiesWithPatternsNotEmptyValidator<M> {
fn is_valid(&self, instance: &Value) -> bool {
if let Value::Object(item) = instance {
for (property, value) in item.iter() {
for (property, value) in item {
if let Some(node) = self.properties.get_validator(property) {
if is_valid!(node, value) {
// Valid for `properties`, check `patternProperties`
@ -861,11 +861,11 @@ impl<M: PropertiesValidatorsMap> Validate for AdditionalPropertiesWithPatternsNo
fn validate<'instance>(
&self,
instance: &'instance Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> ErrorIterator<'instance> {
if let Value::Object(item) = instance {
let mut errors = vec![];
for (property, value) in item.iter() {
for (property, value) in item {
if let Some((name, node)) = self.properties.get_key_validator(property) {
errors.extend(validate!(node, value, instance_path, name));
errors.extend(
@ -899,13 +899,13 @@ impl<M: PropertiesValidatorsMap> Validate for AdditionalPropertiesWithPatternsNo
fn apply<'a>(
&'a self,
instance: &Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> PartialApplication<'a> {
if let Value::Object(item) = instance {
let mut output = BasicOutput::default();
let mut additional_matches = Vec::with_capacity(item.len());
for (property, value) in item.iter() {
let path = instance_path.push(property.clone());
for (property, value) in item {
let path = instance_path.push(property.as_str());
if let Some((_name, node)) = self.properties.get_key_validator(property) {
output += node.apply_rooted(value, &path);
for (pattern, node) in &self.patterns {
@ -928,7 +928,7 @@ impl<M: PropertiesValidatorsMap> Validate for AdditionalPropertiesWithPatternsNo
}
}
let mut result: PartialApplication = output.into();
result.annotate(serde_json::Value::from(additional_matches).into());
result.annotate(Value::from(additional_matches).into());
result
} else {
PartialApplication::valid_empty()
@ -1017,7 +1017,7 @@ impl<M: PropertiesValidatorsMap> Validate
fn is_valid(&self, instance: &Value) -> bool {
if let Value::Object(item) = instance {
// No properties are allowed, except ones defined in `properties` or `patternProperties`
for (property, value) in item.iter() {
for (property, value) in item {
if let Some(node) = self.properties.get_validator(property) {
if is_valid!(node, value) {
// Valid for `properties`, check `patternProperties`
@ -1042,13 +1042,13 @@ impl<M: PropertiesValidatorsMap> Validate
fn validate<'instance>(
&self,
instance: &'instance Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> ErrorIterator<'instance> {
if let Value::Object(item) = instance {
let mut errors = vec![];
let mut unexpected = vec![];
// No properties are allowed, except ones defined in `properties` or `patternProperties`
for (property, value) in item.iter() {
for (property, value) in item {
if let Some((name, node)) = self.properties.get_key_validator(property) {
errors.extend(validate!(node, value, instance_path, name));
errors.extend(
@ -1090,14 +1090,14 @@ impl<M: PropertiesValidatorsMap> Validate
fn apply<'a>(
&'a self,
instance: &Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> PartialApplication<'a> {
if let Value::Object(item) = instance {
let mut output = BasicOutput::default();
let mut unexpected = vec![];
// No properties are allowed, except ones defined in `properties` or `patternProperties`
for (property, value) in item.iter() {
let path = instance_path.push(property.clone());
for (property, value) in item {
let path = instance_path.push(property.as_str());
if let Some((_name, node)) = self.properties.get_key_validator(property) {
output += node.apply_rooted(value, &path);
for (pattern, node) in &self.patterns {

View File

@ -2,7 +2,7 @@ use crate::{
compilation::{compile_validators, context::CompilationContext},
error::{ErrorIterator, ValidationError},
output::BasicOutput,
paths::{InstancePath, JSONPointer},
paths::{JSONPointer, JsonPointerNode},
primitive_type::PrimitiveType,
schema_node::SchemaNode,
validator::{format_iter_of_validators, format_validators, PartialApplication, Validate},
@ -41,7 +41,7 @@ impl Validate for AllOfValidator {
fn validate<'instance>(
&self,
instance: &'instance Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> ErrorIterator<'instance> {
let errors: Vec<_> = self
.schemas
@ -54,7 +54,7 @@ impl Validate for AllOfValidator {
fn apply<'a>(
&'a self,
instance: &Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> PartialApplication<'a> {
self.schemas
.iter()
@ -98,7 +98,7 @@ impl Validate for SingleValueAllOfValidator {
fn validate<'instance>(
&self,
instance: &'instance Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> ErrorIterator<'instance> {
self.node.validate(instance, instance_path)
}
@ -106,7 +106,7 @@ impl Validate for SingleValueAllOfValidator {
fn apply<'a>(
&'a self,
instance: &Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> PartialApplication<'a> {
self.node.apply_rooted(instance, instance_path).into()
}

View File

@ -1,7 +1,7 @@
use crate::{
compilation::{compile_validators, context::CompilationContext},
error::{error, no_error, ErrorIterator, ValidationError},
paths::InstancePath,
paths::JsonPointerNode,
primitive_type::PrimitiveType,
schema_node::SchemaNode,
validator::{format_iter_of_validators, PartialApplication, Validate},
@ -53,7 +53,7 @@ impl Validate for AnyOfValidator {
fn validate<'instance>(
&self,
instance: &'instance Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> ErrorIterator<'instance> {
if self.is_valid(instance) {
no_error()
@ -69,7 +69,7 @@ impl Validate for AnyOfValidator {
fn apply<'a>(
&'a self,
instance: &Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> PartialApplication<'a> {
let mut successes = Vec::new();
let mut failures = Vec::new();

View File

@ -1,4 +1,4 @@
use crate::paths::{InstancePath, JSONPointer};
use crate::paths::{JSONPointer, JsonPointerNode};
use crate::{
error::{error, ErrorIterator, ValidationError},
@ -24,7 +24,7 @@ impl Validate for FalseValidator {
fn validate<'instance>(
&self,
instance: &'instance Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> ErrorIterator<'instance> {
error(ValidationError::false_schema(
self.schema_path.clone(),

View File

@ -7,7 +7,7 @@ use crate::{
use serde_json::{Map, Number, Value};
use std::f64::EPSILON;
use crate::paths::{InstancePath, JSONPointer};
use crate::paths::{JSONPointer, JsonPointerNode};
struct ConstArrayValidator {
value: Vec<Value>,
@ -27,7 +27,7 @@ impl Validate for ConstArrayValidator {
fn validate<'instance>(
&self,
instance: &'instance Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> ErrorIterator<'instance> {
if self.is_valid(instance) {
no_error()
@ -79,7 +79,7 @@ impl Validate for ConstBooleanValidator {
fn validate<'instance>(
&self,
instance: &'instance Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> ErrorIterator<'instance> {
if self.is_valid(instance) {
no_error()
@ -122,7 +122,7 @@ impl Validate for ConstNullValidator {
fn validate<'instance>(
&self,
instance: &'instance Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> ErrorIterator<'instance> {
if self.is_valid(instance) {
no_error()
@ -170,7 +170,7 @@ impl Validate for ConstNumberValidator {
fn validate<'instance>(
&self,
instance: &'instance Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> ErrorIterator<'instance> {
if self.is_valid(instance) {
no_error()
@ -221,7 +221,7 @@ impl Validate for ConstObjectValidator {
fn validate<'instance>(
&self,
instance: &'instance Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> ErrorIterator<'instance> {
if self.is_valid(instance) {
no_error()
@ -277,7 +277,7 @@ impl Validate for ConstStringValidator {
fn validate<'instance>(
&self,
instance: &'instance Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> ErrorIterator<'instance> {
if self.is_valid(instance) {
no_error()

View File

@ -2,7 +2,7 @@ use crate::{
compilation::{compile_validators, context::CompilationContext},
error::{error, no_error, ErrorIterator, ValidationError},
keywords::CompilationResult,
paths::{InstancePath, JSONPointer},
paths::{JSONPointer, JsonPointerNode},
schema_node::SchemaNode,
validator::{format_validators, PartialApplication, Validate},
Draft,
@ -43,7 +43,7 @@ impl Validate for ContainsValidator {
fn validate<'instance>(
&self,
instance: &'instance Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> ErrorIterator<'instance> {
if let Value::Array(items) = instance {
if items.iter().any(|i| self.node.is_valid(i)) {
@ -62,7 +62,7 @@ impl Validate for ContainsValidator {
fn apply<'a>(
&'a self,
instance: &Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> PartialApplication<'a> {
if let Value::Array(items) = instance {
let mut results = Vec::with_capacity(items.len());
@ -86,12 +86,12 @@ impl Validate for ContainsValidator {
.into(),
);
} else {
result.annotate(serde_json::Value::from(indices).into());
result.annotate(Value::from(indices).into());
}
result
} else {
let mut result = PartialApplication::valid_empty();
result.annotate(serde_json::Value::Array(Vec::new()).into());
result.annotate(Value::Array(Vec::new()).into());
result
}
}
@ -132,7 +132,7 @@ impl Validate for MinContainsValidator {
fn validate<'instance>(
&self,
instance: &'instance Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> ErrorIterator<'instance> {
if let Value::Array(items) = instance {
// From docs:
@ -230,7 +230,7 @@ impl Validate for MaxContainsValidator {
fn validate<'instance>(
&self,
instance: &'instance Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> ErrorIterator<'instance> {
if let Value::Array(items) = instance {
// From docs:
@ -338,7 +338,7 @@ impl Validate for MinMaxContainsValidator {
fn validate<'instance>(
&self,
instance: &'instance Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> ErrorIterator<'instance> {
if let Value::Array(items) = instance {
let mut matches = 0;

View File

@ -5,7 +5,7 @@ use crate::{
content_media_type::ContentMediaTypeCheckType,
error::{error, no_error, ErrorIterator, ValidationError},
keywords::CompilationResult,
paths::{InstancePath, JSONPointer},
paths::{JSONPointer, JsonPointerNode},
primitive_type::PrimitiveType,
validator::Validate,
};
@ -46,7 +46,7 @@ impl Validate for ContentMediaTypeValidator {
fn validate<'instance>(
&self,
instance: &'instance Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> ErrorIterator<'instance> {
if let Value::String(item) = instance {
if (self.func)(item) {
@ -105,7 +105,7 @@ impl Validate for ContentEncodingValidator {
fn validate<'instance>(
&self,
instance: &'instance Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> ErrorIterator<'instance> {
if let Value::String(item) = instance {
if (self.func)(item) {
@ -174,7 +174,7 @@ impl Validate for ContentMediaTypeAndEncodingValidator {
fn validate<'instance>(
&self,
instance: &'instance Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> ErrorIterator<'instance> {
if let Value::String(item) = instance {
match (self.converter)(item) {

View File

@ -0,0 +1,84 @@
use crate::{
paths::{JSONPointer, JsonPointerNode},
validator::Validate,
ErrorIterator, ValidationError,
};
use serde_json::{Map, Value};
use std::fmt::{Display, Formatter};
pub(crate) struct CustomKeyword {
inner: Box<dyn Keyword>,
}
impl CustomKeyword {
pub(crate) fn new(inner: Box<dyn Keyword>) -> Self {
Self { inner }
}
}
impl Display for CustomKeyword {
fn fmt(&self, _: &mut Formatter<'_>) -> std::fmt::Result {
Ok(())
}
}
impl Validate for CustomKeyword {
fn validate<'instance>(
&self,
instance: &'instance Value,
instance_path: &JsonPointerNode,
) -> ErrorIterator<'instance> {
self.inner.validate(instance, instance_path)
}
fn is_valid(&self, instance: &Value) -> bool {
self.inner.is_valid(instance)
}
}
/// Trait that allows implementing custom validation for keywords.
pub trait Keyword: Send + Sync {
/// Validate [instance](Value) according to a custom specification
///
/// A custom keyword validator may be used when a validation that cannot be
/// easily or efficiently expressed in JSON schema.
///
/// The custom validation is applied in addition to the JSON schema validation.
fn validate<'instance>(
&self,
instance: &'instance Value,
instance_path: &JsonPointerNode,
) -> ErrorIterator<'instance>;
/// Validate [instance](Value) and return a boolean result.
/// Could be potentilly faster than `validate` method.
fn is_valid(&self, instance: &Value) -> bool;
}
pub(crate) trait KeywordFactory: Send + Sync {
fn init<'a>(
&self,
parent: &'a Map<String, Value>,
schema: &'a Value,
path: JSONPointer,
) -> Result<Box<dyn Keyword>, ValidationError<'a>>;
}
impl<F> KeywordFactory for F
where
F: for<'a> Fn(
&'a Map<String, Value>,
&'a Value,
JSONPointer,
) -> Result<Box<dyn Keyword>, ValidationError<'a>>
+ Send
+ Sync,
{
fn init<'a>(
&self,
parent: &'a Map<String, Value>,
schema: &'a Value,
path: JSONPointer,
) -> Result<Box<dyn Keyword>, ValidationError<'a>> {
self(parent, schema, path)
}
}

View File

@ -2,7 +2,7 @@ use crate::{
compilation::{compile_validators, context::CompilationContext},
error::{no_error, ErrorIterator, ValidationError},
keywords::{required, unique_items, CompilationResult},
paths::{InstancePath, JSONPointer},
paths::{JSONPointer, JsonPointerNode},
primitive_type::PrimitiveType,
schema_node::SchemaNode,
validator::{format_key_value_validators, Validate},
@ -23,7 +23,7 @@ impl DependenciesValidator {
let keyword_context = context.with_path("dependencies");
let mut dependencies = Vec::with_capacity(map.len());
for (key, subschema) in map {
let item_context = keyword_context.with_path(key.to_string());
let item_context = keyword_context.with_path(key.as_str());
let s = match subschema {
Value::Array(_) => {
let validators = vec![required::compile_with_path(
@ -65,7 +65,7 @@ impl Validate for DependenciesValidator {
fn validate<'instance>(
&self,
instance: &'instance Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> ErrorIterator<'instance> {
if let Value::Object(item) = instance {
let errors: Vec<_> = self
@ -106,7 +106,7 @@ impl DependentRequiredValidator {
let keyword_context = context.with_path("dependentRequired");
let mut dependencies = Vec::with_capacity(map.len());
for (key, subschema) in map {
let item_context = keyword_context.with_path(key.to_string());
let item_context = keyword_context.with_path(key.as_str());
if let Value::Array(dependency_array) = subschema {
if !unique_items::is_unique(dependency_array) {
return Err(ValidationError::unique_items(
@ -158,7 +158,7 @@ impl Validate for DependentRequiredValidator {
fn validate<'instance>(
&self,
instance: &'instance Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> ErrorIterator<'instance> {
if let Value::Object(item) = instance {
let errors: Vec<_> = self
@ -196,7 +196,7 @@ impl DependentSchemasValidator {
let keyword_context = context.with_path("dependentSchemas");
let mut dependencies = Vec::with_capacity(map.len());
for (key, subschema) in map {
let item_context = keyword_context.with_path(key.to_string());
let item_context = keyword_context.with_path(key.as_str());
let schema_nodes = compile_validators(subschema, &item_context)?;
dependencies.push((key.clone(), schema_nodes));
}
@ -225,7 +225,7 @@ impl Validate for DependentSchemasValidator {
fn validate<'instance>(
&self,
instance: &'instance Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> ErrorIterator<'instance> {
if let Value::Object(item) = instance {
let errors: Vec<_> = self

View File

@ -2,7 +2,7 @@ use crate::{
compilation::context::CompilationContext,
error::{error, no_error, ErrorIterator, ValidationError},
keywords::{helpers, CompilationResult},
paths::{InstancePath, JSONPointer},
paths::{JSONPointer, JsonPointerNode},
primitive_type::{PrimitiveType, PrimitiveTypesBitMap},
validator::Validate,
};
@ -25,7 +25,7 @@ impl EnumValidator {
schema_path: JSONPointer,
) -> CompilationResult<'a> {
let mut types = PrimitiveTypesBitMap::new();
for item in items.iter() {
for item in items {
types |= PrimitiveType::from(item);
}
Ok(Box::new(EnumValidator {
@ -41,7 +41,7 @@ impl Validate for EnumValidator {
fn validate<'instance>(
&self,
instance: &'instance Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> ErrorIterator<'instance> {
if self.is_valid(instance) {
no_error()
@ -107,7 +107,7 @@ impl Validate for SingleValueEnumValidator {
fn validate<'instance>(
&self,
instance: &'instance Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> ErrorIterator<'instance> {
if self.is_valid(instance) {
no_error()

View File

@ -2,7 +2,7 @@ use crate::{
compilation::context::CompilationContext,
error::{error, no_error, ErrorIterator, ValidationError},
keywords::CompilationResult,
paths::{InstancePath, JSONPointer},
paths::{JSONPointer, JsonPointerNode},
primitive_type::PrimitiveType,
validator::Validate,
};
@ -31,7 +31,7 @@ macro_rules! validate {
fn validate<'instance>(
&self,
instance: &'instance Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> ErrorIterator<'instance> {
if self.is_valid(instance) {
no_error()
@ -90,7 +90,7 @@ impl Validate for ExclusiveMaximumF64Validator {
fn validate<'instance>(
&self,
instance: &'instance Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> ErrorIterator<'instance> {
if self.is_valid(instance) {
no_error()

View File

@ -2,7 +2,7 @@ use crate::{
compilation::context::CompilationContext,
error::{error, no_error, ErrorIterator, ValidationError},
keywords::CompilationResult,
paths::{InstancePath, JSONPointer},
paths::{JSONPointer, JsonPointerNode},
primitive_type::PrimitiveType,
validator::Validate,
};
@ -31,7 +31,7 @@ macro_rules! validate {
fn validate<'instance>(
&self,
instance: &'instance Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> ErrorIterator<'instance> {
if self.is_valid(instance) {
no_error()
@ -88,7 +88,7 @@ impl Validate for ExclusiveMinimumF64Validator {
fn validate<'instance>(
&self,
instance: &'instance Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> ErrorIterator<'instance> {
if self.is_valid(instance) {
no_error()

View File

@ -1,5 +1,5 @@
//! Validator for `format` keyword.
use std::{net::IpAddr, str::FromStr};
use std::{net::IpAddr, str::FromStr, sync::Arc};
use fancy_regex::Regex;
use once_cell::sync::Lazy;
@ -11,7 +11,7 @@ use crate::{
compilation::context::CompilationContext,
error::{error, no_error, ErrorIterator, ValidationError},
keywords::{pattern, CompilationResult},
paths::{InstancePath, JSONPointer},
paths::{JSONPointer, JsonPointerNode},
primitive_type::PrimitiveType,
validator::Validate,
Draft,
@ -65,7 +65,7 @@ macro_rules! validate {
fn validate<'instance>(
&self,
instance: &'instance Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> ErrorIterator<'instance> {
if let Value::String(_item) = instance {
if !self.is_valid(instance) {
@ -369,14 +369,14 @@ impl Validate for DurationValidator {
struct CustomFormatValidator {
schema_path: JSONPointer,
format_name: &'static str,
check: fn(&str) -> bool,
format_name: String,
check: Arc<dyn Format>,
}
impl CustomFormatValidator {
pub(crate) fn compile<'a>(
context: &CompilationContext,
format_name: &'static str,
check: fn(&str) -> bool,
format_name: String,
check: Arc<dyn Format>,
) -> CompilationResult<'a> {
let schema_path = context.as_pointer_with("format");
Ok(Box::new(CustomFormatValidator {
@ -396,30 +396,42 @@ impl Validate for CustomFormatValidator {
fn validate<'instance>(
&self,
instance: &'instance Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> ErrorIterator<'instance> {
if let Value::String(_item) = instance {
if !self.is_valid(instance) {
return error(ValidationError::format(
self.schema_path.clone(),
instance_path.into(),
instance,
self.format_name,
));
}
if !self.is_valid(instance) {
return error(ValidationError::format(
self.schema_path.clone(),
instance_path.into(),
instance,
self.format_name.clone(),
));
}
no_error()
}
fn is_valid(&self, instance: &Value) -> bool {
if let Value::String(item) = instance {
(self.check)(item)
self.check.is_valid(item)
} else {
true
}
}
}
pub(crate) trait Format: Send + Sync + 'static {
fn is_valid(&self, value: &str) -> bool;
}
impl<F> Format for F
where
F: Fn(&str) -> bool + Send + Sync + 'static,
{
#[inline]
fn is_valid(&self, value: &str) -> bool {
self(value)
}
}
#[inline]
pub(crate) fn compile<'a>(
_: &'a Map<String, Value>,
@ -431,8 +443,12 @@ pub(crate) fn compile<'a>(
}
if let Value::String(format) = schema {
if let Some((format, func)) = context.config.format(format) {
return Some(CustomFormatValidator::compile(context, format, *func));
if let Some((name, func)) = context.config.get_format(format) {
return Some(CustomFormatValidator::compile(
context,
name.clone(),
func.clone(),
));
}
let draft_version = context.config.draft();
match format.as_str() {

View File

@ -2,7 +2,7 @@ use crate::{
compilation::{compile_validators, context::CompilationContext},
error::{no_error, ErrorIterator},
keywords::CompilationResult,
paths::InstancePath,
paths::JsonPointerNode,
schema_node::SchemaNode,
validator::{format_validators, PartialApplication, Validate},
};
@ -46,7 +46,7 @@ impl Validate for IfThenValidator {
fn validate<'instance>(
&self,
instance: &'instance Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> ErrorIterator<'instance> {
if self.schema.is_valid(instance) {
let errors: Vec<_> = self.then_schema.validate(instance, instance_path).collect();
@ -59,7 +59,7 @@ impl Validate for IfThenValidator {
fn apply<'a>(
&'a self,
instance: &Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> PartialApplication<'a> {
let mut if_result = self.schema.apply_rooted(instance, instance_path);
if if_result.is_valid() {
@ -121,7 +121,7 @@ impl Validate for IfElseValidator {
fn validate<'instance>(
&self,
instance: &'instance Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> ErrorIterator<'instance> {
if self.schema.is_valid(instance) {
no_error()
@ -134,7 +134,7 @@ impl Validate for IfElseValidator {
fn apply<'a>(
&'a self,
instance: &Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> PartialApplication<'a> {
let if_result = self.schema.apply_rooted(instance, instance_path);
if if_result.is_valid() {
@ -202,7 +202,7 @@ impl Validate for IfThenElseValidator {
fn validate<'instance>(
&self,
instance: &'instance Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> ErrorIterator<'instance> {
if self.schema.is_valid(instance) {
let errors: Vec<_> = self.then_schema.validate(instance, instance_path).collect();
@ -216,7 +216,7 @@ impl Validate for IfThenElseValidator {
fn apply<'a>(
&'a self,
instance: &Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> PartialApplication<'a> {
let mut if_result = self.schema.apply_rooted(instance, instance_path);
if if_result.is_valid() {

View File

@ -2,7 +2,7 @@ use crate::{
compilation::{compile_validators, context::CompilationContext},
error::{no_error, ErrorIterator},
keywords::CompilationResult,
paths::InstancePath,
paths::JsonPointerNode,
schema_node::SchemaNode,
validator::{format_iter_of_validators, format_validators, PartialApplication, Validate},
};
@ -43,7 +43,7 @@ impl Validate for ItemsArrayValidator {
fn validate<'instance>(
&self,
instance: &'instance Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> ErrorIterator<'instance> {
if let Value::Array(items) = instance {
let errors: Vec<_> = items
@ -97,7 +97,7 @@ impl Validate for ItemsObjectValidator {
fn validate<'instance>(
&self,
instance: &'instance Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> ErrorIterator<'instance> {
if let Value::Array(items) = instance {
let errors: Vec<_> = items
@ -114,7 +114,7 @@ impl Validate for ItemsObjectValidator {
fn apply<'a>(
&'a self,
instance: &Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> PartialApplication<'a> {
if let Value::Array(items) = instance {
let mut results = Vec::with_capacity(items.len());
@ -180,7 +180,7 @@ impl Validate for ItemsObjectSkipPrefixValidator {
fn validate<'instance>(
&self,
instance: &'instance Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> ErrorIterator<'instance> {
if let Value::Array(items) = instance {
let errors: Vec<_> = items
@ -201,7 +201,7 @@ impl Validate for ItemsObjectSkipPrefixValidator {
fn apply<'a>(
&'a self,
instance: &Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> PartialApplication<'a> {
if let Value::Array(items) = instance {
let mut results = Vec::with_capacity(items.len().saturating_sub(self.skip_prefix));

View File

@ -2,7 +2,7 @@ use crate::{
compilation::context::CompilationContext,
error::{error, no_error, ErrorIterator, ValidationError},
keywords::{type_, CompilationResult},
paths::{InstancePath, JSONPointer},
paths::{JSONPointer, JsonPointerNode},
primitive_type::{PrimitiveType, PrimitiveTypesBitMap},
validator::Validate,
};
@ -65,7 +65,7 @@ impl Validate for MultipleTypesValidator {
fn validate<'instance>(
&self,
instance: &'instance Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> ErrorIterator<'instance> {
if self.is_valid(instance) {
no_error()
@ -117,7 +117,7 @@ impl Validate for IntegerTypeValidator {
fn validate<'instance>(
&self,
instance: &'instance Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> ErrorIterator<'instance> {
if self.is_valid(instance) {
no_error()

View File

@ -2,7 +2,7 @@ use crate::{
compilation::context::CompilationContext,
error::{error, no_error, ErrorIterator, ValidationError},
keywords::{helpers::fail_on_non_positive_integer, CompilationResult},
paths::{InstancePath, JSONPointer},
paths::{JSONPointer, JsonPointerNode},
validator::Validate,
};
use serde_json::{Map, Value};
@ -36,7 +36,7 @@ impl Validate for MaxItemsValidator {
fn validate<'instance>(
&self,
instance: &'instance Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> ErrorIterator<'instance> {
if let Value::Array(items) = instance {
if (items.len() as u64) > self.limit {

View File

@ -2,7 +2,7 @@ use crate::{
compilation::context::CompilationContext,
error::{error, no_error, ErrorIterator, ValidationError},
keywords::{helpers::fail_on_non_positive_integer, CompilationResult},
paths::{InstancePath, JSONPointer},
paths::{JSONPointer, JsonPointerNode},
validator::Validate,
};
use serde_json::{Map, Value};
@ -36,7 +36,7 @@ impl Validate for MaxLengthValidator {
fn validate<'instance>(
&self,
instance: &'instance Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> ErrorIterator<'instance> {
if let Value::String(item) = instance {
if (bytecount::num_chars(item.as_bytes()) as u64) > self.limit {

View File

@ -2,7 +2,7 @@ use crate::{
compilation::context::CompilationContext,
error::{error, no_error, ErrorIterator, ValidationError},
keywords::{helpers::fail_on_non_positive_integer, CompilationResult},
paths::{InstancePath, JSONPointer},
paths::{JSONPointer, JsonPointerNode},
validator::Validate,
};
use serde_json::{Map, Value};
@ -36,7 +36,7 @@ impl Validate for MaxPropertiesValidator {
fn validate<'instance>(
&self,
instance: &'instance Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> ErrorIterator<'instance> {
if let Value::Object(item) = instance {
if (item.len() as u64) > self.limit {

View File

@ -2,7 +2,7 @@ use crate::{
compilation::context::CompilationContext,
error::{error, no_error, ErrorIterator, ValidationError},
keywords::CompilationResult,
paths::{InstancePath, JSONPointer},
paths::{JSONPointer, JsonPointerNode},
primitive_type::PrimitiveType,
validator::Validate,
};
@ -31,7 +31,7 @@ macro_rules! validate {
fn validate<'instance>(
&self,
instance: &'instance Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> ErrorIterator<'instance> {
if self.is_valid(instance) {
no_error()
@ -88,7 +88,7 @@ impl Validate for MaximumF64Validator {
fn validate<'instance>(
&self,
instance: &'instance Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> ErrorIterator<'instance> {
if self.is_valid(instance) {
no_error()

View File

@ -2,7 +2,7 @@ use crate::{
compilation::context::CompilationContext,
error::{error, no_error, ErrorIterator, ValidationError},
keywords::{helpers::fail_on_non_positive_integer, CompilationResult},
paths::{InstancePath, JSONPointer},
paths::{JSONPointer, JsonPointerNode},
validator::Validate,
};
use serde_json::{Map, Value};
@ -36,7 +36,7 @@ impl Validate for MinItemsValidator {
fn validate<'instance>(
&self,
instance: &'instance Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> ErrorIterator<'instance> {
if let Value::Array(items) = instance {
if (items.len() as u64) < self.limit {

View File

@ -2,7 +2,7 @@ use crate::{
compilation::context::CompilationContext,
error::{error, no_error, ErrorIterator, ValidationError},
keywords::{helpers::fail_on_non_positive_integer, CompilationResult},
paths::{InstancePath, JSONPointer},
paths::{JSONPointer, JsonPointerNode},
validator::Validate,
};
use serde_json::{Map, Value};
@ -36,7 +36,7 @@ impl Validate for MinLengthValidator {
fn validate<'instance>(
&self,
instance: &'instance Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> ErrorIterator<'instance> {
if let Value::String(item) = instance {
if (bytecount::num_chars(item.as_bytes()) as u64) < self.limit {

View File

@ -2,7 +2,7 @@ use crate::{
compilation::context::CompilationContext,
error::{error, no_error, ErrorIterator, ValidationError},
keywords::{helpers::fail_on_non_positive_integer, CompilationResult},
paths::{InstancePath, JSONPointer},
paths::{JSONPointer, JsonPointerNode},
validator::Validate,
};
use serde_json::{Map, Value};
@ -36,7 +36,7 @@ impl Validate for MinPropertiesValidator {
fn validate<'instance>(
&self,
instance: &'instance Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> ErrorIterator<'instance> {
if let Value::Object(item) = instance {
if (item.len() as u64) < self.limit {

View File

@ -2,7 +2,7 @@ use crate::{
compilation::context::CompilationContext,
error::{error, no_error, ErrorIterator, ValidationError},
keywords::CompilationResult,
paths::{InstancePath, JSONPointer},
paths::{JSONPointer, JsonPointerNode},
primitive_type::PrimitiveType,
validator::Validate,
};
@ -31,7 +31,7 @@ macro_rules! validate {
fn validate<'instance>(
&self,
instance: &'instance Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> ErrorIterator<'instance> {
if self.is_valid(instance) {
no_error()
@ -88,7 +88,7 @@ impl Validate for MinimumF64Validator {
fn validate<'instance>(
&self,
instance: &'instance Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> ErrorIterator<'instance> {
if self.is_valid(instance) {
no_error()

View File

@ -6,6 +6,7 @@ pub(crate) mod boolean;
pub(crate) mod const_;
pub(crate) mod contains;
pub(crate) mod content;
pub(crate) mod custom;
pub(crate) mod dependencies;
pub(crate) mod enum_;
pub(crate) mod exclusive_maximum;

View File

@ -2,7 +2,7 @@ use crate::{
compilation::context::CompilationContext,
error::{error, no_error, ErrorIterator, ValidationError},
keywords::CompilationResult,
paths::{InstancePath, JSONPointer},
paths::{JSONPointer, JsonPointerNode},
primitive_type::PrimitiveType,
validator::Validate,
};
@ -48,7 +48,7 @@ impl Validate for MultipleOfFloatValidator {
fn validate<'instance>(
&self,
instance: &'instance Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> ErrorIterator<'instance> {
if !self.is_valid(instance) {
return error(ValidationError::multiple_of(
@ -98,7 +98,7 @@ impl Validate for MultipleOfIntegerValidator {
fn validate<'instance>(
&self,
instance: &'instance Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> ErrorIterator<'instance> {
if !self.is_valid(instance) {
return error(ValidationError::multiple_of(

View File

@ -2,7 +2,7 @@ use crate::{
compilation::{compile_validators, context::CompilationContext},
error::{error, no_error, ErrorIterator, ValidationError},
keywords::CompilationResult,
paths::{InstancePath, JSONPointer},
paths::{JSONPointer, JsonPointerNode},
schema_node::SchemaNode,
validator::{format_validators, Validate},
};
@ -38,7 +38,7 @@ impl Validate for NotValidator {
fn validate<'instance>(
&self,
instance: &'instance Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> ErrorIterator<'instance> {
if self.is_valid(instance) {
no_error()

View File

@ -3,7 +3,7 @@ use crate::{
error::{error, no_error, ErrorIterator, ValidationError},
keywords::CompilationResult,
output::BasicOutput,
paths::{InstancePath, JSONPointer},
paths::{JSONPointer, JsonPointerNode},
primitive_type::PrimitiveType,
schema_node::SchemaNode,
validator::{format_iter_of_validators, PartialApplication, Validate},
@ -54,7 +54,7 @@ impl OneOfValidator {
first_valid_idx
}
#[allow(clippy::integer_arithmetic)]
#[allow(clippy::arithmetic_side_effects)]
fn are_others_valid(&self, instance: &Value, idx: usize) -> bool {
// `idx + 1` will not overflow, because the maximum possible value there is `usize::MAX - 1`
// For example we have `usize::MAX` schemas and only the last one is valid, then
@ -74,7 +74,7 @@ impl Validate for OneOfValidator {
fn validate<'instance>(
&self,
instance: &'instance Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> ErrorIterator<'instance> {
let first_valid_idx = self.get_first_valid(instance);
if let Some(idx) = first_valid_idx {
@ -97,7 +97,7 @@ impl Validate for OneOfValidator {
fn apply<'a>(
&'a self,
instance: &Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> PartialApplication<'a> {
let mut failures = Vec::new();
let mut successes = Vec::new();

View File

@ -2,7 +2,7 @@ use crate::{
compilation::context::CompilationContext,
error::{error, no_error, ErrorIterator, ValidationError},
keywords::CompilationResult,
paths::InstancePath,
paths::JsonPointerNode,
primitive_type::PrimitiveType,
validator::Validate,
};
@ -61,7 +61,7 @@ impl Validate for PatternValidator {
fn validate<'instance>(
&self,
instance: &'instance Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> ErrorIterator<'instance> {
if let Value::String(item) = instance {
match self.pattern.is_match(item) {
@ -103,6 +103,7 @@ impl core::fmt::Display for PatternValidator {
}
// ECMA 262 has differences
#[allow(clippy::result_large_err)]
pub(crate) fn convert_regex(pattern: &str) -> Result<fancy_regex::Regex, fancy_regex::Error> {
// replace control chars
let new_pattern = CONTROL_GROUPS_RE.replace_all(pattern, replace_control_group);
@ -145,7 +146,7 @@ pub(crate) fn convert_regex(pattern: &str) -> Result<fancy_regex::Regex, fancy_r
fancy_regex::Regex::new(&out)
}
#[allow(clippy::integer_arithmetic)]
#[allow(clippy::arithmetic_side_effects)]
fn replace_control_group(captures: &regex::Captures) -> String {
// There will be no overflow, because the minimum value is 65 (char 'A')
((captures

View File

@ -3,7 +3,7 @@ use crate::{
error::{no_error, ErrorIterator, ValidationError},
keywords::CompilationResult,
output::BasicOutput,
paths::{InstancePath, JSONPointer},
paths::{JSONPointer, JsonPointerNode},
primitive_type::PrimitiveType,
schema_node::SchemaNode,
validator::{format_validators, PartialApplication, Validate},
@ -24,7 +24,7 @@ impl PatternPropertiesValidator {
let keyword_context = context.with_path("patternProperties");
let mut patterns = Vec::with_capacity(map.len());
for (pattern, subschema) in map {
let pattern_context = keyword_context.with_path(pattern.to_string());
let pattern_context = keyword_context.with_path(pattern.as_str());
patterns.push((
match Regex::new(pattern) {
Ok(r) => r,
@ -61,7 +61,7 @@ impl Validate for PatternPropertiesValidator {
fn validate<'instance>(
&self,
instance: &'instance Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> ErrorIterator<'instance> {
if let Value::Object(item) = instance {
let errors: Vec<_> = self
@ -71,7 +71,7 @@ impl Validate for PatternPropertiesValidator {
item.iter()
.filter(move |(key, _)| re.is_match(key).unwrap_or(false))
.flat_map(move |(key, value)| {
let instance_path = instance_path.push(key.clone());
let instance_path = instance_path.push(key.as_str());
node.validate(value, &instance_path)
})
})
@ -85,7 +85,7 @@ impl Validate for PatternPropertiesValidator {
fn apply<'a>(
&'a self,
instance: &Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> PartialApplication<'a> {
if let Value::Object(item) = instance {
let mut matched_propnames = Vec::with_capacity(item.len());
@ -93,14 +93,14 @@ impl Validate for PatternPropertiesValidator {
for (pattern, node) in &self.patterns {
for (key, value) in item {
if pattern.is_match(key).unwrap_or(false) {
let path = instance_path.push(key.clone());
let path = instance_path.push(key.as_str());
matched_propnames.push(key.clone());
sub_results += node.apply_rooted(value, &path);
}
}
}
let mut result: PartialApplication = sub_results.into();
result.annotate(serde_json::Value::from(matched_propnames).into());
result.annotate(Value::from(matched_propnames).into());
result
} else {
PartialApplication::valid_empty()
@ -135,7 +135,7 @@ impl SingleValuePatternPropertiesValidator {
context: &CompilationContext,
) -> CompilationResult<'a> {
let keyword_context = context.with_path("patternProperties");
let pattern_context = keyword_context.with_path(pattern.to_string());
let pattern_context = keyword_context.with_path(pattern);
Ok(Box::new(SingleValuePatternPropertiesValidator {
pattern: match Regex::new(pattern) {
Ok(r) => r,
@ -168,14 +168,14 @@ impl Validate for SingleValuePatternPropertiesValidator {
fn validate<'instance>(
&self,
instance: &'instance Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> ErrorIterator<'instance> {
if let Value::Object(item) = instance {
let errors: Vec<_> = item
.iter()
.filter(move |(key, _)| self.pattern.is_match(key).unwrap_or(false))
.flat_map(move |(key, value)| {
let instance_path = instance_path.push(key.clone());
let instance_path = instance_path.push(key.as_str());
self.node.validate(value, &instance_path)
})
.collect();
@ -188,20 +188,20 @@ impl Validate for SingleValuePatternPropertiesValidator {
fn apply<'a>(
&'a self,
instance: &Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> PartialApplication<'a> {
if let Value::Object(item) = instance {
let mut matched_propnames = Vec::with_capacity(item.len());
let mut outputs = BasicOutput::default();
for (key, value) in item {
if self.pattern.is_match(key).unwrap_or(false) {
let path = instance_path.push(key.clone());
let path = instance_path.push(key.as_str());
matched_propnames.push(key.clone());
outputs += self.node.apply_rooted(value, &path);
}
}
let mut result: PartialApplication = outputs.into();
result.annotate(serde_json::Value::from(matched_propnames).into());
result.annotate(Value::from(matched_propnames).into());
result
} else {
PartialApplication::valid_empty()

View File

@ -1,7 +1,7 @@
use crate::{
compilation::{compile_validators, context::CompilationContext},
error::{no_error, ErrorIterator, ValidationError},
paths::{InstancePath, JSONPointer},
paths::{JSONPointer, JsonPointerNode},
primitive_type::PrimitiveType,
schema_node::SchemaNode,
validator::{format_iter_of_validators, PartialApplication, Validate},
@ -47,7 +47,7 @@ impl Validate for PrefixItemsValidator {
fn validate<'instance>(
&self,
instance: &'instance Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> ErrorIterator<'instance> {
if let Value::Array(items) = instance {
let errors: Vec<_> = self
@ -66,7 +66,7 @@ impl Validate for PrefixItemsValidator {
fn apply<'a>(
&'a self,
instance: &Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> PartialApplication<'a> {
if let Value::Array(items) = instance {
if !items.is_empty() {

View File

@ -3,7 +3,7 @@ use crate::{
error::{no_error, ErrorIterator, ValidationError},
keywords::CompilationResult,
output::BasicOutput,
paths::{InstancePath, JSONPointer},
paths::{JSONPointer, JsonPointerNode},
primitive_type::PrimitiveType,
schema_node::SchemaNode,
validator::{format_key_value_validators, PartialApplication, Validate},
@ -25,7 +25,7 @@ impl PropertiesValidator {
let context = context.with_path("properties");
let mut properties = Vec::with_capacity(map.len());
for (key, subschema) in map {
let property_context = context.with_path(key.clone());
let property_context = context.with_path(key.as_str());
properties.push((
key.clone(),
compile_validators(subschema, &property_context)?,
@ -59,7 +59,7 @@ impl Validate for PropertiesValidator {
fn validate<'instance>(
&self,
instance: &'instance Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> ErrorIterator<'instance> {
if let Value::Object(item) = instance {
let errors: Vec<_> = self
@ -68,7 +68,7 @@ impl Validate for PropertiesValidator {
.flat_map(move |(name, node)| {
let option = item.get(name);
option.into_iter().flat_map(move |item| {
let instance_path = instance_path.push(name.clone());
let instance_path = instance_path.push(name.as_str());
node.validate(item, &instance_path)
})
})
@ -82,20 +82,20 @@ impl Validate for PropertiesValidator {
fn apply<'a>(
&'a self,
instance: &Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> PartialApplication<'a> {
if let Value::Object(props) = instance {
let mut result = BasicOutput::default();
let mut matched_props = Vec::with_capacity(props.len());
for (prop_name, node) in &self.properties {
if let Some(prop) = props.get(prop_name) {
let path = instance_path.push(prop_name.clone());
let path = instance_path.push(prop_name.as_str());
matched_props.push(prop_name.clone());
result += node.apply_rooted(prop, &path);
}
}
let mut application: PartialApplication = result.into();
application.annotate(serde_json::Value::from(matched_props).into());
application.annotate(Value::from(matched_props).into());
application
} else {
PartialApplication::valid_empty()

View File

@ -2,7 +2,7 @@ use crate::{
compilation::{compile_validators, context::CompilationContext},
error::{error, no_error, ErrorIterator, ValidationError},
keywords::CompilationResult,
paths::{InstancePath, JSONPointer},
paths::{JSONPointer, JsonPointerNode},
schema_node::SchemaNode,
validator::{format_validators, PartialApplication, Validate},
};
@ -41,7 +41,7 @@ impl Validate for PropertyNamesObjectValidator {
fn validate<'instance>(
&self,
instance: &'instance Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> ErrorIterator<'instance> {
if let Value::Object(item) = &instance {
let errors: Vec<_> = item
@ -72,7 +72,7 @@ impl Validate for PropertyNamesObjectValidator {
fn apply<'a>(
&'a self,
instance: &Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> PartialApplication<'a> {
if let Value::Object(item) = instance {
item.keys()
@ -121,7 +121,7 @@ impl Validate for PropertyNamesBooleanValidator {
fn validate<'instance>(
&self,
instance: &'instance Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> ErrorIterator<'instance> {
if self.is_valid(instance) {
no_error()

View File

@ -2,7 +2,7 @@ use crate::{
compilation::{compile_validators, context::CompilationContext},
error::{error, ErrorIterator},
keywords::CompilationResult,
paths::{InstancePath, JSONPointer},
paths::{JSONPointer, JsonPointerNode},
primitive_type::PrimitiveType,
resolver::Resolver,
schema_node::SchemaNode,
@ -17,11 +17,6 @@ use url::Url;
pub(crate) struct RefValidator {
original_reference: String,
reference: Url,
/// Precomputed validators.
/// They are behind a RwLock as is not possible to compute them
/// at compile time without risking infinite loops of references
/// and at the same time during validation we iterate over shared
/// references (&self) and not owned references (&mut self).
sub_nodes: RwLock<Option<SchemaNode>>,
schema_path: JSONPointer,
config: Arc<CompilationOptions>,
@ -72,11 +67,17 @@ impl Validate for RefValidator {
fn validate<'instance>(
&self,
instance: &'instance Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> ErrorIterator<'instance> {
let extend_error_schema_path = move |mut error: ValidationError<'instance>| {
let schema_path = self.schema_path.clone();
error.schema_path = schema_path.extend_with(error.schema_path.as_slice());
error
};
if let Some(node) = self.sub_nodes.read().as_ref() {
return Box::new(
node.validate(instance, instance_path)
node.err_iter(instance, instance_path)
.map(extend_error_schema_path)
.collect::<Vec<_>>()
.into_iter(),
);
@ -96,12 +97,7 @@ impl Validate for RefValidator {
Ok(node) => {
let result = Box::new(
node.err_iter(instance, instance_path)
.map(move |mut error| {
let schema_path = self.schema_path.clone();
error.schema_path =
schema_path.extend_with(error.schema_path.as_slice());
error
})
.map(extend_error_schema_path)
.collect::<Vec<_>>()
.into_iter(),
);
@ -155,15 +151,70 @@ pub(crate) const fn supports_adjacent_validation(draft: Draft) -> bool {
#[cfg(test)]
mod tests {
use crate::tests_util;
use serde_json::json;
use crate::{tests_util, JSONSchema};
use serde_json::{json, Value};
use test_case::test_case;
#[test_case(
&json!({
"properties": {
"foo": {"$ref": "#/definitions/foo"}
},
"definitions": {
"foo": {"type": "string"}
}
}),
&json!({"foo": 42}),
"/properties/foo/type"
)]
fn schema_path(schema: &Value, instance: &Value, expected: &str) {
tests_util::assert_schema_path(schema, instance, expected)
}
#[test]
fn schema_path() {
tests_util::assert_schema_path(
&json!({"properties": {"foo": {"$ref": "#/definitions/foo"}}, "definitions": {"foo": {"type": "string"}}}),
&json!({"foo": 42}),
"/properties/foo/type",
)
fn multiple_errors_schema_paths() {
let instance = json!({
"things": [
{ "code": "CC" },
{ "code": "CC" },
]
});
let schema = json!({
"type": "object",
"properties": {
"things": {
"type": "array",
"items": {
"type": "object",
"properties": {
"code": {
"type": "string",
"$ref": "#/$defs/codes"
}
},
"required": ["code"]
}
}
},
"required": ["things"],
"$defs": { "codes": { "enum": ["AA", "BB"] } }
});
let compiled = JSONSchema::options().compile(&schema).unwrap();
let mut iter = compiled.validate(&instance).expect_err("Should fail");
let expected = "/properties/things/items/properties/code/enum";
assert_eq!(
iter.next()
.expect("Should be present")
.schema_path
.to_string(),
expected
);
assert_eq!(
iter.next()
.expect("Should be present")
.schema_path
.to_string(),
expected
);
}
}

View File

@ -2,7 +2,7 @@ use crate::{
compilation::context::CompilationContext,
error::{error, no_error, ErrorIterator, ValidationError},
keywords::CompilationResult,
paths::{InstancePath, JSONPointer},
paths::{JSONPointer, JsonPointerNode},
primitive_type::PrimitiveType,
validator::Validate,
};
@ -51,7 +51,7 @@ impl Validate for RequiredValidator {
fn validate<'instance>(
&self,
instance: &'instance Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> ErrorIterator<'instance> {
if let Value::Object(item) = instance {
let mut errors = vec![];
@ -99,7 +99,7 @@ impl Validate for SingleItemRequiredValidator {
fn validate<'instance>(
&self,
instance: &'instance Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> ErrorIterator<'instance> {
if !self.is_valid(instance) {
return error(ValidationError::required(

View File

@ -8,7 +8,7 @@ use crate::{
use serde_json::{json, Map, Number, Value};
use std::convert::TryFrom;
use crate::paths::{InstancePath, JSONPointer};
use crate::paths::{JSONPointer, JsonPointerNode};
pub(crate) struct MultipleTypesValidator {
types: PrimitiveTypesBitMap,
@ -66,7 +66,7 @@ impl Validate for MultipleTypesValidator {
fn validate<'instance>(
&self,
instance: &'instance Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> ErrorIterator<'instance> {
if self.is_valid(instance) {
no_error()
@ -113,7 +113,7 @@ impl Validate for NullTypeValidator {
fn validate<'instance>(
&self,
instance: &'instance Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> ErrorIterator<'instance> {
if self.is_valid(instance) {
no_error()
@ -152,7 +152,7 @@ impl Validate for BooleanTypeValidator {
fn validate<'instance>(
&self,
instance: &'instance Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> ErrorIterator<'instance> {
if self.is_valid(instance) {
no_error()
@ -192,7 +192,7 @@ impl Validate for StringTypeValidator {
fn validate<'instance>(
&self,
instance: &'instance Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> ErrorIterator<'instance> {
if self.is_valid(instance) {
no_error()
@ -231,7 +231,7 @@ impl Validate for ArrayTypeValidator {
fn validate<'instance>(
&self,
instance: &'instance Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> ErrorIterator<'instance> {
if self.is_valid(instance) {
no_error()
@ -270,7 +270,7 @@ impl Validate for ObjectTypeValidator {
fn validate<'instance>(
&self,
instance: &'instance Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> ErrorIterator<'instance> {
if self.is_valid(instance) {
no_error()
@ -309,7 +309,7 @@ impl Validate for NumberTypeValidator {
fn validate<'instance>(
&self,
instance: &'instance Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> ErrorIterator<'instance> {
if self.is_valid(instance) {
no_error()
@ -350,7 +350,7 @@ impl Validate for IntegerTypeValidator {
fn validate<'instance>(
&self,
instance: &'instance Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> ErrorIterator<'instance> {
if self.is_valid(instance) {
no_error()

View File

@ -5,7 +5,7 @@ use crate::{
error::{no_error, ErrorIterator, ValidationError},
keywords::CompilationResult,
output::BasicOutput,
paths::{InstancePath, JSONPointer},
paths::{JSONPointer, JsonPointerNode},
primitive_type::PrimitiveType,
properties::*,
schema_node::SchemaNode,
@ -189,8 +189,8 @@ impl UnevaluatedPropertiesValidator {
fn validate_property<'instance>(
&self,
instance: &'instance Value,
instance_path: &InstancePath,
property_path: &InstancePath,
instance_path: &JsonPointerNode,
property_path: &JsonPointerNode,
property_instance: &'instance Value,
property_name: &str,
) -> Option<ErrorIterator<'instance>> {
@ -264,8 +264,8 @@ impl UnevaluatedPropertiesValidator {
fn apply_property<'a>(
&'a self,
instance: &Value,
instance_path: &InstancePath,
property_path: &InstancePath,
instance_path: &JsonPointerNode,
property_path: &JsonPointerNode,
property_instance: &Value,
property_name: &str,
) -> Option<BasicOutput<'a>> {
@ -354,14 +354,14 @@ impl Validate for UnevaluatedPropertiesValidator {
fn validate<'instance>(
&self,
instance: &'instance Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> ErrorIterator<'instance> {
if let Value::Object(props) = instance {
let mut errors = vec![];
let mut unevaluated = vec![];
for (property_name, property_instance) in props {
let property_path = instance_path.push(property_name.clone());
let property_path = instance_path.push(property_name.as_str());
let maybe_property_errors = self.validate_property(
instance,
instance_path,
@ -396,14 +396,14 @@ impl Validate for UnevaluatedPropertiesValidator {
fn apply<'a>(
&'a self,
instance: &Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> PartialApplication<'a> {
if let Value::Object(props) = instance {
let mut output = BasicOutput::default();
let mut unevaluated = vec![];
for (property_name, property_instance) in props {
let property_path = instance_path.push(property_name.clone());
let property_path = instance_path.push(property_name.as_str());
let maybe_property_output = self.apply_property(
instance,
instance_path,
@ -471,7 +471,7 @@ impl PropertySubvalidator {
fn validate_property<'instance>(
&self,
property_path: &InstancePath,
property_path: &JsonPointerNode,
property_instance: &'instance Value,
property_name: &str,
) -> Option<ErrorIterator<'instance>> {
@ -482,7 +482,7 @@ impl PropertySubvalidator {
fn apply_property<'a>(
&'a self,
property_path: &InstancePath,
property_path: &JsonPointerNode,
property_instance: &Value,
property_name: &str,
) -> Option<BasicOutput<'a>> {
@ -528,7 +528,7 @@ impl PatternSubvalidator {
fn validate_property<'instance>(
&self,
property_path: &InstancePath,
property_path: &JsonPointerNode,
property_instance: &'instance Value,
property_name: &str,
) -> Option<ErrorIterator<'instance>> {
@ -548,7 +548,7 @@ impl PatternSubvalidator {
fn apply_property<'a>(
&'a self,
property_path: &InstancePath,
property_path: &JsonPointerNode,
property_instance: &Value,
property_name: &str,
) -> Option<BasicOutput<'a>> {
@ -610,7 +610,7 @@ impl SubschemaSubvalidator {
for (i, value) in values.iter().enumerate() {
if let Value::Object(subschema) = value {
let subschema_context = keyword_context.with_path(i.to_string());
let subschema_context = keyword_context.with_path(i);
let node = compile_validators(value, &subschema_context)?;
let subvalidator = UnevaluatedPropertiesValidator::compile(
@ -692,8 +692,8 @@ impl SubschemaSubvalidator {
fn validate_property<'instance>(
&self,
instance: &'instance Value,
instance_path: &InstancePath,
property_path: &InstancePath,
instance_path: &JsonPointerNode,
property_path: &JsonPointerNode,
property_instance: &'instance Value,
property_name: &str,
) -> Option<ErrorIterator<'instance>> {
@ -774,8 +774,8 @@ impl SubschemaSubvalidator {
fn apply_property<'a>(
&'a self,
instance: &Value,
instance_path: &InstancePath,
property_path: &InstancePath,
instance_path: &JsonPointerNode,
property_path: &JsonPointerNode,
property_instance: &Value,
property_name: &str,
) -> Option<BasicOutput<'a>> {
@ -893,7 +893,7 @@ impl UnevaluatedSubvalidator {
fn validate_property<'instance>(
&self,
property_path: &InstancePath,
property_path: &JsonPointerNode,
property_instance: &'instance Value,
_property_name: &str,
) -> Option<ErrorIterator<'instance>> {
@ -908,7 +908,7 @@ impl UnevaluatedSubvalidator {
fn apply_property<'a>(
&'a self,
property_path: &InstancePath,
property_path: &JsonPointerNode,
property_instance: &Value,
_property_name: &str,
) -> Option<BasicOutput<'a>> {
@ -1014,8 +1014,8 @@ impl ConditionalSubvalidator {
fn validate_property<'instance>(
&self,
instance: &'instance Value,
instance_path: &InstancePath,
property_path: &InstancePath,
instance_path: &JsonPointerNode,
property_path: &JsonPointerNode,
property_instance: &'instance Value,
property_name: &str,
) -> Option<ErrorIterator<'instance>> {
@ -1058,8 +1058,8 @@ impl ConditionalSubvalidator {
fn apply_property<'a>(
&'a self,
instance: &Value,
instance_path: &InstancePath,
property_path: &InstancePath,
instance_path: &JsonPointerNode,
property_path: &JsonPointerNode,
property_instance: &Value,
property_name: &str,
) -> Option<BasicOutput<'a>> {
@ -1124,7 +1124,7 @@ impl DependentSchemaSubvalidator {
.as_object()
.ok_or_else(ValidationError::null_schema)?;
let schema_context = keyword_context.with_path(dependent_property_name.to_string());
let schema_context = keyword_context.with_path(dependent_property_name.as_str());
let node = UnevaluatedPropertiesValidator::compile(
dependent_schema,
get_transitive_unevaluated_props_schema(dependent_schema, parent),
@ -1156,8 +1156,8 @@ impl DependentSchemaSubvalidator {
fn validate_property<'instance>(
&self,
instance: &'instance Value,
instance_path: &InstancePath,
property_path: &InstancePath,
instance_path: &JsonPointerNode,
property_path: &JsonPointerNode,
property_instance: &'instance Value,
property_name: &str,
) -> Option<ErrorIterator<'instance>> {
@ -1181,8 +1181,8 @@ impl DependentSchemaSubvalidator {
fn apply_property<'a>(
&'a self,
instance: &Value,
instance_path: &InstancePath,
property_path: &InstancePath,
instance_path: &JsonPointerNode,
property_path: &JsonPointerNode,
property_instance: &Value,
property_name: &str,
) -> Option<BasicOutput<'a>> {
@ -1263,8 +1263,8 @@ impl ReferenceSubvalidator {
fn validate_property<'instance>(
&self,
instance: &'instance Value,
instance_path: &InstancePath,
property_path: &InstancePath,
instance_path: &JsonPointerNode,
property_path: &JsonPointerNode,
property_instance: &'instance Value,
property_name: &str,
) -> Option<ErrorIterator<'instance>> {
@ -1280,8 +1280,8 @@ impl ReferenceSubvalidator {
fn apply_property<'a>(
&'a self,
instance: &Value,
instance_path: &InstancePath,
property_path: &InstancePath,
instance_path: &JsonPointerNode,
property_path: &JsonPointerNode,
property_instance: &Value,
property_name: &str,
) -> Option<BasicOutput<'a>> {

View File

@ -7,7 +7,7 @@ use crate::{
use ahash::{AHashSet, AHasher};
use serde_json::{Map, Value};
use crate::paths::{InstancePath, JSONPointer};
use crate::paths::{JSONPointer, JsonPointerNode};
use std::hash::{Hash, Hasher};
// Based on implementation proposed by Sven Marnach:
@ -54,7 +54,7 @@ impl Hash for HashedValue<'_> {
}
// Empirically calculated threshold after which the validator resorts to hashing.
// Calculated for an array of mixed types, large homogenous arrays of primitive values might be
// Calculated for an array of mixed types, large homogeneous arrays of primitive values might be
// processed faster with different thresholds, but this one gives a good baseline for the common
// case.
const ITEMS_SIZE_THRESHOLD: usize = 15;
@ -114,7 +114,7 @@ impl Validate for UniqueItemsValidator {
fn validate<'instance>(
&self,
instance: &'instance Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> ErrorIterator<'instance> {
if self.is_valid(instance) {
no_error()

View File

@ -82,7 +82,7 @@
clippy::upper_case_acronyms,
clippy::needless_collect
)]
#![cfg_attr(not(test), allow(clippy::integer_arithmetic, clippy::unwrap_used))]
#![cfg_attr(not(test), allow(clippy::arithmetic_side_effects, clippy::unwrap_used))]
mod compilation;
mod content_encoding;
mod content_media_type;
@ -99,6 +99,7 @@ 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;

View File

@ -1,33 +1,76 @@
use std::{
error::Error,
fs::File,
io::BufReader,
path::{Path, PathBuf},
process,
};
use clap::Parser;
use jsonschema::JSONSchema;
type BoxErrorResult<T> = Result<T, Box<dyn Error>>;
#[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<Vec<PathBuf>>,
/// The JSON Schema to validate with (i.e. schema.json).
#[arg(value_parser, required_unless_present("version"))]
schema: Option<PathBuf>,
/// Show program's version number and exit.
#[arg(short = 'v', long = "version")]
version: bool,
#[cfg(not(feature = "cli"))]
fn main() {
eprintln!("`jsonschema` CLI is only available with the `cli` feature");
std::process::exit(1);
}
pub fn main() -> BoxErrorResult<()> {
#[cfg(feature = "cli")]
fn main() -> Result<(), Box<dyn std::error::Error>> {
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<Vec<PathBuf>>,
/// The JSON Schema to validate with (i.e. schema.json).
#[arg(value_parser, required_unless_present("version"))]
schema: Option<PathBuf>,
/// Show program's version number and exit.
#[arg(short = 'v', long = "version")]
version: bool,
}
fn read_json(path: &Path) -> serde_json::Result<serde_json::Value> {
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<bool, Box<dyn std::error::Error>> {
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 {
@ -48,40 +91,3 @@ pub fn main() -> BoxErrorResult<()> {
Ok(())
}
fn read_json(path: &Path) -> serde_json::Result<serde_json::Value> {
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) -> BoxErrorResult<bool> {
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)
}

View File

@ -1,7 +1,7 @@
//! Implementation of json schema output formats specified in <https://json-schema.org/draft/2020-12/json-schema-core.html#rfc.section.12.2>
//!
//! Currently the "flag" and "basic" formats are supported. The "flag" format is
//! idential to the [`JSONSchema::is_valid`] method and so is uninteresting. The
//! identical to the [`JSONSchema::is_valid`] method and so is uninteresting. The
//! main contribution of this module is [`Output::basic`]. See the documentation
//! of that method for more information.
@ -18,7 +18,7 @@ use ahash::AHashMap;
use serde::ser::SerializeMap;
use crate::{
paths::{AbsolutePath, InstancePath, JSONPointer},
paths::{AbsolutePath, JSONPointer, JsonPointerNode},
schema_node::SchemaNode,
JSONSchema,
};
@ -107,7 +107,7 @@ impl<'a, 'b> Output<'a, 'b> {
#[must_use]
pub fn basic(&self) -> BasicOutput<'a> {
self.root_node
.apply_rooted(self.instance, &InstancePath::new())
.apply_rooted(self.instance, &JsonPointerNode::new())
}
}

View File

@ -80,7 +80,6 @@ impl fmt::Display for JSONPointer {
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
/// A key within a JSON object or an index within a JSON array.
/// A sequence of chunks represents a valid path within a JSON value.
///
@ -96,6 +95,7 @@ impl fmt::Display for JSONPointer {
/// 2. Take the 2nd value from the array - `PathChunk::Index(2)`
///
/// The primary purpose of this enum is to avoid converting indexes to strings during validation.
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum PathChunk {
/// Property name within a JSON object.
Property(Box<str>),
@ -105,43 +105,78 @@ pub enum PathChunk {
Keyword(&'static str),
}
#[derive(Debug, Clone)]
pub(crate) struct InstancePath<'a> {
pub(crate) chunk: Option<PathChunk>,
pub(crate) parent: Option<&'a InstancePath<'a>>,
/// A borrowed variant of `PathChunk`.
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum PathChunkRef<'a> {
/// Property name within a JSON object.
Property(&'a str),
/// JSON Schema keyword.
Index(usize),
}
impl<'a> InstancePath<'a> {
pub(crate) const fn new() -> Self {
InstancePath {
chunk: None,
/// A node in a linked list representing a JSON pointer.
///
/// `JsonPointerNode` is used to build a JSON pointer incrementally during the JSON Schema validation process.
/// Each node contains a segment of the JSON pointer and a reference to its parent node, forming
/// a linked list.
///
/// The linked list representation allows for efficient traversal and manipulation of the JSON pointer
/// without the need for memory allocation.
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct JsonPointerNode<'a, 'b> {
pub(crate) segment: PathChunkRef<'a>,
pub(crate) parent: Option<&'b JsonPointerNode<'b, 'a>>,
}
impl Default for JsonPointerNode<'_, '_> {
fn default() -> Self {
JsonPointerNode::new()
}
}
impl<'a, 'b> JsonPointerNode<'a, 'b> {
/// Create a root node of a JSON pointer.
pub const fn new() -> Self {
JsonPointerNode {
// The value does not matter, it will never be used
segment: PathChunkRef::Index(0),
parent: None,
}
}
/// Push a new segment to the JSON pointer.
#[inline]
pub(crate) fn push(&'a self, chunk: impl Into<PathChunk>) -> Self {
InstancePath {
chunk: Some(chunk.into()),
pub fn push(&'a self, segment: impl Into<PathChunkRef<'a>>) -> Self {
JsonPointerNode {
segment: segment.into(),
parent: Some(self),
}
}
pub(crate) fn to_vec(&'a self) -> Vec<PathChunk> {
// The path capacity should be the average depth so we avoid extra allocations
let mut result = Vec::with_capacity(6);
let mut current = self;
if let Some(chunk) = &current.chunk {
result.push(chunk.clone())
/// Convert the JSON pointer node to a vector of path segments.
pub fn to_vec(&'a self) -> Vec<PathChunk> {
// Walk the linked list to calculate the capacity
let mut capacity = 0;
let mut head = self;
while let Some(next) = head.parent {
head = next;
capacity += 1;
}
while let Some(next) = current.parent {
current = next;
if let Some(chunk) = &current.chunk {
result.push(chunk.clone())
// Callect the segments from the head to the tail
let mut buffer = Vec::with_capacity(capacity);
let mut head = self;
if head.parent.is_some() {
buffer.push(head.segment.into())
}
while let Some(next) = head.parent {
head = next;
if head.parent.is_some() {
buffer.push(head.segment.into());
}
}
result.reverse();
result
// Reverse the buffer to get the segments in the correct order
buffer.reverse();
buffer
}
}
@ -169,12 +204,14 @@ impl From<String> for PathChunk {
PathChunk::Property(value.into_boxed_str())
}
}
impl From<&'static str> for PathChunk {
#[inline]
fn from(value: &'static str) -> Self {
PathChunk::Keyword(value)
}
}
impl From<usize> for PathChunk {
#[inline]
fn from(value: usize) -> Self {
@ -182,16 +219,40 @@ impl From<usize> for PathChunk {
}
}
impl<'a> From<&'a InstancePath<'a>> for JSONPointer {
impl<'a> From<&'a str> for PathChunkRef<'a> {
#[inline]
fn from(path: &'a InstancePath<'a>) -> Self {
fn from(value: &'a str) -> PathChunkRef<'a> {
PathChunkRef::Property(value)
}
}
impl From<usize> for PathChunkRef<'_> {
#[inline]
fn from(value: usize) -> Self {
PathChunkRef::Index(value)
}
}
impl<'a> From<PathChunkRef<'a>> for PathChunk {
#[inline]
fn from(value: PathChunkRef<'a>) -> Self {
match value {
PathChunkRef::Property(value) => PathChunk::Property(value.into()),
PathChunkRef::Index(value) => PathChunk::Index(value),
}
}
}
impl<'a, 'b> From<&'a JsonPointerNode<'a, 'b>> for JSONPointer {
#[inline]
fn from(path: &'a JsonPointerNode<'a, 'b>) -> Self {
JSONPointer(path.to_vec())
}
}
impl From<InstancePath<'_>> for JSONPointer {
impl From<JsonPointerNode<'_, '_>> for JSONPointer {
#[inline]
fn from(path: InstancePath<'_>) -> Self {
fn from(path: JsonPointerNode<'_, '_>) -> Self {
JSONPointer(path.to_vec())
}
}

View File

@ -142,7 +142,7 @@ pub struct PrimitiveTypesBitMapIterator {
}
impl Iterator for PrimitiveTypesBitMapIterator {
type Item = PrimitiveType;
#[allow(clippy::integer_arithmetic)]
#[allow(clippy::arithmetic_side_effects)]
fn next(&mut self) -> Option<Self::Item> {
while self.idx <= 7 {
let bit_value = 1 << self.idx;

View File

@ -95,7 +95,7 @@ pub(crate) fn compile_small_map<'a>(
let mut properties = Vec::with_capacity(map.len());
let keyword_context = context.with_path("properties");
for (key, subschema) in map {
let property_context = keyword_context.with_path(key.clone());
let property_context = keyword_context.with_path(key.as_str());
properties.push((
key.clone(),
compile_validators(subschema, &property_context)?,
@ -111,7 +111,7 @@ pub(crate) fn compile_big_map<'a>(
let mut properties = AHashMap::with_capacity(map.len());
let keyword_context = context.with_path("properties");
for (key, subschema) in map {
let property_context = keyword_context.with_path(key.clone());
let property_context = keyword_context.with_path(key.as_str());
properties.insert(
key.clone(),
compile_validators(subschema, &property_context)?,
@ -143,7 +143,7 @@ pub(crate) fn compile_patterns<'a>(
let keyword_context = context.with_path("patternProperties");
let mut compiled_patterns = Vec::with_capacity(obj.len());
for (pattern, subschema) in obj {
let pattern_context = keyword_context.with_path(pattern.to_string());
let pattern_context = keyword_context.with_path(pattern.as_str());
if let Ok(compiled_pattern) = Regex::new(pattern) {
let node = compile_validators(subschema, &pattern_context)?;
compiled_patterns.push((compiled_pattern, node));

View File

@ -3,7 +3,7 @@ use crate::{
error::ErrorIterator,
keywords::BoxedValidator,
output::{Annotations, BasicOutput, ErrorDescription, OutputUnit},
paths::{AbsolutePath, InstancePath, JSONPointer},
paths::{AbsolutePath, JSONPointer, JsonPointerNode},
validator::{format_validators, PartialApplication, Validate},
};
use ahash::AHashMap;
@ -87,7 +87,7 @@ impl SchemaNode {
}
}
pub(crate) fn validators(&self) -> impl Iterator<Item = &BoxedValidator> + ExactSizeIterator {
pub(crate) fn validators(&self) -> impl ExactSizeIterator<Item = &BoxedValidator> {
match &self.validators {
NodeValidators::Boolean { validator } => {
if let Some(v) = validator {
@ -116,7 +116,7 @@ impl SchemaNode {
pub(crate) fn apply_rooted(
&self,
instance: &serde_json::Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> BasicOutput {
match self.apply(instance, instance_path) {
PartialApplication::Valid {
@ -143,7 +143,7 @@ impl SchemaNode {
/// Create an error output which is marked as occurring at this schema node
pub(crate) fn error_at(
&self,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
error: ErrorDescription,
) -> OutputUnit<ErrorDescription> {
OutputUnit::<ErrorDescription>::error(
@ -157,7 +157,7 @@ impl SchemaNode {
/// Create an annotation output which is marked as occurring at this schema node
pub(crate) fn annotation_at<'a>(
&self,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
annotations: Annotations<'a>,
) -> OutputUnit<Annotations<'a>> {
OutputUnit::<Annotations<'_>>::annotations(
@ -174,7 +174,7 @@ impl SchemaNode {
pub(crate) fn err_iter<'a>(
&self,
instance: &'a serde_json::Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> NodeValidatorsErrIter<'a> {
match &self.validators {
NodeValidators::Keyword(kvs) if kvs.validators.len() == 1 => {
@ -210,13 +210,13 @@ impl SchemaNode {
fn apply_subschemas<'a, I, P>(
&self,
instance: &serde_json::Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
path_and_validators: I,
annotations: Option<Annotations<'a>>,
) -> PartialApplication<'a>
where
I: Iterator<Item = (P, &'a Box<dyn Validate + Send + Sync + 'a>)> + 'a,
P: Into<crate::paths::PathChunk> + std::fmt::Display,
P: Into<crate::paths::PathChunk> + fmt::Display,
{
let mut success_results: VecDeque<OutputUnit<Annotations>> = VecDeque::new();
let mut error_results = VecDeque::new();
@ -281,9 +281,9 @@ impl Validate for SchemaNode {
fn validate<'instance>(
&self,
instance: &'instance serde_json::Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> ErrorIterator<'instance> {
return Box::new(self.err_iter(instance, instance_path));
Box::new(self.err_iter(instance, instance_path))
}
fn is_valid(&self, instance: &serde_json::Value) -> bool {
@ -307,7 +307,7 @@ impl Validate for SchemaNode {
fn apply<'a>(
&'a self,
instance: &serde_json::Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> PartialApplication<'a> {
match self.validators {
NodeValidators::Array { ref validators } => {

View File

@ -2,7 +2,7 @@ use crate::{
error::ErrorIterator,
keywords::BoxedValidator,
output::{Annotations, ErrorDescription, OutputUnit},
paths::InstancePath,
paths::JsonPointerNode,
schema_node::SchemaNode,
};
use serde_json::Value;
@ -26,7 +26,7 @@ pub(crate) trait Validate: Send + Sync + core::fmt::Display {
fn validate<'instance>(
&self,
instance: &'instance Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> ErrorIterator<'instance>;
// The same as above, but does not construct ErrorIterator.
// It is faster for cases when the result is not needed (like anyOf), since errors are
@ -79,7 +79,7 @@ pub(crate) trait Validate: Send + Sync + core::fmt::Display {
fn apply<'a>(
&'a self,
instance: &Value,
instance_path: &InstancePath,
instance_path: &JsonPointerNode,
) -> PartialApplication<'a> {
let errors: Vec<ErrorDescription> = self
.validate(instance, instance_path)

View File

@ -0,0 +1,2 @@
imports_granularity = "Crate"
edition = "2021"