Compare commits
17 Commits
python-v0.
...
master
Author | SHA1 | Date |
---|---|---|
Dmitry Dygalo | 8adae12108 | |
Dmitry Dygalo | de4fb4a16c | |
Dmitry Dygalo | dbe6c90d78 | |
Dmitry Dygalo | bcb6d393b5 | |
Dmitry Dygalo | ffdbcc98d5 | |
Dmitry Dygalo | 2cbc86fd6a | |
Dmitry Dygalo | cfab6027a1 | |
Dmitry Dygalo | 21df1810bd | |
Dmitry Dygalo | e564888da5 | |
Dmitry Dygalo | 0a00839482 | |
Dmitry Dygalo | 146f6cde42 | |
Sam Roberts | aa94a4b24a | |
Dmitry Dygalo | 7946e978b5 | |
Dmitry Dygalo | 092b573163 | |
Dmitry Dygalo | cd0b70a49a | |
Joel Natividad | 6cf82328e3 | |
Dmitry Dygalo | 9aae87e573 |
|
@ -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]
|
||||
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,31 +146,47 @@ 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
|
||||
|
||||
|
@ -206,16 +195,20 @@ jobs:
|
|||
|
||||
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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
23
CHANGELOG.md
23
CHANGELOG.md
|
@ -2,6 +2,14 @@
|
|||
|
||||
## [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`.
|
||||
|
@ -13,6 +21,18 @@
|
|||
- 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
|
||||
|
||||
|
@ -431,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
|
||||
|
|
2
LICENSE
2
LICENSE
|
@ -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
|
||||
|
|
89
README.md
89
README.md
|
@ -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.
|
||||
|
@ -158,7 +225,7 @@ $ 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.
|
||||
|
||||
|
@ -181,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:
|
||||
|
||||
|
@ -196,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
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
imports_granularity = "Crate"
|
||||
edition = "2021"
|
|
@ -1,4 +1,4 @@
|
|||
use criterion::Criterion;
|
||||
use codspeed_criterion_compat::Criterion;
|
||||
use serde::Deserialize;
|
||||
use serde_json::{from_reader, Value};
|
||||
use std::{
|
||||
|
|
|
@ -2,6 +2,19 @@
|
|||
|
||||
## [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
|
||||
|
@ -320,7 +333,8 @@
|
|||
## 0.1.0 - 2020-06-09
|
||||
- Initial public release
|
||||
|
||||
[Unreleased]: https://github.com/Stranger6667/jsonschema-rs/compare/python-v0.17.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
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,10 +1,10 @@
|
|||
[package]
|
||||
name = "jsonschema-python"
|
||||
version = "0.17.3"
|
||||
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"]
|
||||
|
@ -17,7 +17,7 @@ crate-type = ["cdylib"]
|
|||
|
||||
[build-dependencies]
|
||||
built = { version = "0.7.1", features = ["cargo-lock", "chrono"] }
|
||||
pyo3-build-config = { version = "0.20.3", features = ["resolve-config"] }
|
||||
pyo3-build-config = { version = "0.21.2", features = ["resolve-config"] }
|
||||
|
||||
[dependencies.jsonschema]
|
||||
path = "../../jsonschema"
|
||||
|
@ -28,8 +28,8 @@ features = ["resolve-http", "resolve-file", "draft201909", "draft202012"]
|
|||
[dependencies]
|
||||
serde_json = "1.0.91"
|
||||
serde = "1.0.152"
|
||||
pyo3 = { version = "0.20.3", 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
|
||||
|
|
|
@ -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.
|
|
@ -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, 3.11, and 3.12.
|
||||
|
||||
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
|
|
@ -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",
|
||||
|
|
|
@ -8,7 +8,7 @@ authors = [
|
|||
maintainers = [
|
||||
{name = "Dmitry Dygalo", email = "dmitry@dygalo.dev"}
|
||||
]
|
||||
readme = "README.rst"
|
||||
readme = "README.md"
|
||||
license = { text = "MIT" }
|
||||
classifiers = [
|
||||
"Development Status :: 3 - Alpha",
|
||||
|
@ -36,18 +36,16 @@ 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"
|
||||
|
|
|
@ -1 +1 @@
|
|||
from .jsonschema_rs import *
|
||||
from .jsonschema_rs import * # noqa: F403
|
||||
|
|
|
@ -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,11 @@ 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
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
imports_granularity = "Crate"
|
||||
edition = "2021"
|
|
@ -16,12 +16,18 @@
|
|||
)]
|
||||
#![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},
|
||||
types::{PyAny, PyDict, PyList, PyString, PyType},
|
||||
wrap_pyfunction,
|
||||
};
|
||||
#[macro_use]
|
||||
|
@ -89,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) => {
|
||||
|
@ -111,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> {
|
||||
|
@ -128,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 {
|
||||
|
@ -139,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(),
|
||||
})
|
||||
|
@ -163,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"));
|
||||
|
@ -227,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.
|
||||
///
|
||||
|
@ -237,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
|
||||
///
|
||||
|
@ -268,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),
|
||||
|
@ -284,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
|
||||
///
|
||||
|
@ -295,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),
|
||||
|
@ -338,17 +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)")]
|
||||
#[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 {
|
||||
|
@ -358,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.
|
||||
///
|
||||
|
@ -366,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) };
|
||||
|
@ -389,7 +453,7 @@ impl JSONSchema {
|
|||
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,
|
||||
|
@ -410,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)
|
||||
|
@ -426,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)
|
||||
}
|
||||
|
||||
|
@ -439,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 {
|
||||
|
@ -454,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();
|
||||
|
@ -462,7 +531,7 @@ 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)?;
|
||||
|
|
|
@ -301,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()))
|
||||
}
|
||||
|
|
|
@ -275,3 +275,41 @@ 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
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
imports_granularity = "Crate"
|
||||
edition = "2021"
|
|
@ -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"]
|
||||
|
@ -53,7 +53,7 @@ once_cell = "1.19"
|
|||
parking_lot = "0.12"
|
||||
percent-encoding = "2.3"
|
||||
regex = "1.10"
|
||||
reqwest = { version = "0.11", features = [
|
||||
reqwest = { version = "0.12", features = [
|
||||
"blocking",
|
||||
"json",
|
||||
], default-features = false, optional = true }
|
||||
|
@ -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"
|
||||
|
|
|
@ -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);
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
imports_granularity = "Crate"
|
||||
edition = "2021"
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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,7 +32,7 @@ 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
|
||||
|
@ -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\"");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
let path = instance_path.push(name.to_string());
|
||||
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 {
|
||||
|
@ -526,7 +526,7 @@ 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![];
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -861,7 +861,7 @@ 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![];
|
||||
|
@ -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 {
|
||||
let path = instance_path.push(property.clone());
|
||||
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()
|
||||
|
@ -1042,7 +1042,7 @@ 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![];
|
||||
|
@ -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 {
|
||||
let path = instance_path.push(property.clone());
|
||||
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 {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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},
|
||||
|
@ -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();
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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>> {
|
||||
|
|
|
@ -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:
|
||||
|
@ -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()
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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) = ¤t.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) = ¤t.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())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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;
|
||||
|
@ -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 } => {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
imports_granularity = "Crate"
|
||||
edition = "2021"
|
Loading…
Reference in New Issue