// Validation, delegated to `mizchi/jsonschema`.
//
// This file is an adapter: `mizchi/jsonschema` does the schema checking, and
// MoonCheck maps each of its errors to a flat `ValidationError` with a
// `$.a[0]`-style path and a stable `ErrorKind`, which is what the reports and
// exit codes are built on.
///|
/// Validate a JSON value against a schema.
///
/// Returns all validation errors. An empty array means the value is valid.
pub fn validate(schema : Schema, value : Json) -> Array[ValidationError] {
match schema.validator.validate(value) {
Ok(_) => []
Err(errors) => errors.map(convert)
}
}
///|
/// Convenience: `true` when `validate` finds no error.
pub fn is_valid(schema : Schema, value : Json) -> Bool {
validate(schema, value).is_empty()
}
///|
/// Validate in one step from raw text.
///
/// Takes a schema document and a data document, both as JSON text.
/// Returns `Ok(errors)` where an empty list means the data is valid, or
/// `Err(reason)` when either document could not be parsed.
pub fn validate_strings(
schema_text : String,
data_text : String,
) -> Result[Array[ValidationError], String] {
let schema = parse_schema_string(schema_text) catch {
SchemaError::InvalidSchema(msg) => return Err("invalid schema: \{msg}")
}
let data = @json.parse(data_text) catch {
err => return Err("data is not valid JSON: \{err}")
}
Ok(validate(schema, data))
}
///|
/// Parse a schema document, returning the reason as a string on failure.
///
/// Batch and CLI callers parse the schema once and reuse it for every data
/// document, so a broken schema is reported separately from data errors.
pub fn parse_schema_text(text : String) -> Result[Schema, String] {
Ok(parse_schema_string(text)) catch {
SchemaError::InvalidSchema(msg) => Err(msg)
}
}
///|
/// Validate a data document against an already parsed schema.
///
/// Unlike `validate_strings`, a data document that is not valid JSON is
/// reported as a single `InvalidJson` error instead of aborting the call, so
/// callers that process many documents can keep going and report everything.
pub fn validate_text(
schema : Schema,
data_text : String,
) -> Array[ValidationError] {
let data = @json.parse(data_text) catch {
err =>
return [
{
path: "$",
kind: InvalidJson,
message: "data is not valid JSON: \{err}",
},
]
}
validate(schema, data)
}
///|
fn convert(error : @jsonschema.ValidationError) -> ValidationError {
{
path: data_path(error.json_path),
kind: classify(error.schema_path, error.message),
message: error.message,
}
}
///|
/// Render a data pointer as `$`, `$.user.age` or `$.tags[0]`.
fn data_path(pointer : @jsonschema.JsonPointer) -> String {
match pointer {
Root => "$"
Key(parent, key~) => "\{data_path(parent)}.\{key}"
Index(parent, index~) => "\{data_path(parent)}[\{index}]"
}
}
///|
/// Classify an error by the schema keyword that produced it.
///
/// `mizchi/jsonschema` points `schema_path` at the failing keyword (for
/// example `#/properties/age/minimum`). Errors raised by a whole schema node
/// (wrong type, a bare `enum`, `const`) point at the node itself instead, so
/// those are told apart by their message.
fn classify(
schema_path : @jsonschema.JsonPointer,
message : String,
) -> ErrorKind {
if is_node(schema_path) {
return if message.has_prefix("Value does not match constant") ||
message.has_prefix("Failed to resolve") ||
message.has_prefix("Array does not have item") {
Constraint
} else if message.contains(" is not in ") {
NotInEnum
} else {
Type
}
}
match schema_path {
Key(_, key="required") => MissingRequired
Key(_, key="minimum") | Key(_, key="exclusiveMinimum") => BelowMinimum
Key(_, key="maximum") | Key(_, key="exclusiveMaximum") => AboveMaximum
Key(_, key="minLength") | Key(_, key="minItems") => TooShort
Key(_, key="maxLength") | Key(_, key="maxItems") => TooLong
Key(_, key="enum") => NotInEnum
_ => Constraint
}
}
///|
/// `true` when `schema_path` points at a schema node rather than a keyword.
fn is_node(schema_path : @jsonschema.JsonPointer) -> Bool {
match schema_path {
Root => true
Key(Key(_, key="properties"), ..) => true
Key(_, key="items") | Key(_, key="$ref") => true
Index(_, ..) => true
_ => false
}
}