-
Notifications
You must be signed in to change notification settings - Fork 54
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Introduce the
jsonSchemaMerge
reduction strategy (#1132)
- Loading branch information
Showing
5 changed files
with
144 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
use super::{count_nodes_heap, Cursor, Error, Result}; | ||
use crate::{inference::Shape, schema::SchemaBuilder, AsNode, HeapNode}; | ||
use json::schema::index::IndexBuilder; | ||
|
||
pub fn json_schema_merge<'alloc, L: AsNode, R: AsNode>( | ||
cur: Cursor<'alloc, '_, '_, '_, '_, L, R>, | ||
) -> Result<HeapNode<'alloc>> { | ||
let Cursor { | ||
tape, | ||
loc, | ||
full: _, | ||
lhs, | ||
rhs, | ||
alloc, | ||
} = cur; | ||
|
||
let (lhs, rhs) = (lhs.into_heap_node(alloc), rhs.into_heap_node(alloc)); | ||
|
||
*tape = &tape[count_nodes_heap(&rhs)..]; | ||
|
||
// Ensure that we're working with objects on both sides | ||
// Question: Should we actually relax this to support | ||
// reducing valid schemas like "true" and "false"? | ||
let ( | ||
lhs @ HeapNode::Object(_), | ||
rhs @ HeapNode::Object(_) | ||
) = (lhs, rhs) else { | ||
return Err(Error::with_location(Error::JsonSchemaMergeWrongType { detail: None }, loc) ) | ||
}; | ||
|
||
let left = shape_from_node(lhs).map_err(|e| Error::with_location(e, loc))?; | ||
let right = shape_from_node(rhs).map_err(|e| Error::with_location(e, loc))?; | ||
|
||
let mut merged_shape = Shape::union(left, right); | ||
merged_shape.enforce_field_count_limits(json::Location::Root); | ||
|
||
// Union together the LHS and RHS, and convert back from `Shape` into `HeapNode`. | ||
let merged_doc = serde_json::to_value(&SchemaBuilder::new(merged_shape).root_schema()) | ||
.and_then(|value| HeapNode::from_serde(value, alloc)) | ||
.map_err(|e| { | ||
Error::with_location( | ||
Error::JsonSchemaMergeWrongType { | ||
detail: Some(e.to_string()), | ||
}, | ||
loc, | ||
) | ||
})?; | ||
|
||
Ok(merged_doc) | ||
} | ||
|
||
fn shape_from_node<'a, N: AsNode>(node: N) -> Result<Shape> { | ||
// Should this be something more specific/useful? | ||
let url = url::Url::parse("json-schema-reduction:///").unwrap(); | ||
|
||
let serialized = | ||
serde_json::to_value(node.as_node()).map_err(|e| Error::JsonSchemaMergeWrongType { | ||
detail: Some(e.to_string()), | ||
})?; | ||
|
||
let schema = json::schema::build::build_schema::<crate::Annotation>(url.clone(), &serialized) | ||
.map_err(|e| Error::JsonSchemaMergeWrongType { | ||
detail: Some(e.to_string()), | ||
})?; | ||
|
||
let mut index = IndexBuilder::new(); | ||
index.add(&schema).unwrap(); | ||
index.verify_references().unwrap(); | ||
let index = index.into_index(); | ||
|
||
Ok(Shape::infer( | ||
index | ||
.must_fetch(&url) | ||
.map_err(|e| Error::JsonSchemaMergeWrongType { | ||
detail: Some(e.to_string()), | ||
})?, | ||
&index, | ||
)) | ||
} | ||
|
||
#[cfg(test)] | ||
mod test { | ||
use super::super::test::*; | ||
use super::*; | ||
|
||
#[test] | ||
fn test_merge_json_schemas() { | ||
run_reduce_cases( | ||
json!({ "reduce": { "strategy": "jsonSchemaMerge" } }), | ||
vec![ | ||
Partial { | ||
rhs: json!({ | ||
"type": "string", | ||
"maxLength": 5, | ||
"minLength": 5 | ||
}), | ||
expect: Ok(json!({ | ||
"type": "string", | ||
"maxLength": 5, | ||
"minLength": 5 | ||
})), | ||
}, | ||
Partial { | ||
rhs: json!("oops!"), | ||
expect: Err(Error::JsonSchemaMergeWrongType { detail: None }), | ||
}, | ||
Partial { | ||
rhs: json!({ | ||
"type": "foo" | ||
}), | ||
expect: Err(Error::JsonSchemaMergeWrongType { | ||
detail: Some( | ||
r#"at keyword 'type' of schema 'json-schema-reduction:///': expected a type or array of types: invalid type name: 'foo'"#.to_owned(), | ||
), | ||
}), | ||
}, | ||
Partial { | ||
rhs: json!({ | ||
"type": "string", | ||
"minLength": 8, | ||
"maxLength": 10 | ||
}), | ||
expect: Ok(json!({ | ||
"$schema": "http://json-schema.org/draft-07/schema#", | ||
"type": "string", | ||
"minLength": 5, | ||
"maxLength": 10, | ||
})), | ||
}, | ||
], | ||
) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters