Skip to content

Commit 470fb81

Browse files
committed
chore: Split Registry
Signed-off-by: Dmitry Dygalo <dmitry@dygalo.dev>
1 parent bf78920 commit 470fb81

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+3856
-2032
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@
22

33
## [Unreleased]
44

5+
### Breaking Changes
6+
7+
- Registry construction now uses an explicit prepare step, and `with_registry` now borrows the prepared registry. `ValidationOptions::with_resource` and `ValidationOptions::with_resources` were removed in favor of building a `Registry` first. See the [Migration Guide](MIGRATION.md) for the details.
8+
9+
### Performance
10+
11+
- Avoid registry clones and document clones during validator construction. This improves real-world schema compilation by roughly 10-20% in internal benchmarks.
12+
513
### Fixed
614

715
- Incorrect handling of `duration` format when hours and seconds appear without minutes, or years and days without months.

MIGRATION.md

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,74 @@
11
# Migration Guide
22

3+
## Upgrading from 0.45.x to 0.46.0
4+
5+
Registry construction is now explicit: add shared schemas first, then call
6+
`prepare()` to build a reusable registry. Validators no longer take ownership of
7+
that registry; pass it by reference with `with_registry(&registry)`.
8+
`ValidationOptions::with_resource` and `ValidationOptions::with_resources` were
9+
removed in favor of building a `Registry` first. For cases with multiple shared
10+
schemas, `extend([...])` is the batch form of `add(...)`.
11+
12+
```rust
13+
// Old (0.45.x)
14+
use jsonschema::{Registry, Resource};
15+
16+
// Inline shared schema
17+
let validator = jsonschema::options()
18+
.with_resource(
19+
"https://example.com/schema",
20+
Resource::from_contents(shared_schema),
21+
)
22+
.build(&schema)?;
23+
24+
// Multiple shared schemas
25+
let validator = jsonschema::options()
26+
.with_resources([
27+
(
28+
"https://example.com/schema-1",
29+
Resource::from_contents(schema_1),
30+
),
31+
(
32+
"https://example.com/schema-2",
33+
Resource::from_contents(schema_2),
34+
),
35+
].into_iter())
36+
.build(&schema)?;
37+
38+
// Prebuilt registry
39+
let registry = Registry::try_from_resources([
40+
(
41+
"https://example.com/schema",
42+
Resource::from_contents(shared_schema),
43+
),
44+
])?;
45+
let validator = jsonschema::options()
46+
.with_registry(registry)
47+
.build(&schema)?;
48+
49+
// New (0.46.0)
50+
use jsonschema::Registry;
51+
52+
// Shared registry + borrowed validator build
53+
let registry = Registry::new()
54+
.add("https://example.com/schema", shared_schema)?
55+
.prepare()?;
56+
let validator = jsonschema::options()
57+
.with_registry(&registry)
58+
.build(&schema)?;
59+
60+
// Multiple shared schemas
61+
let registry = Registry::new()
62+
.extend([
63+
("https://example.com/schema-1", schema_1),
64+
("https://example.com/schema-2", schema_2),
65+
])?
66+
.prepare()?;
67+
let validator = jsonschema::options()
68+
.with_registry(&registry)
69+
.build(&schema)?;
70+
```
71+
372
## Upgrading from 0.38.x to 0.39.0
473

574
### Custom keyword API simplified

crates/jsonschema-cli/src/main.rs

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -444,10 +444,10 @@ fn path_to_uri(path: &std::path::Path) -> String {
444444
result
445445
}
446446

447-
fn options_for_schema(
447+
fn options_for_schema<'a>(
448448
schema_path: &Path,
449449
http_options: Option<&jsonschema::HttpOptions>,
450-
) -> Result<jsonschema::ValidationOptions, Box<dyn std::error::Error>> {
450+
) -> Result<jsonschema::ValidationOptions<'a>, Box<dyn std::error::Error>> {
451451
let base_uri = path_to_uri(schema_path);
452452
let base_uri = referencing::uri::from_str(&base_uri)?;
453453
let mut options = jsonschema::options().with_base_uri(base_uri);
@@ -695,16 +695,30 @@ fn run_bundle(args: BundleArgs) -> ExitCode {
695695
Err(error) => return fail_with_error(error),
696696
};
697697

