Compare commits

...

4 Commits

Author SHA1 Message Date
Sam Roberts 7b082597f2
Merge 194a0b0db7 into 6cf82328e3 2024-04-14 21:17:51 +02:00
Joel Natividad 6cf82328e3 chore: bump reqwest from 0.11 to 0.12 2024-04-14 21:01:58 +02:00
Dmitry Dygalo 9aae87e573 build: Update builds
Signed-off-by: Dmitry Dygalo <dmitry@dygalo.dev>
2024-04-14 18:24:03 +02:00
Dmitry Dygalo 194a0b0db7
fix: tests
Signed-off-by: Dmitry Dygalo <dmitry@dygalo.dev>
2024-04-14 16:45:52 +02:00
17 changed files with 311 additions and 274 deletions

View File

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

View File

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

View File

@ -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

View File

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

View File

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

View File

@ -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"

View File

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

View File

@ -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

View File

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

View File

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

View File

@ -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 }

2
jsonschema/rustfmt.toml Normal file
View File

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

View File

@ -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
}

View File

@ -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;

View File

@ -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> {

View File

@ -1,33 +1,76 @@
use std::{
error::Error,
fs::File,
io::BufReader,
path::{Path, PathBuf},
process,
};
use clap::Parser;
use jsonschema::JSONSchema;
type BoxErrorResult<T> = Result<T, Box<dyn Error>>;
#[derive(Parser)]
#[command(name = "jsonschema")]
struct Cli {
/// A path to a JSON instance (i.e. filename.json) to validate (may be specified multiple times).
#[arg(short = 'i', long = "instance")]
instances: Option<Vec<PathBuf>>,
/// The JSON Schema to validate with (i.e. schema.json).
#[arg(value_parser, required_unless_present("version"))]
schema: Option<PathBuf>,
/// Show program's version number and exit.
#[arg(short = 'v', long = "version")]
version: bool,
#[cfg(not(feature = "cli"))]
fn main() {
eprintln!("`jsonschema` CLI is only available with the `cli` feature");
std::process::exit(1);
}
pub fn main() -> BoxErrorResult<()> {
#[cfg(feature = "cli")]
fn main() -> Result<(), Box<dyn std::error::Error>> {
use std::{
fs::File,
io::BufReader,
path::{Path, PathBuf},
process,
};
use clap::Parser;
use jsonschema::JSONSchema;
#[derive(Parser)]
#[command(name = "jsonschema")]
struct Cli {
/// A path to a JSON instance (i.e. filename.json) to validate (may be specified multiple times).
#[arg(short = 'i', long = "instance")]
instances: Option<Vec<PathBuf>>,
/// The JSON Schema to validate with (i.e. schema.json).
#[arg(value_parser, required_unless_present("version"))]
schema: Option<PathBuf>,
/// Show program's version number and exit.
#[arg(short = 'v', long = "version")]
version: bool,
}
fn read_json(path: &Path) -> serde_json::Result<serde_json::Value> {
let file = File::open(path).expect("Failed to open file");
let reader = BufReader::new(file);
serde_json::from_reader(reader)
}
fn validate_instances(
instances: &[PathBuf],
schema_path: PathBuf,
) -> Result<bool, Box<dyn std::error::Error>> {
let mut success = true;
let schema_json = read_json(&schema_path)?;
match JSONSchema::compile(&schema_json) {
Ok(schema) => {
for instance in instances {
let instance_json = read_json(instance)?;
let validation = schema.validate(&instance_json);
let filename = instance.to_string_lossy();
match validation {
Ok(_) => println!("{} - VALID", filename),
Err(errors) => {
success = false;
println!("{} - INVALID. Errors:", filename);
for (i, e) in errors.enumerate() {
println!("{}. {}", i + 1, e);
}
}
}
}
}
Err(error) => {
println!("Schema is invalid. Error: {}", error);
success = false;
}
}
Ok(success)
}
let config = Cli::parse();
if config.version {
@ -48,40 +91,3 @@ pub fn main() -> BoxErrorResult<()> {
Ok(())
}
fn read_json(path: &Path) -> serde_json::Result<serde_json::Value> {
let file = File::open(path).expect("Failed to open file");
let reader = BufReader::new(file);
serde_json::from_reader(reader)
}
fn validate_instances(instances: &[PathBuf], schema_path: PathBuf) -> BoxErrorResult<bool> {
let mut success = true;
let schema_json = read_json(&schema_path)?;
match JSONSchema::compile(&schema_json) {
Ok(schema) => {
for instance in instances {
let instance_json = read_json(instance)?;
let validation = schema.validate(&instance_json);
let filename = instance.to_string_lossy();
match validation {
Ok(_) => println!("{} - VALID", filename),
Err(errors) => {
success = false;
println!("{} - INVALID. Errors:", filename);
for (i, e) in errors.enumerate() {
println!("{}. {}", i + 1, e);
}
}
}
}
}
Err(error) => {
println!("Schema is invalid. Error: {}", error);
success = false;
}
}
Ok(success)
}

View File

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