Compare commits
4 Commits
36d87ac1ca
...
7b082597f2
Author | SHA1 | Date |
---|---|---|
Sam Roberts | 7b082597f2 | |
Joel Natividad | 6cf82328e3 | |
Dmitry Dygalo | 9aae87e573 | |
Dmitry Dygalo | 194a0b0db7 |
|
@ -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,14 @@ jobs:
|
|||
- run: python -m pip install tox
|
||||
working-directory: ./bindings/python
|
||||
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
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: python-${{ matrix.python-version }}-${{ matrix.os }}-cargo-cache
|
||||
workspaces: |
|
||||
jsonschema
|
||||
bindings/python
|
||||
key: ${{ matrix.python-version }}-${{ matrix.os }}
|
||||
|
||||
- name: Run ${{ matrix.python }} tox job
|
||||
run: tox -e py
|
||||
|
@ -173,31 +153,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 +202,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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
- 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.
|
||||
|
||||
## [0.17.1] - 2023-07-05
|
||||
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
imports_granularity = "Crate"
|
||||
edition = "2021"
|
|
@ -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",
|
||||
|
|
|
@ -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,48 @@
|
|||
from typing import Any, TypeVar
|
||||
from collections.abc import Iterator
|
||||
|
||||
_SchemaT = TypeVar('_SchemaT', bool, dict[str, Any])
|
||||
|
||||
_SchemaT = TypeVar("_SchemaT", bool, dict[str, Any])
|
||||
|
||||
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,
|
||||
) -> 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,
|
||||
) -> 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,
|
||||
) -> 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,
|
||||
) -> 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,
|
||||
) -> "JSONSchema":
|
||||
pass
|
||||
|
||||
def is_valid(self, instance: Any) -> bool:
|
||||
|
@ -57,13 +54,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"
|
|
@ -0,0 +1,2 @@
|
|||
imports_granularity = "Crate"
|
||||
edition = "2021"
|
|
@ -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 }
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
imports_granularity = "Crate"
|
||||
edition = "2021"
|
|
@ -332,8 +332,8 @@ mod tests {
|
|||
// Define a custom validator that verifies the object's keys consist of
|
||||
// only ASCII representable characters.
|
||||
struct CustomObjectValidator;
|
||||
impl<'instance> CustomKeywordValidator<'instance, '_> for CustomObjectValidator {
|
||||
fn validate(
|
||||
impl CustomKeywordValidator for CustomObjectValidator {
|
||||
fn validate<'instance>(
|
||||
&mut self,
|
||||
instance: &'instance Value,
|
||||
instance_path: JSONPointer,
|
||||
|
@ -418,8 +418,8 @@ mod tests {
|
|||
// Define a custom keyword validator that overrides "minimum"
|
||||
// so that "minimum" may apply to "currency"-formatted strings as well
|
||||
struct CustomMinimumValidator;
|
||||
impl<'instance> CustomKeywordValidator<'instance, '_> for CustomMinimumValidator {
|
||||
fn validate(
|
||||
impl CustomKeywordValidator for CustomMinimumValidator {
|
||||
fn validate<'instance>(
|
||||
&mut self,
|
||||
instance: &'instance Value,
|
||||
instance_path: JSONPointer,
|
||||
|
@ -458,16 +458,14 @@ mod tests {
|
|||
let mut valid = true;
|
||||
if let Some(schema) = schema.as_object() {
|
||||
if let Some(format) = schema.get("format") {
|
||||
if format == "currency" {
|
||||
if currency_format_checker(instance) {
|
||||
// all preconditions for minimum applying are met
|
||||
let as_f64 = instance
|
||||
.parse::<f64>()
|
||||
.expect("format validated by regex checker");
|
||||
println!("1 {:#?} {:#?}", as_f64, limit.as_f64().unwrap());
|
||||
valid = !NumCmp::num_lt(as_f64, limit.as_f64().unwrap());
|
||||
println!("valid {:#?}", valid);
|
||||
}
|
||||
if format == "currency" && currency_format_checker(instance) {
|
||||
// all preconditions for minimum applying are met
|
||||
let as_f64 = instance
|
||||
.parse::<f64>()
|
||||
.expect("format validated by regex checker");
|
||||
println!("1 {:#?} {:#?}", as_f64, limit.as_f64().unwrap());
|
||||
valid = !NumCmp::num_lt(as_f64, limit.as_f64().unwrap());
|
||||
println!("valid {:#?}", valid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -491,7 +489,6 @@ mod tests {
|
|||
}
|
||||
|
||||
fn is_valid(&mut self, instance: &Value, subschema: &Value, schema: &Value) -> bool {
|
||||
let subschema: &Value = &subschema;
|
||||
let limit = match subschema {
|
||||
Value::Number(limit) => limit,
|
||||
_ => return false,
|
||||
|
@ -513,16 +510,14 @@ mod tests {
|
|||
let mut valid = true;
|
||||
if let Some(schema) = schema.as_object() {
|
||||
if let Some(format) = schema.get("format") {
|
||||
if format == "currency" {
|
||||
if currency_format_checker(instance) {
|
||||
// all preconditions for minimum applying are met
|
||||
let as_f64 = instance
|
||||
.parse::<f64>()
|
||||
.expect("format validated by regex checker");
|
||||
println!("1 {:#?} {:#?}", as_f64, limit.as_f64().unwrap());
|
||||
valid = !NumCmp::num_lt(as_f64, limit.as_f64().unwrap());
|
||||
println!("valid {:#?}", valid);
|
||||
}
|
||||
if format == "currency" && currency_format_checker(instance) {
|
||||
// all preconditions for minimum applying are met
|
||||
let as_f64 = instance
|
||||
.parse::<f64>()
|
||||
.expect("format validated by regex checker");
|
||||
println!("1 {:#?} {:#?}", as_f64, limit.as_f64().unwrap());
|
||||
valid = !NumCmp::num_lt(as_f64, limit.as_f64().unwrap());
|
||||
println!("valid {:#?}", valid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -531,7 +526,7 @@ mod tests {
|
|||
// in all other cases, the "minimum" keyword should not apply
|
||||
_ => true,
|
||||
};
|
||||
return valid;
|
||||
valid
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -588,39 +583,51 @@ mod tests {
|
|||
// Define a custom keyword validator that wraps "minimum"
|
||||
// but maintains a counter of how many times the validator was applied.
|
||||
struct CountingValidator {
|
||||
count: u32,
|
||||
count: Arc<Mutex<i64>>,
|
||||
}
|
||||
impl<'instance> CustomKeywordValidator<'instance, '_> for CountingValidator {
|
||||
fn validate(
|
||||
|
||||
impl CountingValidator {
|
||||
fn increment(&mut self, amount: i64) {
|
||||
let mut count = self.count.lock().expect("Lock is poisoned");
|
||||
*count += amount;
|
||||
}
|
||||
}
|
||||
|
||||
impl CustomKeywordValidator for CountingValidator {
|
||||
fn validate<'instance>(
|
||||
&mut self,
|
||||
instance: &'instance Value,
|
||||
instance_path: JSONPointer,
|
||||
_: &'instance Value,
|
||||
_: JSONPointer,
|
||||
subschema: Arc<Value>,
|
||||
subschema_path: JSONPointer,
|
||||
schema: Arc<Value>,
|
||||
_: JSONPointer,
|
||||
_: Arc<Value>,
|
||||
) -> ErrorIterator<'instance> {
|
||||
let amount = match &*subschema {
|
||||
Value::Number(x) => x.as_i64().expect("countme value must be integer"),
|
||||
_ => panic!("Validator requires numeric values"),
|
||||
};
|
||||
self.count += amount as u32;
|
||||
self.increment(amount);
|
||||
Box::new(None.into_iter())
|
||||
}
|
||||
|
||||
fn is_valid(&mut self, instance: &Value, subschema: &Value, schema: &Value) -> bool {
|
||||
fn is_valid(&mut self, _: &Value, subschema: &Value, _: &Value) -> bool {
|
||||
let amount = match subschema {
|
||||
Value::Number(x) => x.as_i64().expect("countme value must be integer"),
|
||||
_ => return false,
|
||||
};
|
||||
self.count += amount as u32;
|
||||
self.increment(amount);
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
// define compilation options that include the custom format and the overridden keyword
|
||||
let validator = Arc::new(Mutex::new(Box::new(CountingValidator { count: 0 })));
|
||||
let count = Arc::new(Mutex::new(0));
|
||||
let boxed: Box<dyn CustomKeywordValidator> = Box::new(CountingValidator {
|
||||
count: count.clone(),
|
||||
});
|
||||
let validator = Arc::new(Mutex::new(boxed));
|
||||
let mut options = JSONSchema::options();
|
||||
let options = options.with_custom_keyword("countme", validator.clone());
|
||||
let options = options.with_custom_keyword("countme", validator);
|
||||
|
||||
// Define a schema that includes the custom keyword and therefore should increase the count
|
||||
let schema = json!({ "countme": 3, "type": "string" });
|
||||
|
@ -628,11 +635,11 @@ mod tests {
|
|||
|
||||
// Because the schema has "countme" in it, whenever we run validation we should expect the validator's count to increase
|
||||
let instance_ok = json!("i am a string");
|
||||
assert_eq!(validator.lock().expect("lock").count, 0);
|
||||
assert_eq!(*count.lock().expect("Lock is poinsoned"), 0);
|
||||
assert!(compiled.validate(&instance_ok).is_err());
|
||||
assert_eq!(validator.lock().expect("lock").count, 3);
|
||||
assert_eq!(*count.lock().expect("Lock is poinsoned"), 3);
|
||||
assert!(!compiled.is_valid(&instance_ok));
|
||||
assert_eq!(validator.lock().expect("lock").count, 6);
|
||||
assert_eq!(*count.lock().expect("Lock is poinsoned"), 6);
|
||||
|
||||
// TODO compile a schema that doesn't have "countme" and ensure count does not increase
|
||||
}
|
||||
|
|
|
@ -281,7 +281,7 @@ pub struct CompilationOptions {
|
|||
ignore_unknown_formats: bool,
|
||||
custom_keywords: AHashMap<
|
||||
String, // TODO<samgqroberts> 2024-04-13 should this also be a &'static str
|
||||
Arc<Mutex<Box<dyn for<'instance, 'schema> CustomKeywordValidator<'instance, 'schema>>>>,
|
||||
Arc<Mutex<Box<dyn CustomKeywordValidator>>>,
|
||||
>,
|
||||
}
|
||||
|
||||
|
@ -689,14 +689,7 @@ impl CompilationOptions {
|
|||
pub fn with_custom_keyword<T>(
|
||||
&mut self,
|
||||
keyword: T,
|
||||
definition: Arc<
|
||||
Mutex<
|
||||
Box<
|
||||
dyn for<'instance, 'schema> CustomKeywordValidator<'instance, 'schema>
|
||||
+ 'static,
|
||||
>,
|
||||
>,
|
||||
>,
|
||||
definition: Arc<Mutex<Box<dyn CustomKeywordValidator + 'static>>>,
|
||||
) -> &mut Self
|
||||
where
|
||||
T: Into<String>,
|
||||
|
@ -708,9 +701,7 @@ impl CompilationOptions {
|
|||
pub(crate) fn get_custom_keyword_definition(
|
||||
&self,
|
||||
keyword: &str,
|
||||
) -> Option<
|
||||
&Arc<Mutex<Box<dyn for<'instance, 'schema> CustomKeywordValidator<'instance, 'schema>>>>,
|
||||
> {
|
||||
) -> Option<&Arc<Mutex<Box<dyn CustomKeywordValidator>>>> {
|
||||
self.custom_keywords.get(keyword)
|
||||
}
|
||||
}
|
||||
|
@ -731,7 +722,7 @@ impl fmt::Debug for CompilationOptions {
|
|||
}
|
||||
|
||||
/// Trait that allows implementing custom validation for keywords.
|
||||
pub trait CustomKeywordValidator<'instance, 'schema>: Send + Sync {
|
||||
pub trait CustomKeywordValidator: Send + Sync {
|
||||
/// Validate [instance](serde_json::Value) according to a custom specification
|
||||
///
|
||||
/// A custom keyword validator may be used when a validation that cannot, or
|
||||
|
@ -739,7 +730,7 @@ pub trait CustomKeywordValidator<'instance, 'schema>: Send + Sync {
|
|||
///
|
||||
/// The custom validation is applied in addition to the JSON schema validation.
|
||||
/// Validate an instance returning any and all detected validation errors
|
||||
fn validate(
|
||||
fn validate<'instance>(
|
||||
&mut self,
|
||||
instance: &'instance serde_json::Value,
|
||||
instance_path: JSONPointer,
|
||||
|
@ -748,9 +739,9 @@ pub trait CustomKeywordValidator<'instance, 'schema>: Send + Sync {
|
|||
schema: Arc<serde_json::Value>,
|
||||
) -> ErrorIterator<'instance>;
|
||||
/// Determine if an instance is valid
|
||||
fn is_valid(
|
||||
fn is_valid<'schema>(
|
||||
&mut self,
|
||||
instance: &'instance serde_json::Value,
|
||||
instance: &serde_json::Value,
|
||||
subschema: &'schema serde_json::Value,
|
||||
schema: &'schema serde_json::Value,
|
||||
) -> bool;
|
||||
|
|
|
@ -13,13 +13,12 @@ pub(crate) struct CompiledCustomKeywordValidator {
|
|||
schema: Arc<Value>,
|
||||
subschema: Arc<Value>,
|
||||
subschema_path: JSONPointer,
|
||||
validator:
|
||||
Arc<Mutex<Box<dyn for<'instance, 'schema> CustomKeywordValidator<'instance, 'schema>>>>,
|
||||
validator: Arc<Mutex<Box<dyn CustomKeywordValidator>>>,
|
||||
}
|
||||
|
||||
impl Display for CompiledCustomKeywordValidator {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "")
|
||||
fn fmt(&self, _: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -48,9 +47,7 @@ impl Validate for CompiledCustomKeywordValidator {
|
|||
pub(crate) fn compile_custom_keyword_validator<'a>(
|
||||
context: &CompilationContext,
|
||||
keyword: impl Into<PathChunk>,
|
||||
validator: Arc<
|
||||
Mutex<Box<dyn for<'instance, 'schema> CustomKeywordValidator<'instance, 'schema>>>,
|
||||
>,
|
||||
validator: Arc<Mutex<Box<dyn CustomKeywordValidator>>>,
|
||||
subschema: Value,
|
||||
schema: Value,
|
||||
) -> CompilationResult<'a> {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
imports_granularity = "Crate"
|
||||
edition = "2021"
|
Loading…
Reference in New Issue