Skip to content

Commit

Permalink
fix: Location-independent references in remote schemas
Browse files Browse the repository at this point in the history
Signed-off-by: Dmitry Dygalo <dmitry@dygalo.dev>
  • Loading branch information
Stranger6667 committed Sep 18, 2024
1 parent eba86a2 commit a0a3578
Show file tree
Hide file tree
Showing 5 changed files with 58 additions and 37 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@

Old names are retained for backward compatibility but will be removed in a future release.

### Fixed

- Location-independent references in remote schemas on drafts 4, 6, and 7.

## [0.19.1] - 2024-09-15

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion crates/jsonschema-cli/tests/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ fn test_invalid_instance() {
fn test_invalid_schema() {
let dir = tempdir().unwrap();
let schema = create_temp_file(&dir, "schema.json", r#"{"type": "invalid"}"#);
let instance = create_temp_file(&dir, "instance.json", r#"{}"#);
let instance = create_temp_file(&dir, "instance.json", "{}");

let mut cmd = cli();
cmd.arg(&schema).arg("--instance").arg(&instance);
Expand Down
4 changes: 4 additions & 0 deletions crates/jsonschema-py/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## [Unreleased]

### Fixed

- Location-independent references in remote schemas on drafts 4, 6, and 7.

## [0.19.1] - 2024-09-15

### Fixed
Expand Down
76 changes: 49 additions & 27 deletions crates/jsonschema/src/resolver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
use crate::{
compilation::DEFAULT_ROOT_URL,
error::ValidationError,
schemas::{id_of, Draft},
schemas::{draft_from_schema, id_of, Draft},
};
use ahash::AHashMap;
use parking_lot::RwLock;
Expand Down Expand Up @@ -116,10 +116,11 @@ impl SchemaResolver for DefaultResolver {
pub(crate) struct Resolver {
external_resolver: Arc<dyn SchemaResolver>,
root_schema: Arc<Value>,
draft: Draft,
// canonical_id: sub-schema mapping to resolve documents by their ID
// canonical_id is composed with the root document id
// (if not specified, then `DEFAULT_ROOT_URL` is used for this purpose)
schemas: AHashMap<String, Arc<Value>>,
schemas: RwLock<AHashMap<String, Arc<Value>>>,
store: RwLock<AHashMap<Cow<'static, str>, Arc<Value>>>,
}

Expand Down Expand Up @@ -149,8 +150,9 @@ impl Resolver {
})?;
Ok(Resolver {
external_resolver,
draft,
root_schema: schema,
schemas,
schemas: RwLock::new(schemas),
store: RwLock::new(store),
})
}
Expand All @@ -163,22 +165,29 @@ impl Resolver {
fn resolve_url(&self, url: &Url, orig_ref: &str) -> Result<Arc<Value>, ValidationError<'_>> {
match url.as_str() {
DEFAULT_ROOT_URL => Ok(self.root_schema.clone()),
url_str => match self.schemas.get(url_str) {
Some(value) => Ok(value.clone()),
None => {
if let Some(cached) = self.store.read().get(url_str) {
return Ok(cached.clone());
}
let resolved = self
.external_resolver
.resolve(&self.root_schema, url, orig_ref)
.map_err(|error| ValidationError::resolver(url.clone(), error))?;
self.store
.write()
.insert(url.to_string().into(), resolved.clone());
Ok(resolved)
url_str => {
if let Some(value) = self.schemas.read().get(url_str) {
return Ok(value.clone());
}
},
if let Some(cached) = self.store.read().get(url_str) {
return Ok(cached.clone());
}
let resolved = self
.external_resolver
.resolve(&self.root_schema, url, orig_ref)
.map_err(|error| ValidationError::resolver(url.clone(), error))?;
self.store
.write()
.insert(url.to_string().into(), resolved.clone());
let draft = draft_from_schema(&resolved).unwrap_or(self.draft);
// traverse the schema and store all named ones under their canonical ids
let mut schemas = self.schemas.write();
find_schemas(draft, &resolved, &url, &mut |id, schema| {
schemas.insert(id, Arc::new(schema.clone()));
None
})?;
Ok(resolved)
}
}
}

Expand All @@ -200,7 +209,7 @@ impl Resolver {

// Location-independent identifiers are searched before trying to resolve by
// fragment-less url
if let Some(document) = self.schemas.get(url.as_str()) {
if let Some(document) = self.schemas.read().get(url.as_str()) {
return Ok((resource, Arc::clone(document)));
}

Expand Down Expand Up @@ -378,7 +387,7 @@ mod tests {
let schema = json!({"type": "string"});
let resolver = make_resolver(&schema);
// Then in the resolver schema there should be no schemas
assert_eq!(resolver.schemas.len(), 0);
assert_eq!(resolver.schemas.read().len(), 0);
}

#[test]
Expand All @@ -392,10 +401,11 @@ mod tests {
});
let resolver = make_resolver(&schema);
// Then in the resolver schema there should be only this schema
assert_eq!(resolver.schemas.len(), 1);
assert_eq!(resolver.schemas.read().len(), 1);
assert_eq!(
resolver
.schemas
.read()
.get("json-schema:///#foo")
.map(AsRef::as_ref),
schema.pointer("/definitions/A")
Expand All @@ -415,17 +425,19 @@ mod tests {
});
let resolver = make_resolver(&schema);
// Then in the resolver schema there should be only these schemas
assert_eq!(resolver.schemas.len(), 2);
assert_eq!(resolver.schemas.read().len(), 2);
assert_eq!(
resolver
.schemas
.read()
.get("json-schema:///#foo")
.map(AsRef::as_ref),
schema.pointer("/definitions/A/0")
);
assert_eq!(
resolver
.schemas
.read()
.get("json-schema:///#bar")
.map(AsRef::as_ref),
schema.pointer("/definitions/A/1")
Expand Down Expand Up @@ -462,17 +474,19 @@ mod tests {
});
let resolver = make_resolver(&schema);
// Then in the resolver schema there should be root & sub-schema
assert_eq!(resolver.schemas.len(), 2);
assert_eq!(resolver.schemas.read().len(), 2);
assert_eq!(
resolver
.schemas
.read()
.get("http://localhost:1234/tree")
.map(AsRef::as_ref),
schema.pointer("")
);
assert_eq!(
resolver
.schemas
.read()
.get("http://localhost:1234/node")
.map(AsRef::as_ref),
schema.pointer("/definitions/node")
Expand All @@ -488,10 +502,11 @@ mod tests {
}
});
let resolver = make_resolver(&schema);
assert_eq!(resolver.schemas.len(), 1);
assert_eq!(resolver.schemas.read().len(), 1);
assert_eq!(
resolver
.schemas
.read()
.get("http://localhost:1234/bar#foo")
.map(AsRef::as_ref),
schema.pointer("/definitions/A")
Expand All @@ -516,24 +531,27 @@ mod tests {
}
});
let resolver = make_resolver(&schema);
assert_eq!(resolver.schemas.len(), 3);
assert_eq!(resolver.schemas.read().len(), 3);
assert_eq!(
resolver
.schemas
.read()
.get("http://localhost:1234/root")
.map(AsRef::as_ref),
schema.pointer("")
);
assert_eq!(
resolver
.schemas
.read()
.get("http://localhost:1234/nested.json")
.map(AsRef::as_ref),
schema.pointer("/definitions/A")
);
assert_eq!(
resolver
.schemas
.read()
.get("http://localhost:1234/nested.json#foo")
.map(AsRef::as_ref),
schema.pointer("/definitions/A/definitions/B")
Expand All @@ -550,17 +568,19 @@ mod tests {
}
});
let resolver = make_resolver(&schema);
assert_eq!(resolver.schemas.len(), 2);
assert_eq!(resolver.schemas.read().len(), 2);
assert_eq!(
resolver
.schemas
.read()
.get("http://localhost:1234/")
.map(AsRef::as_ref),
schema.pointer("")
);
assert_eq!(
resolver
.schemas
.read()
.get("http://localhost:1234/folder/")
.map(AsRef::as_ref),
schema.pointer("/items")
Expand All @@ -584,17 +604,19 @@ mod tests {
"type": "object"
});
let resolver = make_resolver(&schema);
assert_eq!(resolver.schemas.len(), 2);
assert_eq!(resolver.schemas.read().len(), 2);
assert_eq!(
resolver
.schemas
.read()
.get("http://localhost:1234/scope_change_defs1.json")
.map(AsRef::as_ref),
schema.pointer("")
);
assert_eq!(
resolver
.schemas
.read()
.get("http://localhost:1234/folder/")
.map(AsRef::as_ref),
schema.pointer("/definitions/baz")
Expand Down
9 changes: 0 additions & 9 deletions crates/jsonschema/tests/suite.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,10 @@ use testsuite::{suite, Test};
"draft4::optional::bignum::integer::a_bignum_is_an_integer",
"draft4::optional::bignum::integer::a_negative_bignum_is_an_integer",
"draft4::optional::ecmascript_regex",
"draft4::ref_remote::location_independent_identifier_in_remote_ref",
"draft6::optional::ecmascript_regex",
"draft6::ref_remote::location_independent_identifier_in_remote_ref",
"draft6::ref_remote::ref_to_ref_finds_location_independent_id",
"draft7::optional::ecmascript_regex",
"draft7::optional::format::idn_hostname::validation_of_internationalized_host_names",
"draft7::optional::format::time::validation_of_time_strings",
"draft7::ref_remote::location_independent_identifier_in_remote_ref",
"draft7::ref_remote::ref_to_ref_finds_location_independent_id",
"draft2019-09::anchor",
"draft2019-09::defs",
"draft2019-09::optional::anchor",
Expand All @@ -42,7 +37,6 @@ use testsuite::{suite, Test};
"draft2019-09::ref_remote::base_uri_change_change_folder",
"draft2019-09::ref_remote::location_independent_identifier_in_remote_ref",
"draft2019-09::ref_remote::ref_to_ref_finds_detached_anchor",
"draft2019-09::ref_remote::remote_http_ref_with",
"draft2019-09::unevaluated_items",
"draft2019-09::unevaluated_properties::unevaluated_properties_with_recursive_ref",
"draft2019-09::vocabulary::schema_that_uses_custom_metaschema_with_with_no_validation_vocabulary",
Expand Down Expand Up @@ -72,9 +66,6 @@ use testsuite::{suite, Test};
"draft2020-12::ref_remote::base_uri_change_change_folder",
"draft2020-12::ref_remote::location_independent_identifier_in_remote_ref",
"draft2020-12::ref_remote::ref_to_ref_finds_detached_anchor",
"draft2020-12::ref_remote::remote_http_ref_with_different_id",
"draft2020-12::ref_remote::remote_http_ref_with_different_urn_id",
"draft2020-12::ref_remote::remote_http_ref_with_nested_absolute_ref",
"draft2020-12::unevaluated_properties::unevaluated_properties_with_dynamic_ref",
"draft2020-12::unevaluated_items",
"draft2020-12::vocabulary",
Expand Down

0 comments on commit a0a3578

Please sign in to comment.