///|
/// Validate without allocating a diagnostic array. Invalid JSON numbers and
/// exhausted traversal budgets are errors, not ordinary validation failures.
pub fn Schema::valid(
self : Schema,
value : Json,
max_depth~ : Int32 = 128,
) -> Bool raise {
check_instance(value, 0, max_depth)
evaluate(self.plan, self.plan.nodes[0], value, "", None, 0, max_depth)
}
///|
/// Collect assertion failures. Paths are RFC 6901 JSON Pointers; failed
/// speculative anyOf/oneOf/not/if branches do not leak their diagnostics.
pub fn Schema::faults(
self : Schema,
value : Json,
max_depth~ : Int32 = 128,
) -> Array[Fault] raise {
check_instance(value, 0, max_depth)
let failures : Array[Fault] = []
ignore(
evaluate(
self.plan,
self.plan.nodes[0],
value,
"",
Some(failures),
0,
max_depth,
),
)
failures
}
///|
// Preflight traverses the entire instance before evaluation, so invalid numeric
// nodes and exhausted depth budgets are errors even when a schema rejects early.
// Return a pointer only when a numeric error exists; the common successful
// traversal allocates no path segments or index strings.
fn check_instance(
value : Json,
depth : Int32,
limit : Int32,
) -> Unit raise EvaluationError {
if invalid_number_path(value, depth, limit) is Some(path) {
raise InvalidNumber(path~)
}
}
///|
fn invalid_number_path(
value : Json,
depth : Int32,
limit : Int32,
) -> String? raise EvaluationError {
if limit < 1 || depth > limit {
raise EvaluationError::DepthLimit(limit)
}
match value {
Number(number, repr~) =>
if (repr is Some(_) && ExactNumber::of_json(value) is None) ||
(repr is None && (number.is_inf() || number.is_nan())) {
Some("")
} else {
None
}
Object(fields) => {
for name, child in fields {
if invalid_number_path(child, depth + 1, limit) is Some(tail) {
return Some(pointer_step("", name) + tail)
}
}
None
}
Array(values) => {
for index, child in values {
if invalid_number_path(child, depth + 1, limit) is Some(tail) {
return Some("/" + index.to_string() + tail)
}
}
None
}
_ => None
}
}
///|
fn numeric_value(
value : Json,
path : String,
) -> ExactNumber raise EvaluationError {
match ExactNumber::of_json(value) {
Some(number) => number
None => raise InvalidNumber(path~)
}
}
///|
fn number_has_repr(value : Json) -> Bool {
match value {
Number(_, repr~) => repr is Some(_)
_ => false
}
}
///|
// JSON Schema equality compares numbers mathematically, including nested ones.
// Inputs have already passed numeric/depth checks before this recursion.
fn schema_equal(left : Json, right : Json) -> Bool raise NumericError {
match (left, right) {
(Number(a, ..), Number(b, ..)) =>
if !number_has_repr(left) && !number_has_repr(right) {
a == b
} else {
match (ExactNumber::of_json(left), ExactNumber::of_json(right)) {
(Some(a), Some(b)) => a.equal(b)
_ => false
}
}
(Object(a), Object(b)) => {
if a.length() != b.length() {
return false
}
for key, value in a {
match b.get(key) {
Some(other) => if !schema_equal(value, other) { return false }
None => return false
}
}
true
}
(Array(a), Array(b)) => {
// SAFETY (Core Array::unsafe_get): length equality was checked, and
// 0 <= index < a.length() for the whole iteration. schema_equal only
// reads JSON trees and never changes the length of either array.
if a.length() != b.length() {
return false
}
for index, value in a {
if !schema_equal(value, b.unsafe_get(index)) {
return false
}
}
true
}
_ => left == right
}
}
///|
fn fail(
failures : Array[Fault]?,
instance_path : String,
schema_path : String,
keyword : String,
message : String,
) -> Bool {
if failures is Some(entries) {
entries.push({
instance_path,
schema_path,
schema_uri: None,
keyword,
message,
})
}
false
}
///|
fn matches_type(kind : ValueType, value : Json, path : String) -> Bool raise {
match (kind, value) {
(NullType, Null)
| (BooleanType, True)
| (BooleanType, False)
| (NumberType, Number(_, ..))
| (StringType, String(_))
| (ArrayType, Array(_))
| (ObjectType, Object(_)) => true
(IntegerType, Number(_, ..)) => numeric_value(value, path).is_integer()
_ => false
}
}
///|
fn evaluate_rule(
program : Program,
rule : Rule,
value : Json,
path : String,
failures : Array[Fault]?,
depth : Int32,
limit : Int32,
scope~ : Array[Int32],
evaluated~ : EvaluationMarks?,
) -> Bool raise {
match rule {
Types(types, location) => {
for kind in types {
if matches_type(kind, value, path) {
return true
}
}
fail(
failures, path, location, "type", "instance has none of the permitted types",
)
}
Constant(expected, location) =>
schema_equal(value, expected) ||
fail(
failures, path, location, "const", "instance differs from the constant",
)
Enumeration(values, location) =>
values.any(expected => schema_equal(value, expected)) ||
fail(
failures, path, location, "enum", "instance is not an enumerated value",
)
Lower(bound, exclusive, location) => {
guard value is Number(_, ..) else { return true }
let ordering = numeric_value(value, path).compare(bound)
(if exclusive { ordering > 0 } else { ordering >= 0 }) ||
fail(
failures,
path,
location,
if exclusive {
"exclusiveMinimum"
} else {
"minimum"
},
"number is below its lower bound",
)
}
Upper(bound, exclusive, location) => {
guard value is Number(_, ..) else { return true }
let ordering = numeric_value(value, path).compare(bound)
(if exclusive { ordering < 0 } else { ordering <= 0 }) ||
fail(
failures,
path,
location,
if exclusive {
"exclusiveMaximum"
} else {
"maximum"
},
"number exceeds its upper bound",
)
}
Multiple(divisor, location) => {
guard value is Number(_, ..) else { return true }
numeric_value(value, path).multiple_of(divisor) ||
fail(
failures, path, location, "multipleOf", "number is not an exact multiple",
)
}
StringLength(minimum, maximum, location) => {
guard value is String(text) else { return true }
let length = text.char_length()
let mut valid = true
if minimum is Some(bound) && bound.above(length) {
valid = fail(
failures,
path,
pointer_step(location, "minLength"),
"minLength",
"string has too few Unicode code points",
)
if failures is None {
return false
}
}
if maximum is Some(bound) && bound.below(length) {
valid = fail(
failures,
path,
pointer_step(location, "maxLength"),
"maxLength",
"string has too many Unicode code points",
)
}
valid
}
Pattern(pattern, location) => {
guard value is String(text) else { return true }
pattern_matches(pattern, text) ||
fail(
failures, path, location, "pattern", "string does not match the pattern",
)
}
_ =>
evaluate_applicator(
program, rule, value, path, failures, depth, limit, scope, evaluated,
)
}
}