// The JSON Schema specifications (Python's `referencing.jsonschema`).
//
// These live in the root package (rather than only in
// `bobzhang/referencing/jsonschema`) because `Specification::detect` /
// `Resource::from_contents` need `specification_with`, and MoonBit packages
// cannot be mutually dependent. `bobzhang/referencing/jsonschema` re-exports
// them under the upstream module layout.
///|
fn draft_specification(
name~ : String,
id_of~ : (Json) -> String?,
anchors_in~ : (Specification, Json) -> Array[Anchor],
in_value~ : Array[String],
in_subarray~ : Array[String],
in_subvalues~ : Array[String],
quirks~ : Quirks,
pointer_in_value? : Array[String] = in_value,
pointer_quirks~ : PointerQuirks,
) -> Specification {
Specification::new(
name~,
id_of~,
subresources_of=make_subresources_of(
in_value~,
in_subarray~,
in_subvalues~,
quirks~,
),
anchors_in~,
maybe_in_subresource=make_maybe_in_subresource(
in_value=pointer_in_value,
in_subarray~,
in_subvalues~,
quirks=pointer_quirks,
),
)
}
///|
/// JSON Schema draft 2020-12.
pub fn draft202012() -> Specification {
cached_specification("draft202012", () => {
draft_specification(
name="draft2020-12",
id_of=dollar_id,
anchors_in=anchors_2020,
in_value=[
"additionalProperties", "contains", "contentSchema", "else", "if", "items",
"not", "propertyNames", "then", "unevaluatedItems", "unevaluatedProperties",
],
in_subarray=["allOf", "anyOf", "oneOf", "prefixItems"],
in_subvalues=[
"$defs", "definitions", "dependentSchemas", "patternProperties", "properties",
],
quirks=NoQuirks,
pointer_quirks=NoPointerQuirks,
)
})
}
///|
/// JSON Schema draft 2019-09.
pub fn draft201909() -> Specification {
cached_specification("draft201909", () => {
draft_specification(
name="draft2019-09",
id_of=dollar_id,
anchors_in=anchors_2019,
in_value=[
"additionalItems", "additionalProperties", "contains", "contentSchema", "else",
"if", "not", "propertyNames", "then", "unevaluatedItems", "unevaluatedProperties",
],
in_subarray=["allOf", "anyOf", "oneOf"],
in_subvalues=[
"$defs", "definitions", "dependentSchemas", "patternProperties", "properties",
],
quirks=CrazyItems,
pointer_quirks=ItemsPointer,
)
})
}
///|
/// JSON Schema draft 7.
pub fn draft7() -> Specification {
cached_specification("draft7", () => {
draft_specification(
name="draft-07",
id_of=legacy_dollar_id,
anchors_in=legacy_anchor_in_dollar_id,
in_value=[
"additionalItems", "additionalProperties", "contains", "else", "if", "not",
"propertyNames", "then",
],
in_subarray=["allOf", "anyOf", "oneOf"],
in_subvalues=["definitions", "patternProperties", "properties"],
quirks=CrazyItemsDependencies,
pointer_quirks=ItemsDependenciesPointer,
)
})
}
///|
/// JSON Schema draft 6.
pub fn draft6() -> Specification {
cached_specification("draft6", () => {
draft_specification(
name="draft-06",
id_of=legacy_dollar_id,
anchors_in=legacy_anchor_in_dollar_id,
in_value=[
"additionalItems", "additionalProperties", "contains", "not", "propertyNames",
],
in_subarray=["allOf", "anyOf", "oneOf"],
in_subvalues=["definitions", "patternProperties", "properties"],
quirks=CrazyItemsDependencies,
pointer_quirks=ItemsDependenciesPointer,
)
})
}
///|
/// JSON Schema draft 4.
pub fn draft4() -> Specification {
cached_specification("draft4", () => {
draft_specification(
name="draft-04",
id_of=legacy_id,
anchors_in=legacy_anchor_in_id,
in_value=["not"],
pointer_in_value=["additionalItems", "additionalProperties", "not"],
in_subarray=["allOf", "anyOf", "oneOf"],
in_subvalues=["definitions", "patternProperties", "properties"],
quirks=CrazyAPItemsDependencies,
pointer_quirks=ItemsDependenciesPointer,
)
})
}
///|
/// JSON Schema draft 3.
pub fn draft3() -> Specification {
cached_specification("draft3", () => {
draft_specification(
name="draft-03",
id_of=legacy_id,
anchors_in=legacy_anchor_in_id,
in_value=[],
pointer_in_value=["additionalItems", "additionalProperties"],
in_subarray=["extends"],
in_subvalues=["definitions", "patternProperties", "properties"],
quirks=CrazyAPItemsDependencies,
pointer_quirks=ItemsDependenciesPointer,
)
})
}
///|
/// The specification for a known dialect identifier (without trailing `#`).
fn known_specification(dialect_id : String) -> Specification? {
match dialect_id {
"https://json-schema.org/draft/2020-12/schema" => Some(draft202012())
"https://json-schema.org/draft/2019-09/schema" => Some(draft201909())
"http://json-schema.org/draft-07/schema" => Some(draft7())
"http://json-schema.org/draft-06/schema" => Some(draft6())
"http://json-schema.org/draft-04/schema" => Some(draft4())
"http://json-schema.org/draft-03/schema" => Some(draft3())
_ => None
}
}
///|
/// The specifications are built lazily (once) rather than being top-level
/// `let`s, since their callbacks (transitively) refer back to
/// `specification_with`, which MoonBit rejects as a definition cycle among
/// top-level values. Each function always returns the very same object.
let specification_cache : Map[String, Specification] = Map([])
///|
fn cached_specification(
key : String,
build : () -> Specification,
) -> Specification {
match specification_cache.get(key) {
Some(specification) => specification
None => {
let specification = build()
specification_cache[key] = specification
specification
}
}
}
///|
/// The empty JSON Schema registry (Python's `EMPTY_REGISTRY`).
pub let empty_registry : Registry = Registry::new()
///|
/// Retrieve the `Specification` with the given dialect identifier (trailing
/// `#`s are ignored).
///
/// Raises `UnknownDialect` if the given `dialect_id` isn't known and no
/// `default` is given.
///
/// ```mbt check
/// test {
/// let spec = @referencing.specification_with(
/// "http://json-schema.org/draft-07/schema#",
/// )
/// inspect(spec, content="")
/// }
/// ```
pub fn specification_with(
dialect_id : String,
default? : Specification,
) -> Specification raise ReferencingError {
match known_specification(rstrip_hash(dialect_id)) {
Some(specification) => specification
None =>
match default {
Some(default) => default
None => raise UnknownDialect(uri=dialect_id)
}
}
}
///|
/// Create a dynamic anchor, introduced in draft 2020-12 (Python's
/// `DynamicAnchor(name=..., resource=...)`). Its `kind` is `"DynamicAnchor"`.
///
/// Resolving it walks the resolver's dynamic scope (outermost last), taking
/// the outermost resource which also declares a dynamic anchor with this
/// name, and enters it as a subresource.
pub fn dynamic_anchor(name~ : String, resource~ : Resource) -> Anchor {
Anchor::custom(
name~,
resource~,
kind="DynamicAnchor",
resolve=resolve_dynamic,
)
}
///|
/// Whether this anchor is a `DynamicAnchor` (Python's
/// `isinstance(anchor, DynamicAnchor)`).
pub fn Anchor::is_dynamic(self : Anchor) -> Bool {
self.kind() == "DynamicAnchor"
}
///|
/// Python's `DynamicAnchor.resolve`.
fn resolve_dynamic(anchor_ : Anchor, resolver : Resolver) -> Resolved raise {
let mut last = anchor_.resource
for scope in resolver.dynamic_scope() {
let (uri, registry) = scope
let anchor = registry.anchor(uri, anchor_.name) catch {
NoSuchAnchor(..) => continue
error => raise error
}
if anchor.value.is_dynamic() {
last = anchor.value.resource
}
}
{ contents: last.contents, resolver: resolver.in_subresource(last), }
}
///|
/// Recursive references (via recursive anchors), present only in draft
/// 2019-09.
///
/// As per the 2019 specification (ยง 8.2.4.2.1), only the `#` recursive
/// reference is supported (and is therefore assumed to be the relevant
/// reference).
pub fn lookup_recursive_ref(resolver : Resolver) -> Resolved raise {
let mut resolved = resolver.lookup("#")
if has_truthy_recursive_anchor(resolved.contents) {
for scope in resolver.dynamic_scope() {
let next_resolved = resolver.lookup(scope.0)
if !has_truthy_recursive_anchor(next_resolved.contents) {
break
}
resolved = next_resolved
}
}
resolved
}
///|
fn has_truthy_recursive_anchor(contents : Json) -> Bool {
contents is Object(obj) &&
obj.get("$recursiveAnchor") is Some(value) &&
py_truthy(value)
}