Add continuum-schema-instance: build JSON instances from a JSON Schema
The Rust equivalent of json-schema-faker, scoped to what's actually useful in Rust: resolves $ref (relative to whichever file references it)/allOf, fills const/enum/default, and builds a value only for properties listed in required - everything else is left out. For code that has to construct a raw serde_json::Value against a schema it doesn't own, instead of a typed struct. Verified against continuum-schemas' real bambulab schemas manually, and against local fixtures in tests (round-tripped through the jsonschema crate to confirm the output actually validates).
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use continuum_schema_instance::build_instance_from_file;
|
||||
use serde_json::json;
|
||||
|
||||
fn fixture(relative: &str) -> PathBuf {
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures").join(relative)
|
||||
}
|
||||
|
||||
/// Validates `instance` against the real schema file - closes the loop:
|
||||
/// this confirms the builder's output is actually accepted by a real JSON
|
||||
/// Schema validator (the `jsonschema` crate), not just "looks right".
|
||||
fn assert_valid(schema_path: &PathBuf, instance: &serde_json::Value) {
|
||||
let schema_text = std::fs::read_to_string(schema_path).unwrap();
|
||||
let mut schema: serde_json::Value = serde_json::from_str(&schema_text).unwrap();
|
||||
// These fixtures have no `$id` (real schemas in continuum-schemas do,
|
||||
// as an absolute https:// URL) - the `jsonschema` crate resolves a
|
||||
// relative `$ref` against the schema's own `$id`, so give it one
|
||||
// pointing at this file's real location for the relative `$ref`s
|
||||
// inside to resolve from during this check.
|
||||
let absolute_path = schema_path.canonicalize().unwrap();
|
||||
schema["$id"] = json!(format!("file://{}", absolute_path.display()));
|
||||
|
||||
let validator = jsonschema::validator_for(&schema).expect("schema itself should compile");
|
||||
let errors: Vec<_> = validator.iter_errors(instance).collect();
|
||||
assert!(errors.is_empty(), "instance {instance} did not validate: {errors:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn required_fields_are_filled_and_optional_fields_are_left_out() {
|
||||
let path = fixture("events/item_created.schema.json");
|
||||
let mut instance = build_instance_from_file(&path).unwrap();
|
||||
|
||||
// "type" has no const at the envelope level, but item_created's own
|
||||
// allOf branch overrides it to one - that const wins.
|
||||
assert_eq!(instance["type"], json!("item_created"));
|
||||
// "event_id"/"item_id" are required with no const - each gets a
|
||||
// placeholder, ready to be overridden.
|
||||
assert!(instance.get("event_id").is_some());
|
||||
assert!(instance.get("item_id").is_some());
|
||||
// "note" is optional and correctly left out entirely.
|
||||
assert_eq!(instance.as_object().unwrap().len(), 3);
|
||||
|
||||
assert_valid(&path, &instance);
|
||||
|
||||
// The one field the caller actually has a value for - plain
|
||||
// serde_json::Value indexing, no special "overrides" API needed.
|
||||
instance["item_id"] = json!("item-42");
|
||||
assert_valid(&path, &instance);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allof_merge_lets_a_later_branch_override_an_earlier_ones_property() {
|
||||
let path = fixture("events/item_deleted.schema.json");
|
||||
let instance = build_instance_from_file(&path).unwrap();
|
||||
|
||||
// "type" is defined twice across the allOf: a plain string in the
|
||||
// shared envelope, then overridden to `const: "item_deleted"` in this
|
||||
// event's own branch - the later branch has to win.
|
||||
assert_eq!(instance["type"], json!("item_deleted"));
|
||||
assert_eq!(instance.as_object().unwrap().len(), 2); // event_id, type
|
||||
|
||||
assert_valid(&path, &instance);
|
||||
}
|
||||
Reference in New Issue
Block a user