///|
/// Which upstream exception class an error stands for.
pub(all) enum ErrorKind {
/// `jsonschema.exceptions.ValidationError`: an instance was invalid under a
/// provided schema.
Validation
/// `jsonschema.exceptions.SchemaError`: a schema was invalid under its
/// corresponding metaschema.
Schema
} derive(Eq, Debug)
///|
/// A validation error (upstream's `_Error`, i.e. `ValidationError` or
/// `SchemaError` depending on `kind`).
///
/// Errors are mutable, like upstream's: validators fill in details
/// (`validator`, `validator_value`, `instance`, `schema`) as errors bubble up
/// and prepend to the relative paths.
///
/// The `validator`, `validator_value`, `instance` and `schema` fields are
/// `None` while unset (upstream's `_unset`); a Python `None` is represented
/// as `Some(Json::null())` (e.g. `validator` is `Some(null)` for errors
/// produced by a `false` schema).
pub struct ValidationError {
kind : ErrorKind
/// A human readable message explaining the error.
message : String
/// The path to the offending element within the instance, relative to
/// the parent error (upstream's `path` / `relative_path`).
path : Array[PathItem]
/// The path to the failing keyword within the schema, relative to the
/// parent error (upstream's `schema_path` / `relative_schema_path`).
schema_path : Array[PathItem]
/// Errors from subschemas (e.g. of `anyOf`) explaining this error.
context : Array[ValidationError]
/// The underlying error, e.g. from a format checker.
cause : Error?
/// The name of the failed keyword.
mut validator : Json?
/// The keyword's value in the schema.
mut validator_value : Json?
/// The instance that was being validated.
mut instance : Json?
/// The (sub)schema the keyword is in.
mut schema : Json?
/// The error this one is in the context of, if any.
mut parent : ValidationError?
priv mut type_checker : TypeChecker?
}
///|
/// Raised by validation: carries the `ValidationError` data.
pub(all) suberror JsonSchemaError {
/// `jsonschema.exceptions.ValidationError`
ValidationError(ValidationError)
/// `jsonschema.exceptions.SchemaError`
SchemaError(ValidationError)
}
///|
pub impl Show for JsonSchemaError with fn output(self, logger) {
match self {
ValidationError(e) | SchemaError(e) => logger.write_string("\{e}")
}
}
///|
/// Construct an error (`ValidationError(message, ...)` upstream).
pub fn ValidationError::new(
message : String,
validator? : Json,
path? : ArrayView[PathItem] = [],
cause? : Error,
context? : ArrayView[ValidationError] = [],
validator_value? : Json,
instance? : Json,
schema? : Json,
schema_path? : ArrayView[PathItem] = [],
parent? : ValidationError,
type_checker? : TypeChecker,
kind? : ErrorKind = Validation,
) -> ValidationError {
let error = {
kind,
message,
path: path.to_owned(),
schema_path: schema_path.to_owned(),
context: context.to_owned(),
cause,
validator,
validator_value,
instance,
schema,
parent,
type_checker,
}
for e in error.context {
e.parent = Some(error)
}
error
}
///|
/// `SchemaError.create_from(other)`: a copy of `other`'s contents as a
/// `SchemaError` (`kind=Schema`; the type checker is not copied).
pub fn ValidationError::create_from(
other : ValidationError,
kind? : ErrorKind = Schema,
) -> ValidationError {
ValidationError::new(
other.message,
validator?=other.validator,
path=other.path,
cause?=other.cause,
context=other.context,
validator_value?=other.validator_value,
instance?=other.instance,
schema?=other.schema,
schema_path=other.schema_path,
parent?=other.parent,
kind~,
)
}
///|
/// The error as something that can be raised.
pub fn ValidationError::to_error(self : ValidationError) -> JsonSchemaError {
match self.kind {
Validation => ValidationError(self)
Schema => SchemaError(self)
}
}
///|
/// Upstream's class name for this error.
pub fn ValidationError::class_name(self : ValidationError) -> String {
match self.kind {
Validation => "ValidationError"
Schema => "SchemaError"
}
}
///|
/// `repr(error)`: ``.
pub fn ValidationError::repr(self : ValidationError) -> String {
"<\{self.class_name()}: \{@pycompat.repr_string(self.message)}>"
}
///|
/// The failed keyword, when it is a string.
pub fn ValidationError::keyword(self : ValidationError) -> String? {
match self.validator {
Some(String(k)) => Some(k)
_ => None
}
}
///|
/// `relative_path`: same as `path`.
pub fn ValidationError::relative_path(
self : ValidationError,
) -> Array[PathItem] {
self.path
}
///|
/// `relative_schema_path`: same as `schema_path`.
pub fn ValidationError::relative_schema_path(
self : ValidationError,
) -> Array[PathItem] {
self.schema_path
}
///|
/// The path to the offending element within the instance, from the root of
/// the instance.
pub fn ValidationError::absolute_path(
self : ValidationError,
) -> Array[PathItem] {
match self.parent {
None => self.path.copy()
Some(parent) => parent.absolute_path() + self.path
}
}
///|
/// The path to the failing keyword within the schema, from the root schema.
pub fn ValidationError::absolute_schema_path(
self : ValidationError,
) -> Array[PathItem] {
match self.parent {
None => self.schema_path.copy()
Some(parent) => parent.absolute_schema_path() + self.schema_path
}
}
///|
/// `_JSON_PATH_COMPATIBLE_PROPERTY_PATTERN.match(elem)`:
/// `^[a-zA-Z][a-zA-Z0-9_]*$` (where `$` also matches before a final newline).
fn json_path_compatible(elem : String) -> Bool {
let s = if elem.has_suffix("\n") {
elem.unsafe_substring(start=0, end=elem.length() - 1)
} else {
elem
}
if s.length() == 0 {
return false
}
for i, c in s {
let ok = (c >= 'a' && c <= 'z') ||
(c >= 'A' && c <= 'Z') ||
(i > 0 && ((c >= '0' && c <= '9') || c == '_'))
if !ok {
return false
}
}
true
}
///|
/// A JSONPath expression for the offending element (e.g. `$.foo[0]`).
pub fn ValidationError::json_path(self : ValidationError) -> String {
let sb = StringBuilder()
sb.write_char('$')
for elem in self.absolute_path() {
match elem {
Index(i) => sb.write_string("[\{i}]")
Key(k) =>
if json_path_compatible(k) {
sb.write_char('.')
sb.write_string(k)
} else {
let escaped = k
.replace_all(old="\\", new="\\\\")
.replace_all(old="'", new="\\'")
sb.write_string("['\{escaped}']")
}
}
}
sb.to_string()
}
///|
/// Upstream's `_set`: fill in details that are still unset.
fn ValidationError::set_unset(
self : ValidationError,
type_checker~ : TypeChecker,
validator~ : Json,
validator_value~ : Json,
instance~ : Json,
schema~ : Json,
) -> Unit {
if self.type_checker is None {
self.type_checker = Some(type_checker)
}
if self.validator is None {
self.validator = Some(validator)
}
if self.validator_value is None {
self.validator_value = Some(validator_value)
}
if self.instance is None {
self.instance = Some(instance)
}
if self.schema is None {
self.schema = Some(schema)
}
}
///|
/// Upstream's `_matches_type`: whether the instance matches the `type` of the
/// schema the error is in.
fn ValidationError::matches_type(self : ValidationError) -> Bool {
guard self.schema is Some(Object({ "type": expected, .. })) else {
return false
}
guard self.type_checker is Some(checker) else { return false }
let instance = self.instance.unwrap_or(Json::null())
let check = (t : Json) => {
match t {
String(name) => checker.is_type(instance, name) catch { _ => false }
_ => false
}
}
match expected {
String(_) => check(expected)
Array(types) => types.iter().any(check)
_ => false
}
}
///|
/// Format something for an error message as prettily as we currently can.
fn pretty(thing : Json, prefix : String) -> String {
let text = @pycompat.text_indent(@pycompat.pformat(thing, width=72), prefix)
// .lstrip()
let cps = @unicodedata.code_points(text)
let mut i = 0
while i < cps.length() && @unicodedata.is_space(cps[i]) {
i += 1
}
@unicodedata.from_code_points(cps[i:])
}
///|
/// Python's `str.rstrip()`.
fn rstrip(s : String) -> String {
let cps = @unicodedata.code_points(s)
let mut n = cps.length()
while n > 0 && @unicodedata.is_space(cps[n - 1]) {
n -= 1
}
@unicodedata.from_code_points(cps[:n])
}
///|
/// `str(error)`: the message, followed (when the details are known) by the
/// failing keyword, schema and instance, exactly like upstream.
pub impl Show for ValidationError with fn output(self, logger) {
guard self.validator is Some(validator) &&
self.validator_value is Some(_) &&
self.instance is Some(instance) &&
self.schema is Some(schema) else {
logger.write_string(self.message)
return
}
let (word_schema, word_instance) = match self.kind {
Validation => ("schema", "instance")
Schema => ("metaschema", "schema")
}
let schema_path = format_as_index(
word_schema,
self.schema_path[:@cmp.maximum(self.schema_path.length() - 1, 0)],
)
let instance_path = format_as_index(word_instance, self.path)
let prefix = " ".repeat(16)
let indent = " ".repeat(12)
let text = "\{indent}\{self.message}\n\n" +
"\{indent}Failed validating \{@pycompat.repr(validator)} in \{schema_path}:\n" +
"\{indent} \{pretty(schema, prefix)}\n\n" +
"\{indent}On \{instance_path}:\n" +
"\{indent} \{pretty(instance, prefix)}\n" +
indent
logger.write_string(@pycompat.text_dedent(rstrip(text)))
}
///|
/// Raised when a validator is asked to validate against an unknown type
/// (`jsonschema.exceptions.UnknownType`).
pub suberror UnknownType {
UnknownType(type_~ : Json, instance~ : Json, schema~ : Json)
}
///|
pub impl Show for UnknownType with fn output(self, logger) {
let UnknownType(type_~, instance~, schema~) = self
let prefix = " ".repeat(16)
let indent = " ".repeat(12)
let text = "\{indent}Unknown type \{@pycompat.repr(type_)} for validator with schema:\n" +
"\{indent} \{pretty(schema, prefix)}\n\n" +
"\{indent}While checking instance:\n" +
"\{indent} \{pretty(instance, prefix)}\n" +
indent
logger.write_string(@pycompat.text_dedent(rstrip(text)))
}
///|
/// Raised by `FormatChecker::check` when an instance does not conform
/// (`jsonschema.exceptions.FormatError`).
pub suberror FormatError {
FormatError(message~ : String, cause~ : Error?)
}
///|
pub impl Show for FormatError with fn output(self, logger) {
let FormatError(message~, ..) = self
logger.write_string(message)
}