///|
/// A keyword implementation (upstream's `SchemaKeywordValidator`): given the
/// validator, the keyword's value, the instance and the (sub)schema
/// containing the keyword, the (lazy) errors.
pub type KeywordFn = (Validator, Json, Json, Json) -> Errors
///|
/// Which keywords of a schema apply (upstream's `applicable_validators`).
pub type ApplicableValidators = (Json) -> Array[(String, Json)]
///|
/// A validator class (upstream: the class returned by `create`, e.g.
/// `Draft202012Validator`): the keyword implementations, meta-schema, type
/// checker and format checker of a JSON Schema dialect.
///
/// Create validators for a schema with `ValidatorClass::new`.
pub struct ValidatorClass {
/// The class name, e.g. `Draft7Validator`.
name : String
/// `VALIDATORS`: keyword name to implementation, in definition order.
validators : Map[String, KeywordFn]
/// `META_SCHEMA`
meta_schema : Json
/// `TYPE_CHECKER`
type_checker : TypeChecker
/// `FORMAT_CHECKER`: the checker to use when format validation is wanted
/// (validators only check formats when given a `format_checker`).
format_checker : FormatChecker
/// `ID_OF`
id_of : (Json) -> String?
/// `_APPLICABLE_VALIDATORS`
applicable_validators : ApplicableValidators
priv specification : @referencing.Specification
}
///|
pub impl Show for ValidatorClass with fn output(self, logger) {
logger.write_string("")
}
///|
/// A validator for a particular schema (an instance of a validator class).
pub struct Validator {
/// The validator class.
cls : ValidatorClass
/// The schema being validated against.
schema : Json
/// The format checker in use, if any (format validation is off without
/// one).
format_checker : FormatChecker?
priv registry : @referencing.Registry?
priv resolver : @referencing.Resolver
priv validators : Array[(KeywordFn, String, Json)]
}
///|
/// `repr(validator)`: e.g. `Draft7Validator(schema={'type': 'string'},
/// format_checker=None)` (the schema is abbreviated like `reprlib.repr`).
pub impl Show for Validator with fn output(self, logger) {
let fc = match self.format_checker {
Some(fc) => "\{fc}"
None => "None"
}
logger.write_string(
"\{self.cls.name}(schema=\{@pycompat.short_repr(self.schema)}, format_checker=\{fc})",
)
}
///|
/// `jsonschema_specifications.REGISTRY`: all meta-schemas and vocabularies.
pub fn specifications_registry() -> @referencing.Registry {
match specifications_cache.val {
Some(r) => r
None => {
let resources = @specifications.schemas().map(pair => {
@referencing.Resource::from_contents(pair.1) catch {
e => abort("cannot load \{pair.0}: \{e}")
}
})
let registry = @referencing.Registry::new().with_identified_resources(
resources,
) catch {
e => abort("cannot load the specifications: \{e}")
}
let registry = registry.crawl() catch {
e => abort("cannot crawl the specifications: \{e}")
}
specifications_cache.val = Some(registry)
registry
}
}
}
///|
let specifications_cache : Ref[@referencing.Registry?] = { val: None, }
///|
/// `_VALIDATORS`: version name to validator class.
let validators_by_version : Map[String, ValidatorClass] = Map([])
///|
/// `_META_SCHEMAS`: normalized meta-schema id to validator class.
let meta_schemas : Map[String, ValidatorClass] = Map([])
///|
/// `URIDict.normalize`: `urlsplit(uri).geturl()`.
fn normalize_uri(uri : String) -> String {
@urllib.urlsplit(uri).geturl() catch {
_ => uri
}
}
///|
/// Register a validator class for a `version` of the specification
/// (upstream's `validates` decorator): it is then considered when looking
/// up `$schema` URIs.
pub fn validates(cls : ValidatorClass, version : String) -> ValidatorClass {
validators_by_version[version] = cls
match (cls.id_of)(cls.meta_schema) {
Some(id) => meta_schemas[normalize_uri(id)] = cls
None => meta_schemas[normalize_uri("")] = cls
}
cls
}
///|
/// The registered validator classes (upstream's `_VALIDATORS`), keyed by
/// version name.
pub fn registered_validators() -> Map[String, ValidatorClass] {
validators_by_version.copy()
}
///|
fn default_applicable(schema : Json) -> Array[(String, Json)] {
match schema {
Object(m) => m.iter().collect()
_ => []
}
}
///|
/// Create a new validator class (upstream's `create`).
///
/// `validators` maps keyword names to their implementations. When a
/// `version` is given, the class is named after it (`version.title()` minus
/// spaces and dashes, plus `Validator`) and registered with `validates`.
pub fn create(
meta_schema~ : Json,
validators? : ArrayView[(String, KeywordFn)] = [],
version? : String,
type_checker? : TypeChecker = draft202012_type_checker,
format_checker? : FormatChecker = draft202012_format_checker,
id_of? : (Json) -> String? = fn(j) { @referencing.draft202012().id_of(j) },
applicable_validators? : ApplicableValidators = default_applicable,
) -> ValidatorClass {
let dialect_id = id_of(meta_schema).unwrap_or("urn:unknown-dialect")
let specification = @referencing.specification_with(
dialect_id,
default=@referencing.opaque_specification,
) catch {
_ => @referencing.opaque_specification
}
let keyword_map = Map([])
for pair in validators {
keyword_map[pair.0] = pair.1
}
let name = match version {
Some(v) =>
title_case(v).replace_all(old=" ", new="").replace_all(old="-", new="") +
"Validator"
None => "Validator"
}
let cls = {
name,
validators: keyword_map,
meta_schema,
type_checker,
format_checker,
id_of,
applicable_validators,
specification,
}
match version {
Some(v) => validates(cls, v)
None => cls
}
}
///|
/// Python's `str.title()` for ASCII text.
fn title_case(s : String) -> String {
let sb = StringBuilder()
let mut prev_letter = false
for c in s {
let is_letter = (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
if is_letter {
if prev_letter {
sb.write_char(
if c >= 'A' && c <= 'Z' {
Int::unsafe_to_char(c.to_int() + 32)
} else {
c
},
)
} else {
sb.write_char(
if c >= 'a' && c <= 'z' {
Int::unsafe_to_char(c.to_int() - 32)
} else {
c
},
)
}
} else {
sb.write_char(c)
}
prev_letter = is_letter
}
sb.to_string()
}
///|
/// Create a new validator class by extending an existing one (upstream's
/// `extend`; renamed as `extend` is reserved in MoonBit). Keyword implementations in `validators` replace any existing
/// ones with the same name.
pub fn ValidatorClass::extend_with(
cls : ValidatorClass,
validators? : ArrayView[(String, KeywordFn)] = [],
version? : String,
type_checker? : TypeChecker,
format_checker? : FormatChecker,
) -> ValidatorClass {
let all = cls.validators.iter().collect()
for pair in validators {
match all.search_by(p => p.0 == pair.0) {
Some(i) => all[i] = pair
None => all.push(pair)
}
}
create(
meta_schema=cls.meta_schema,
validators=all,
version?,
type_checker=type_checker.unwrap_or(cls.type_checker),
format_checker=format_checker.unwrap_or(cls.format_checker),
id_of=cls.id_of,
applicable_validators=cls.applicable_validators,
)
}
///|
/// Retrieve the validator class appropriate for validating the given
/// schema, from its `$schema` keyword (upstream's `validator_for`).
///
/// Schemas without (a known) `$schema` get `default`, or the latest
/// supported draft.
pub fn validator_for(
schema : Json,
default? : ValidatorClass,
) -> ValidatorClass {
let default_validator = match default {
Some(d) => d
None => latest_version()
}
match schema {
Object({ "$schema": String(dialect), .. }) =>
match meta_schemas.get(normalize_uri(dialect)) {
Some(cls) => cls
None => default_validator
}
_ => default_validator
}
}
///|
/// Validate `schema` against its class's meta-schema (upstream's
/// `check_schema`), raising `SchemaError` for the first problem found.
///
/// Format validation of the schema uses the meta-schema's validator class's
/// `FORMAT_CHECKER` unless `format_checker` is given.
pub fn ValidatorClass::check_schema(
self : ValidatorClass,
schema : Json,
format_checker? : FormatChecker?,
) -> Unit raise {
let validator_cls = validator_for(self.meta_schema, default=self)
let format_checker = match format_checker {
Some(fc) => fc
None => Some(validator_cls.format_checker)
}
let validator = validator_cls.new(self.meta_schema, format_checker?)
match validator.iter_errors(schema).first() {
Some(error) => raise SchemaError(ValidationError::create_from(error))
None => ()
}
}
///|
/// Create a validator for `schema` (upstream: calling the class).
///
/// `registry` holds the resources `$ref`s may refer to (it is combined with
/// `specifications_registry()`); `format_checker` enables format
/// validation.
pub fn ValidatorClass::new(
self : ValidatorClass,
schema : Json,
format_checker? : FormatChecker,
registry? : @referencing.Registry,
) -> Validator raise {
let combined = match registry {
None => specifications_registry()
Some(r) => specifications_registry().combine([r])
}
if (schema is True || schema is False) &&
(
self.specification.name == "draft-03" ||
self.specification.name == "draft-04"
) {
// `referencing.jsonschema._legacy_id` does `"$ref" in contents`.
raise PythonError("TypeError: argument of type 'bool' is not iterable")
}
let resource = self.specification.create_resource(schema)
let resolver = combined.resolver_with_root(resource)
Validator::make(self, schema, format_checker, registry, resolver)
}
///|
fn Validator::make(
cls : ValidatorClass,
schema : Json,
format_checker : FormatChecker?,
registry : @referencing.Registry?,
resolver : @referencing.Resolver,
) -> Validator {
let validators = match schema {
True | False => []
_ =>
(cls.applicable_validators)(schema).filter_map(pair => {
match cls.validators.get(pair.0) {
Some(f) => Some((f, pair.0, pair.1))
None => None
}
})
}
{ cls, schema, format_checker, registry, resolver, validators, }
}
///|
/// A copy of this validator with some attributes changed (upstream's
/// `evolve`). When the schema changes, the validator class is re-selected
/// with `validator_for(schema, default=self.cls)`.
pub fn Validator::evolve(
self : Validator,
schema? : Json,
format_checker? : FormatChecker?,
resolver? : @referencing.Resolver,
) -> Validator {
let schema = schema.unwrap_or(self.schema)
let cls = validator_for(schema, default=self.cls)
let format_checker = match format_checker {
Some(fc) => fc
None => self.format_checker
}
Validator::make(
cls,
schema,
format_checker,
self.registry,
resolver.unwrap_or(self.resolver),
)
}
///|
/// The resolver used for `$ref`s in the current schema.
pub fn Validator::resolver(self : Validator) -> @referencing.Resolver {
self.resolver
}
///|
fn false_schema_error(
instance : Json,
schema : Json,
path? : PathItem,
schema_path? : PathItem,
) -> ValidationError {
ValidationError::new(
"False schema does not allow \{@pycompat.repr(instance)}",
validator=Json::null(),
validator_value=Json::null(),
instance~,
schema~,
path=match path {
Some(p) => [p]
None => []
},
schema_path=match schema_path {
Some(p) => [p]
None => []
},
)
}
///|
/// Lazily yield each of the errors in the given instance (upstream's
/// `iter_errors`).
pub fn Validator::iter_errors(self : Validator, instance : Json) -> Errors {
let schema = self.schema
Errors::new(fn(yield_) raise {
match schema {
True => return true
False => return yield_(false_schema_error(instance, schema))
_ => ()
}
for entry in self.validators {
let (f, k, v) = entry
let ok = f(self, v, instance, schema).run(fn(error) raise {
error.set_unset(
validator=Json::string(k),
validator_value=v,
instance~,
schema~,
type_checker=self.cls.type_checker,
)
if k != "if" && k != "$ref" {
error.schema_path.insert(0, Key(k))
}
yield_(error)
})
if !ok {
return false
}
}
true
})
}
///|
/// Validate `instance` against a subschema, prefixing the errors' paths
/// (upstream's `descend`).
pub fn Validator::descend(
self : Validator,
instance : Json,
schema : Json,
path? : PathItem,
schema_path? : PathItem,
resolver? : @referencing.Resolver,
) -> Errors {
Errors::new(fn(yield_) raise {
match schema {
True => return true
False =>
return yield_(false_schema_error(instance, schema, path?, schema_path?))
_ => ()
}
let resolver = match resolver {
Some(r) => r
None =>
self.resolver.in_subresource(
self.cls.specification.create_resource(schema),
)
}
let evolved = self.evolve(schema~, resolver~)
for pair in (self.cls.applicable_validators)(schema) {
let (k, v) = pair
guard evolved.cls.validators.get(k) is Some(f) else { continue }
let ok = f(evolved, v, instance, schema).run(fn(error) raise {
error.set_unset(
validator=Json::string(k),
validator_value=v,
instance~,
schema~,
type_checker=evolved.cls.type_checker,
)
if k != "if" && k != "$ref" {
error.schema_path.insert(0, Key(k))
}
if path is Some(p) {
error.path.insert(0, p)
}
if schema_path is Some(p) {
error.schema_path.insert(0, p)
}
yield_(error)
})
if !ok {
return false
}
}
true
})
}
///|
/// Check if the instance is valid under the current schema, raising the
/// first error otherwise (upstream's `validate` method).
pub fn Validator::validate(self : Validator, instance : Json) -> Unit raise {
match self.iter_errors(instance).first() {
Some(error) => raise ValidationError(error)
None => ()
}
}
///|
/// Check if the instance is of the given (JSON Schema) type, raising
/// `UnknownType` for types the type checker does not know.
pub fn Validator::is_type(
self : Validator,
instance : Json,
type_ : String,
) -> Bool raise {
self.cls.type_checker.is_type(instance, type_) catch {
UndefinedTypeCheck(_) =>
raise UnknownType(
type_=Json::string(type_),
instance~,
schema=self.schema,
)
e => raise e
}
}
///|
/// Whether the instance is valid under the current schema (stops at the
/// first error).
pub fn Validator::is_valid(self : Validator, instance : Json) -> Bool raise {
self.iter_errors(instance).is_empty()
}
///|
/// Upstream's `_validate_reference`: look `ref` up and validate against
/// what it points to.
fn Validator::validate_reference(
self : Validator,
ref_ : String,
instance : Json,
) -> Errors raise {
let resolved = self.resolver.lookup(ref_)
self.descend(instance, resolved.contents, resolver=resolved.resolver)
}
///|
/// Validate an instance under the given schema (upstream's
/// `jsonschema.validate`): the schema is checked first (raising
/// `SchemaError`), then the best match among the instance's errors is
/// raised as a `ValidationError`.
///
/// Without `cls`, the validator class is chosen from the schema's
/// `$schema` (see `validator_for`).
pub fn validate(
instance : Json,
schema : Json,
cls? : ValidatorClass,
format_checker? : FormatChecker,
registry? : @referencing.Registry,
) -> Unit raise {
let cls = match cls {
Some(c) => c
None => validator_for(schema)
}
cls.check_schema(schema)
let validator = cls.new(schema, format_checker?, registry?)
let errors = validator.iter_errors(instance).to_array()
match best_match(errors.iter()) {
Some(error) => raise ValidationError(error)
None => ()
}
}