698+
let mut registry = if let Some(http_opts) = http_options.as_ref() {
699+
let retriever = match jsonschema::HttpRetriever::new(http_opts) {
700+
Ok(retriever) => retriever,
701+
Err(error) => return fail_with_error(error),
702+
};
703+
jsonschema::Registry::new().retriever(retriever)
704+
} else {
705+
jsonschema::Registry::new()
706+
};
698707
for (uri, path) in &resources {
699708
let resource_json = match read_json(path) {
700709
Ok(value) => value,
701710
Err(error) => return fail_with_error(error),
702711
};
703-
opts = opts.with_resource(
704-
uri.as_str(),
705-
referencing::Resource::from_contents(resource_json),
706-
);
712+
registry = match registry.add(uri, resource_json) {
713+
Ok(registry) => registry,
714+
Err(error) => return fail_with_error(error),
715+
};
707716
}
717+
let registry = match registry.prepare() {
718+
Ok(registry) => registry,
719+
Err(error) => return fail_with_error(error),
720+
};
721+
opts = opts.with_registry(&registry);
708722

709723
match opts.bundle(&schema_json) {
710724
Ok(bundled) => {

crates/jsonschema-py/src/lib.rs

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -832,19 +832,19 @@ impl jsonschema::Keyword for CustomKeyword {
832832
}
833833
}
834834

835-
fn make_options(
835+
fn make_options<'a>(
836836
draft: Option<u8>,
837-
formats: Option<&Bound<'_, PyDict>>,
837+
formats: Option<&Bound<'a, PyDict>>,
838838
validate_formats: Option<bool>,
839839
ignore_unknown_formats: Option<bool>,
840-
retriever: Option<&Bound<'_, PyAny>>,
841-
registry: Option<&registry::Registry>,
840+
retriever: Option<&Bound<'a, PyAny>>,
841+
registry: Option<&'a registry::Registry>,
842842
base_uri: Option<String>,
843-
pattern_options: Option<&Bound<'_, PyAny>>,
844-
email_options: Option<&Bound<'_, PyAny>>,
845-
http_options: Option<&Bound<'_, PyAny>>,
846-
keywords: Option<&Bound<'_, PyDict>>,
847-
) -> PyResult<jsonschema::ValidationOptions> {
843+
pattern_options: Option<&Bound<'a, PyAny>>,
844+
email_options: Option<&Bound<'a, PyAny>>,
845+
http_options: Option<&Bound<'a, PyAny>>,
846+
keywords: Option<&Bound<'a, PyDict>>,
847+
) -> PyResult<jsonschema::ValidationOptions<'a>> {
848848
let mut options = jsonschema::options();
849849
if let Some(raw_draft_version) = draft {
850850
options = options.with_draft(get_draft(raw_draft_version)?);
@@ -890,7 +890,7 @@ fn make_options(
890890
options = options.with_retriever(Retriever { func });
891891
}
892892
if let Some(registry) = registry {
893-
options = options.with_registry(registry.inner.clone());
893+
options = options.with_registry(&registry.inner);
894894
}
895895
if let Some(base_uri) = base_uri {
896896
options = options.with_base_uri(base_uri);
@@ -2021,7 +2021,7 @@ mod meta {
20212021
let schema = crate::ser::to_value(schema)?;
20222022
let result = if let Some(registry) = registry {
20232023
jsonschema::meta::options()
2024-
.with_registry(registry.inner.clone())
2024+
.with_registry(&registry.inner)
20252025
.validate(&schema)
20262026
} else {
20272027
jsonschema::meta::validate(&schema)
@@ -2070,7 +2070,7 @@ mod meta {
20702070
let schema = crate::ser::to_value(schema)?;
20712071
let result = if let Some(registry) = registry {
20722072
jsonschema::meta::options()
2073-
.with_registry(registry.inner.clone())
2073+
.with_registry(&registry.inner)
20742074
.validate(&schema)
20752075
} else {
20762076
jsonschema::meta::validate(&schema)

crates/jsonschema-py/src/registry.rs

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
1-
use jsonschema::Resource;
21
use pyo3::{exceptions::PyValueError, prelude::*};
32

43
use crate::{get_draft, retriever::into_retriever, to_value, Retriever};
54

65
/// A registry of JSON Schema resources, each identified by their canonical URIs.
76
#[pyclass]
87
pub(crate) struct Registry {
9-
pub(crate) inner: jsonschema::Registry,
8+
pub(crate) inner: jsonschema::Registry<'static>,
109
}
1110

1211
#[pymethods]
@@ -19,30 +18,29 @@ impl Registry {
1918
draft: Option<u8>,
2019
retriever: Option<&Bound<'_, PyAny>>,
2120
) -> PyResult<Self> {
22-
let mut options = jsonschema::Registry::options();
21+
let mut builder = jsonschema::Registry::new();
2322

2423
if let Some(draft) = draft {
25-
options = options.draft(get_draft(draft)?);
24+
builder = builder.draft(get_draft(draft)?);
2625
}
2726

2827
if let Some(retriever) = retriever {
2928
let func = into_retriever(retriever)?;
30-
options = options.retriever(Retriever { func });
29+
builder = builder.retriever(Retriever { func });
3130
}
3231

33-
let pairs = resources.try_iter()?.map(|item| {
32+
for item in resources.try_iter()? {
3433
let pair = item?.unbind();
3534
let (key, value) = pair.extract::<(Bound<PyAny>, Bound<PyAny>)>(py)?;
3635
let uri = key.extract::<String>()?;
3736
let schema = to_value(&value)?;
38-
let resource = Resource::from_contents(schema);
39-
Ok((uri, resource))
40-
});
41-
42-
let pairs: Result<Vec<_>, PyErr> = pairs.collect();
37+
builder = builder
38+
.add(uri, schema)
39+
.map_err(|e| PyValueError::new_err(e.to_string()))?;
40+
}
4341

44-
let registry = options
45-
.build(pairs?)
42+
let registry = builder
43+
.prepare()
4644
.map_err(|e| PyValueError::new_err(e.to_string()))?;
4745

4846
Ok(Registry { inner: registry })

crates/jsonschema-py/tests-py/test_bundle.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,53 @@ def test_bundle_validates_identically():
4343
assert not validator.is_valid({"age": 30})
4444

4545

46+
def test_bundle_with_registry_and_explicit_draft4_legacy_id_root():
47+
root = {
48+
"id": "urn:root",
49+
"type": "object",
50+
"properties": {"value": {"$ref": "urn:string"}},
51+
"required": ["value"],
52+
}
53+
registry = jsonschema_rs.Registry(
54+
resources=[("urn:string", {"type": "string"})],
55+
draft=jsonschema_rs.Draft4,
56+
)
57+
58+
bundled = jsonschema_rs.bundle(root, registry=registry, draft=jsonschema_rs.Draft4)
59+
60+
assert bundled["properties"]["value"]["$ref"] == "urn:string"
61+
assert "urn:string" in bundled["definitions"]
62+
63+
64+
def test_bundle_uses_call_retriever_when_inline_root_adds_external_ref():
65+
def retrieve(uri: str):
66+
if uri == "urn:external":
67+
return {"type": "string"}
68+
raise KeyError(f"Schema not found: {uri}")
69+
70+
root = {
71+
"type": "object",
72+
"properties": {"value": {"$ref": "urn:external"}},
73+
"required": ["value"],
74+
}
75+
registry = jsonschema_rs.Registry(resources=[("urn:seed", {"type": "integer"})])
76+
77+
bundled = jsonschema_rs.bundle(root, registry=registry, retriever=retrieve)
78+
79+
assert bundled["properties"]["value"]["$ref"] == "urn:external"
80+
assert "urn:external" in bundled["$defs"]
81+
82+
83+
def test_bundle_with_registry_accepts_equivalent_base_uri_with_empty_fragment():
84+
root = {"$id": "urn:root", "$ref": "urn:shared"}
85+
registry = jsonschema_rs.Registry(resources=[("urn:shared", {"type": "integer"})])
86+
87+
bundled = jsonschema_rs.bundle(root, registry=registry, base_uri="urn:root#")
88+
89+
assert bundled["$ref"] == "urn:shared"
90+
assert bundled["$defs"]["urn:shared"]["type"] == "integer"
91+
92+
4693
def test_bundle_unresolvable_raises():
4794
with pytest.raises(jsonschema_rs.ReferencingError):
4895
jsonschema_rs.bundle({"$ref": "https://example.com/missing.json"})

crates/jsonschema-py/tests-py/test_registry.py

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,6 @@ def test_top_level_functions_with_registry(function):
8585
assert list(function(schema, VALID_PERSON, registry=registry)) == []
8686
assert list(function(schema, INVALID_PERSON, registry=registry)) != []
8787

88-
8988
def test_validator_for_with_registry():
9089
registry = Registry(NESTED_RESOURCES)
9190
schema = {"$ref": "https://example.com/person.json"}
@@ -96,6 +95,21 @@ def test_validator_for_with_registry():
9695
assert not validator.is_valid(INVALID_PERSON)
9796

9897

98+
def test_validator_for_with_registry_and_explicit_draft4_legacy_id_root():
99+
registry = Registry([("urn:string", {"type": "string"})], draft=Draft4)
100+
schema = {
101+
"id": "urn:root",
102+
"type": "object",
103+
"properties": {"value": {"$ref": "urn:string"}},
104+
"required": ["value"],
105+
}
106+
107+
validator = Draft4Validator(schema, registry=registry)
108+
109+
assert validator.is_valid({"value": "ok"})
110+
assert not validator.is_valid({"value": 42})
111+
112+
99113
def test_registry_with_retriever_and_validation():
100114
def retrieve(uri: str):
101115
if uri == "https://example.com/dynamic.json":
@@ -118,6 +132,34 @@ def retrieve(uri: str):
118132
assert not dynamic_validator.is_valid("test")
119133

120134

135+
def test_validator_for_uses_call_retriever_when_inline_root_adds_external_ref():
136+
def retrieve(uri: str):
137+
if uri == "urn:external":
138+
return {"type": "string"}
139+
raise KeyError(f"Schema not found: {uri}")
140+
141+
registry = Registry([("urn:seed", {"type": "integer"})])
142+
schema = {
143+
"type": "object",
144+
"properties": {"value": {"$ref": "urn:external"}},
145+
"required": ["value"],
146+
}
147+
148+
validator = validator_for(schema, registry=registry, retriever=retrieve)
149+
assert validator.is_valid({"value": "ok"})
150+
assert not validator.is_valid({"value": 42})
151+
152+
153+
def test_validator_for_with_registry_accepts_equivalent_base_uri_with_empty_fragment():
154+
registry = Registry([("urn:shared", {"type": "integer"})])
155+
schema = {"$id": "urn:root", "$ref": "urn:shared"}
156+
157+
validator = validator_for(schema, registry=registry, base_uri="urn:root#")
158+
159+
assert validator.is_valid(1)
160+
assert not validator.is_valid("x")
161+
162+
121163
def test_registry_error_propagation():
122164
registry = Registry(NESTED_RESOURCES)
123165

crates/jsonschema-rb/spec/bundle_spec.rb

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,23 @@
4343
expect(validator.valid?({ "age" => 30 })).to be false
4444
end
4545

46+
it "bundles inline legacy-id root with registry and explicit draft4" do
47+
root = {
48+
"id" => "urn:root",
49+
"type" => "object",
50+
"properties" => { "value" => { "$ref" => "urn:string" } },
51+
"required" => ["value"]
52+
}
53+
registry = JSONSchema::Registry.new(
54+
[["urn:string", { "type" => "string" }]],
55+
draft: :draft4
56+
)
57+
58+
bundled = JSONSchema.bundle(root, registry: registry, draft: :draft4)
59+
expect(bundled.dig("properties", "value", "$ref")).to eq("urn:string")
60+
expect(bundled.dig("definitions", "urn:string")).not_to be_nil
61+
end
62+
4663
it "raises when a $ref cannot be resolved" do
4764
expect do
4865
JSONSchema.bundle({ "$ref" => "https://example.com/missing.json" })

0 commit comments

Comments
 (0)