///|
/// Validate an instance against a JSON Schema (2020-12).
///
/// The schema must be a `Json` boolean or object, as produced by
/// `@json.parse`. On failure, returns *all* collected errors, each carrying
/// the instance path and schema path of the failure (JSON Pointer, RFC 6901).
pub fn validate(
schema : Json,
instance : Json,
) -> Result[Unit, Array[ValidationError]] {
validate_with_docs(schema, instance, {})
}
///|
/// Validate with additional external documents available for `$ref`
/// resolution, keyed by their absolute URI (their embedded `$id`s are
/// registered as well). This is how multi-file schemas are validated
/// without any network access.
pub fn validate_with_docs(
schema : Json,
instance : Json,
docs : Map[String, Json],
) -> Result[Unit, Array[ValidationError]] {
// the root resource's own $id provides the initial base URI
let root_base : String = if schema is Object(o) &&
o.get("$id") is Some(String(id)) {
uri_join("", id)
} else {
""
}
let ctx : Ctx = {
root: schema,
errors: [],
regex_cache: {},
depth: 0,
anno_stack: [Annos::empty()],
resource_stack: [schema],
docs,
ids: {},
ids_built: false,
jumped: 0,
id_stack: [root_base],
}
for uri, doc in docs {
if ctx.ids.get(uri) is None {
ctx.ids.set(uri, doc)
}
let _ = collect_ids(doc, uri, ctx.ids)
}
validate_node(ctx, schema, instance, PRoot, PRoot)
if ctx.errors.length() == 0 {
Ok(())
} else {
Err(ctx.errors)
}
}
///|
/// Boolean convenience over `validate`.
pub fn is_valid(schema : Json, instance : Json) -> Bool {
validate(schema, instance) is Ok(_)
}
///|
/// Parse two JSON texts, then validate the instance against the schema.
/// Parse failures are reported separately from validation failures.
pub fn validate_json(
schema_text : String,
instance_text : String,
) -> Result[Unit, JsonSchemaError] {
let schema : Json = @json.parse(schema_text) catch {
e => return Err(SchemaParseError(e.to_string()))
}
let instance : Json = @json.parse(instance_text) catch {
e => return Err(InstanceParseError(e.to_string()))
}
match validate(schema, instance) {
Ok(_) => Ok(())
Err(errors) => Err(ValidationErrors(errors))
}
}
///|
/// Dispatch a schema node against an instance value.
fn validate_node(
ctx : Ctx,
schema : Json,
inst : Json,
ip : Path,
sp : Path,
) -> Unit {
ctx.depth = ctx.depth + 1
if ctx.depth > MAX_DEPTH {
ctx.add_error(
ip, sp, "max_depth", "maximum schema recursion depth exceeded (possible cyclic $ref)",
)
ctx.depth = ctx.depth - 1
return
}
match schema {
True => ()
False =>
ctx.add_error(
ip, sp, "boolean_schema", "schema `false` accepts no values",
)
Object(obj) => {
// track $id base URIs and resource scopes on the way in
let pushed_id = ctx.push_id(schema)
kw_ref(ctx, obj, inst, ip, sp)
kw_type_enum_const(ctx, obj, inst, ip, sp)
kw_numeric(ctx, obj, inst, ip, sp)
kw_string(ctx, obj, inst, ip, sp)
kw_array(ctx, obj, inst, ip, sp)
kw_object(ctx, obj, inst, ip, sp)
kw_logic(ctx, obj, inst, ip, sp)
// unevaluated* must run after every other applicator has had a
// chance to record its annotations for this instance location
kw_unevaluated(ctx, obj, inst, ip, sp)
if pushed_id {
let _ = ctx.id_stack.pop()
let _ = ctx.resource_stack.pop()
}
}
_ => () // schemas must be objects or booleans; anything else is ignored
}
ctx.depth = ctx.depth - 1
}