Skip to content

Commit

Permalink
feat: Async retriever
Browse files Browse the repository at this point in the history
Signed-off-by: Dmitry Dygalo <[email protected]>
  • Loading branch information
Stranger6667 committed Feb 8, 2025
1 parent 6a529dd commit a842dad
Show file tree
Hide file tree
Showing 23 changed files with 1,761 additions and 373 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ jobs:
cache-all-crates: "true"
key: ${{ matrix.os }}

- run: cargo test --no-fail-fast
- run: cargo test --no-fail-fast --all-features

test-wasm:
name: Test on WASM
Expand Down
31 changes: 31 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,39 @@

## [Unreleased]

### Breaking Changes

- All builder methods on `ValidationOptions` now take ownership of `self` instead of `&mut self`.
This change enables better support for non-blocking retrieval of external resources during the process of building a validator.
Update your code to chain the builder methods instead of reusing the options instance:

```rust
// Before (0.28.x)
let mut options = jsonschema::options();
options.with_draft(Draft::Draft202012);
options.with_format("custom", my_format);
let validator = options.build(&schema)?;

// After (0.29.0)
let validator = jsonschema::options()
.with_draft(Draft::Draft202012)
.with_format("custom", my_format)
.build(&schema)?;

- The `Retrieve` trait's `retrieve` method now accepts URI references as `&Uri<String>` instead of `&Uri<&str>`.
This aligns with the async version and simplifies internal URI handling. The behavior and available methods remain the same, this is purely a type-level change.

```rust
// Before
fn retrieve(&self, uri: &Uri<&str>) -> Result<Value, Box<dyn std::error::Error + Send + Sync>>

// After
fn retrieve(&self, uri: &Uri<String>) -> Result<Value, Box<dyn std::error::Error + Send + Sync>>
```

### Added

- Support non-blocking retrieval for external resources during schema resolution via the new `resolve-async` feature. [#385](https://github.com/Stranger6667/jsonschema/issues/385)
- Re-export `referencing::Registry` as `jsonschema::Registry`.
- `ValidationOptions::with_registry` that allows for providing a predefined `referencing::Registry`. [#682](https://github.com/Stranger6667/jsonschema/issues/682)

Expand Down
38 changes: 38 additions & 0 deletions MIGRATION.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,43 @@
# Migration Guide

## Upgrading from 0.28.x to 0.29.0

The builder methods on `ValidationOptions` now take ownership of `self`. Change your code to use method chaining instead of reusing the options instance:

```rust
// Old (0.28.x)
let mut options = jsonschema::options();
options.with_draft(Draft::Draft202012);
options.with_format("custom", |s| s.len() > 3);
let validator = options.build(&schema)?;

// New (0.29.0)
let validator = jsonschema::options()
.with_draft(Draft::Draft202012)
.with_format("custom", |s| s.len() > 3)
.build(&schema)?;
```

If you implement the `Retrieve` trait, update the `uri` parameter type in the `retrieve` method:

```rust
// Old (0.28.x)
impl Retrieve for MyRetriever {
fn retrieve(&self, uri: &Uri<&str>) -> Result<Value, Box<dyn std::error::Error + Send + Sync>> {
// ...
}
}

// New (0.29.0)
impl Retrieve for MyRetriever {
fn retrieve(&self, uri: &Uri<String>) -> Result<Value, Box<dyn std::error::Error + Send + Sync>> {
// ...
}
}
```

This is a type-level change only; the behavior and available methods remain the same.

## Upgrading from 0.25.x to 0.26.0

The `Validator::validate` method now returns `Result<(), ValidationError<'i>>` instead of an error iterator. If you need to iterate over all validation errors, use the new `Validator::iter_errors` method.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ See more usage examples in the [documentation](https://docs.rs/jsonschema).

- 📚 Full support for popular JSON Schema drafts
- 🔧 Custom keywords and format validators
- 🌐 Remote reference fetching (network/file)
- 🌐 Blocking & non-blocking remote reference fetching (network/file)
- 🎨 `Basic` output style as per JSON Schema spec
- ✨ Meta-schema validation for schema documents
- 🔗 Bindings for [Python](https://github.com/Stranger6667/jsonschema/tree/master/crates/jsonschema-py)
Expand Down
20 changes: 10 additions & 10 deletions crates/jsonschema-py/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -419,13 +419,13 @@ fn make_options(
) -> PyResult<jsonschema::ValidationOptions> {
let mut options = jsonschema::options();
if let Some(raw_draft_version) = draft {
options.with_draft(get_draft(raw_draft_version)?);
options = options.with_draft(get_draft(raw_draft_version)?);
}
if let Some(yes) = validate_formats {
options.should_validate_formats(yes);
options = options.should_validate_formats(yes);
}
if let Some(yes) = ignore_unknown_formats {
options.should_ignore_unknown_formats(yes);
options = options.should_ignore_unknown_formats(yes);
}
if let Some(formats) = formats {
for (name, callback) in formats.iter() {
Expand All @@ -442,9 +442,10 @@ fn make_options(
callback.call(py, (value,), None)?.is_truthy(py)
})
};
options.with_format(
name.to_string(),
move |value: &str| match call_py_callback(value) {
options =
options.with_format(name.to_string(), move |value: &str| match call_py_callback(
value,
) {
Ok(r) => r,
Err(e) => {
LAST_FORMAT_ERROR.with(|last| {
Expand All @@ -454,16 +455,15 @@ fn make_options(
// Should be caught
panic!("Format checker failed")
}
},
);
});
}
}
if let Some(retriever) = retriever {
let func = into_retriever(retriever)?;
options.with_retriever(Retriever { func });
options = options.with_retriever(Retriever { func });
}
if let Some(registry) = registry {
options.with_registry(registry.inner.clone());
options = options.with_registry(registry.inner.clone());
}
Ok(options)
}
Expand Down
6 changes: 2 additions & 4 deletions crates/jsonschema-py/src/registry.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
use std::sync::Arc;

use jsonschema::Resource;
use pyo3::{exceptions::PyValueError, prelude::*};

Expand Down Expand Up @@ -29,7 +27,7 @@ impl Registry {

if let Some(retriever) = retriever {
let func = into_retriever(retriever)?;
options = options.retriever(Arc::new(Retriever { func }));
options = options.retriever(Retriever { func });
}

let pairs = resources.try_iter()?.map(|item| {
Expand All @@ -45,7 +43,7 @@ impl Registry {
let pairs: Result<Vec<_>, PyErr> = pairs.collect();

let registry = options
.try_from_resources(pairs?.into_iter())
.build(pairs?)
.map_err(|e| PyValueError::new_err(e.to_string()))?;

Ok(Registry { inner: registry })
Expand Down
5 changes: 4 additions & 1 deletion crates/jsonschema-py/src/retriever.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ pub(crate) struct Retriever<T: Fn(&str) -> PyResult<Value>> {
}

impl<T: Send + Sync + Fn(&str) -> PyResult<Value>> Retrieve for Retriever<T> {
fn retrieve(&self, uri: &Uri<&str>) -> Result<Value, Box<dyn std::error::Error + Send + Sync>> {
fn retrieve(
&self,
uri: &Uri<String>,
) -> Result<Value, Box<dyn std::error::Error + Send + Sync>> {
Ok((self.func)(uri.as_str())?)
}
}
Expand Down
8 changes: 8 additions & 0 deletions crates/jsonschema-referencing/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@ parking_lot = "0.12.3"
percent-encoding = "2.3.1"
serde_json.workspace = true

async-trait = { version = "0.1.86", optional = true }
futures = { version = "0.3.31", optional = true }

[features]
default = []
retrieve-async = ["dep:async-trait", "dep:futures"]

[lints]
workspace = true

Expand All @@ -26,6 +33,7 @@ codspeed-criterion-compat = { version = "2.7", default-features = false }
criterion = { version = "0.5", default-features = false }
referencing_testsuite = { package = "jsonschema-referencing-testsuite", path = "../jsonschema-referencing-testsuite/" }
test-case = "3.3.1"
tokio = { version = "1", features = ["macros", "rt"] }

[[bench]]
harness = false
Expand Down
52 changes: 20 additions & 32 deletions crates/jsonschema-referencing/src/anchors/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -243,14 +243,11 @@ mod tests {
},
}));

let registry = Registry::try_from_resources(
[
("http://example.com".to_string(), root.clone()),
("http://example.com/foo/".to_string(), true_resource),
("http://example.com/foo/bar".to_string(), root.clone()),
]
.into_iter(),
)
let registry = Registry::try_from_resources([
("http://example.com".to_string(), root.clone()),
("http://example.com/foo/".to_string(), true_resource),
("http://example.com/foo/bar".to_string(), root.clone()),
])
.expect("Invalid resources");
let resolver = registry
.try_resolver("http://example.com")
Expand Down Expand Up @@ -287,14 +284,11 @@ mod tests {
},
}));

let registry = Registry::try_from_resources(
[
("http://example.com".to_string(), two.clone()),
("http://example.com/foo/".to_string(), one),
("http://example.com/foo/bar".to_string(), two.clone()),
]
.into_iter(),
)
let registry = Registry::try_from_resources([
("http://example.com".to_string(), two.clone()),
("http://example.com/foo/".to_string(), one),
("http://example.com/foo/bar".to_string(), two.clone()),
])
.expect("Invalid resources");
let resolver = registry
.try_resolver("http://example.com")
Expand Down Expand Up @@ -397,14 +391,11 @@ mod tests {
},
}));

