Skip to content

Commit

Permalink
fix: Fragment references within $id-anchored subschemas
Browse files Browse the repository at this point in the history
Signed-off-by: Dmitry Dygalo <[email protected]>
Co-authored-by: Johannes Rudolph <[email protected]>
  • Loading branch information
Stranger6667 and jrudolph committed Dec 31, 2024
1 parent a95d662 commit 9aa4803
Show file tree
Hide file tree
Showing 4 changed files with 130 additions and 6 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## [Unreleased]

### Fixed

- Handle fragment references within `$id`-anchored subschemas. [#640](https://github.com/Stranger6667/jsonschema/issues/640)

## [0.28.0] - 2024-12-29

### Added
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

- Handle fragment references within `$id`-anchored subschemas. [#640](https://github.com/Stranger6667/jsonschema/issues/640)

## [0.28.0] - 2024-12-29

### Added
Expand Down
3 changes: 3 additions & 0 deletions crates/jsonschema-referencing/src/uri.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ pub use fluent_uri::encoding::encoder::Path;
///
/// Returns an error if base has not schema or there is a fragment.
pub fn resolve_against(base: &Uri<&str>, uri: &str) -> Result<Uri<String>, Error> {
if uri.starts_with('#') && base.as_str().ends_with(uri) {
return Ok(base.to_owned());
}
Ok(UriRef::parse(uri)
.map_err(|error| Error::uri_reference_parsing_error(uri, error))?
.resolve_against(base)
Expand Down
125 changes: 119 additions & 6 deletions crates/jsonschema/src/keywords/ref_.rs
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,8 @@ pub(crate) fn compile_recursive_ref<'a>(
#[cfg(test)]
mod tests {
use crate::tests_util;
use referencing::{Draft, Retrieve, Uri};
use ahash::HashMap;
use referencing::{Retrieve, Uri};
use serde_json::{json, Value};
use test_case::test_case;

Expand Down Expand Up @@ -465,6 +466,7 @@ mod tests {
"/types" => Ok(json!({
"$id": "/types",
"foo": {
"$id": "#/foo",
"$ref": "#/bar"
},
"bar": {
Expand All @@ -476,11 +478,7 @@ mod tests {
}
}

let validator = match crate::options()
.with_draft(Draft::Draft201909)
.with_retriever(MyRetrieve)
.build(&schema)
{
let validator = match crate::options().with_retriever(MyRetrieve).build(&schema) {
Ok(validator) => validator,
Err(error) => panic!("{error}"),
};
Expand All @@ -489,6 +487,121 @@ mod tests {
assert!(!validator.is_valid(&json!("")));
}

struct TestRetrieve {
storage: HashMap<String, Value>,
}

impl Retrieve for TestRetrieve {
fn retrieve(
&self,
uri: &Uri<&str>,
) -> Result<Value, Box<dyn std::error::Error + Send + Sync>> {
self.storage
.get(uri.path().as_str())
.cloned()
.ok_or_else(|| "Document not found".into())
}
}

#[test_case(
json!({"$ref": "/doc#/definitions/foo"}),
json!({
"$id": "/doc",
"definitions": {
"foo": {"type": "integer"}
}
}),
None
; "basic_fragment"
)]
#[test_case(
json!({"$ref": "/doc1#/definitions/foo"}),
json!({
"$id": "/doc1",
"definitions": {
"foo": {"$ref": "#/definitions/bar"},
"bar": {"type": "integer"}
}
}),
None
; "intermediate_reference"
)]
#[test_case(
json!({"$ref": "/doc2#/refs/first"}),
json!({
"$id": "/doc2",
"refs": {
"first": {"$ref": "/doc3#/refs/second"}
}
}),
Some(json!({
"/doc3": {
"$id": "/doc3",
"refs": {
"second": {"type": "integer"}
}
}
}))
; "multiple_documents"
)]
#[test_case(
json!({"$ref": "/doc4#/defs/foo"}),
json!({
"$id": "/doc4",
"defs": {
"foo": {
"$id": "#/defs/foo",
"$ref": "#/defs/bar"
},
"bar": {"type": "integer"}
}
}),
None
; "id_and_fragment"
)]
#[test_case(
json!({"$ref": "/doc5#/outer"}),
json!({
"$id": "/doc5",
"outer": {
"$ref": "#/middle",
},
"middle": {
"$id": "#/middle",
"$ref": "#/inner"
},
"inner": {"type": "integer"}
}),
None
; "nested_references"
)]
fn test_fragment_resolution(schema: Value, root: Value, extra: Option<Value>) {
let mut storage = HashMap::default();

let doc_path = schema["$ref"]
.as_str()
.and_then(|r| r.split('#').next())
.expect("Invalid $ref");

storage.insert(doc_path.to_string(), root);

if let Some(extra) = extra {
for (path, document) in extra.as_object().unwrap() {
storage.insert(path.clone(), document.clone());
}
}

let retriever = TestRetrieve { storage };

let validator = crate::options()
.with_retriever(retriever)
.build(&schema)
.expect("Invalid schema");

assert!(validator.is_valid(&json!(42)));
assert!(!validator.is_valid(&json!("string")));
}

#[test]
fn test_infinite_loop() {
let validator = crate::validator_for(&json!({"$ref": "#"})).expect("Invalid schema");
Expand Down

0 comments on commit 9aa4803

Please sign in to comment.