let registry = Registry::try_from_resources(
vec![
("http://example.com".to_string(), root.clone()),
("http://example.com/foo/".to_string(), true_resource),
("http://example.com/foo/bar".to_string(), root.clone()),
]
.into_iter(),
)
let registry = Registry::try_from_resources(vec![
("http://example.com".to_string(), root.clone()),
("http://example.com/foo/".to_string(), true_resource),
("http://example.com/foo/bar".to_string(), root.clone()),
])
.expect("Invalid resources");

let resolver = registry
Expand Down Expand Up @@ -442,14 +433,11 @@ mod tests {
}));
let three = Draft::Draft201909.create_resource(json!({"$recursiveAnchor": false}));

let registry = Registry::try_from_resources(
vec![
("http://example.com".to_string(), three),
("http://example.com/foo/".to_string(), two.clone()),
("http://example.com/foo/bar".to_string(), one),
]
.into_iter(),
)
let registry = Registry::try_from_resources(vec![
("http://example.com".to_string(), three),
("http://example.com/foo/".to_string(), two.clone()),
("http://example.com/foo/bar".to_string(), one),
])
.expect("Invalid resources");

let resolver = registry
Expand Down
1 change: 1 addition & 0 deletions crates/jsonschema-referencing/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ impl std::error::Error for Error {
}
}

/// Errors that can occur during URI handling.
#[derive(Debug)]
pub enum UriError {
Parse {
Expand Down
3 changes: 3 additions & 0 deletions crates/jsonschema-referencing/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,6 @@ pub use retriever::{DefaultRetriever, Retrieve};
pub(crate) use segments::Segments;
pub use specification::Draft;
pub use vocabularies::{Vocabulary, VocabularySet};

#[cfg(feature = "retrieve-async")]
pub use retriever::AsyncRetrieve;
3 changes: 3 additions & 0 deletions crates/jsonschema-referencing/src/meta.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
//! Built-in JSON Schema meta-schemas.
//!
//! This module provides access to the official JSON Schema meta-schemas for different draft versions.
use std::sync::Arc;

use once_cell::sync::Lazy;
Expand Down
Loading

0 comments on commit a842dad

Please sign in to comment.