///|
/// One pinned-artifact violation, relative to the document root.
priv struct Violation {
code : @exact.Code
message : String
path : Array[String]
expected : String?
}
///|
/// The pinned metaschema for a supported version, parsed from the generated
/// pruned copy. Generation proves every keyword in it is interpreted here.
fn artifact_schema(version : DocumentVersion) -> @exact.Value {
let text = if legacy(version) { artifact_1_3_2 } else { artifact_1_4 }
let limits = @exact.Limits::unlimited()
match @exact.parse(text, limits~) {
Ok(cst) =>
match cst.lower() {
Ok(value) => value
Err(_) => abort("Generated artifact schema invariant")
}
Err(_) => abort("Generated artifact schema invariant")
}
}
///|
/// The three patterns the generator admits, as fixed matchers.
fn pattern_matches(pattern : String, text : String) -> Bool {
match pattern {
"^x-" => text.has_prefix("x-")
"[0-z]+" => text.code_units().iter().any(c => c >= 48 && c <= 122)
"^1\\.4\\.\\d+$" => {
guard text.has_prefix("1.4.") && text.length() > 4 else { return false }
text[4:].code_units().iter().all(c => c >= 48 && c <= 57)
}
_ => abort("Generated artifact pattern invariant")
}
}
///|
fn type_matches(name : String, value : @exact.Value) -> Bool {
match (name, value.kind()) {
("object", @exact.Object(_)) => true
("array", @exact.Array(_)) => true
("string", @exact.String(_)) => true
("boolean", @exact.Boolean(_)) => true
("null", @exact.Null) => true
("number", @exact.Numeric(_)) => true
("integer", @exact.Numeric(n)) => n.is_integer()
_ => false
}
}
///|
fn code_points(text : String) -> Int {
let mut count = 0
for _ in text {
count += 1
}
count
}
///|
/// Evaluate the draft-07 subset used by the pinned metaschemas. Work is an
/// explicit stack; only `oneOf` evaluates branches separately, and its
/// nesting depth is fixed by the schema, not by the document.
fn evaluate(
root : @exact.Value,
schema : @exact.Value,
value : @exact.Value,
path : Array[String],
) -> Array[Violation] {
let out = []
let stack = [(schema, value, path)]
while stack.pop() is Some((schema, value, path)) {
let fail = (code, message, expected) => {
out.push({ code, message, path, expected })
}
match schema.kind() {
@exact.Boolean(true) => continue
@exact.Boolean(false) => {
fail(@exact.InvalidValue, "No value is allowed here", None)
continue
}
_ => ()
}
if schema.get("$ref").bind(r => r.text()) is Some(target) {
if target.has_prefix("#/definitions/") {
match root.get("definitions").bind(d => d.get(target[14:].to_owned())) {
Some(definition) => stack.push((definition, value, path))
None => abort("Generated artifact reference invariant")
}
} else if target == "https://meta.json-schema.tools" {
// Keyword validity is the schema-document facet's draft-07 check.
if !(value.kind() is (@exact.Boolean(_) | @exact.Object(_))) {
fail(@exact.WrongType, "Expected a boolean or object schema", None)
}
} else if value.text() is None {
fail(@exact.WrongType, "Expected a reference string", None)
}
continue
}
if schema.get("type") is Some(expected) {
let names = match expected.kind() {
@exact.String(name) => [name]
@exact.Array(items) => items.filter_map(item => item.text())
_ => []
}
if !names.iter().any(name => type_matches(name, value)) {
fail(@exact.WrongType, "Expected " + names.join(" or "), None)
continue
}
}
if schema.get("enum") is Some(allowed) && allowed.kind() is @exact.Array(items) {
let limits = @exact.Limits::unlimited()
if !items.iter().any(item => item.equals(value, limits~) is Ok(true)) {
fail(@exact.InvalidValue, "Value is not one of the allowed values", None)
}
}
if value.text() is Some(text) {
if schema.get("pattern").bind(p => p.text()) is Some(pattern) &&
!pattern_matches(pattern, text) {
fail(@exact.InvalidValue, "Value does not match " + pattern, None)
}
if schema.get("minLength") is Some(minimum) &&
minimum.kind() is @exact.Numeric(n) &&
n.numeric_compare(@exact.Number::parse(code_points(text).to_string()).unwrap()) >
0 {
fail(@exact.InvalidValue, "Value is shorter than the minimum length", None)
}
}
if schema.get("oneOf") is Some(branches) &&
branches.kind() is @exact.Array(branches) {
let results = branches.map(branch => evaluate(root, branch, value, path))
let passed = results.filter(r => r.is_empty()).length()
if passed > 1 {
fail(@exact.InvalidValue, "Value matches more than one alternative", None)
} else if passed == 0 {
// Report the alternative the value was written as: a reference when
// it has `$ref`, otherwise the object alternative.
let reference = value.get("$ref") is Some(_)
let chosen = branches.search_by(branch => {
let to_reference = branch
.get("$ref")
.bind(r => r.text())
.map(t => t == "#/definitions/referenceObject")
.unwrap_or(false)
to_reference == reference
})
out.append(results[chosen.unwrap_or(0)])
}
}
match value.kind() {
@exact.Object(fields) => {
if schema.get("required") is Some(required) &&
required.kind() is @exact.Array(names) {
for name in names {
if name.text() is Some(name) && value.get(name) is None {
fail(@exact.MissingField, "Missing required field " + name, Some(name))
}
}
}
let properties = schema.get("properties")
let patterns = match schema.get("patternProperties").map(p => p.kind()) {
Some(@exact.Object(entries)) => entries
_ => []
}
let additional = schema.get("additionalProperties")
for i in 0.. p.get(name)) is Some(property) {
matched = true
stack.push((property, item, at))
}
for entry in patterns {
if pattern_matches(entry.0, name) {
matched = true
stack.push((entry.1, item, at))
}
}
if !matched && additional is Some(additional) {
match additional.kind() {
@exact.Boolean(false) =>
out.push({
code: @exact.UnknownField,
message: "Field is not allowed by the pinned metaschema",
path: at,
expected: None,
})
_ => stack.push((additional, item, at))
}
}
}
}
@exact.Array(items) =>
if schema.get("items") is Some(each) {
for i in 0.. ()
}
}
out
}
///|
/// Check the document's own JSON against its version's pinned metaschema.
/// External resources are not documents and are not artifact-checked.
fn Check::artifact(
self : Check,
document : Document,
owner : @reference.UriReference?,
) -> Unit {
let schema = artifact_schema(document.version)
for v in evaluate(schema, schema, document.value, []) {
add(
self.findings,
v.code,
v.message,
v.path,
Artifact,
resource=owner,
expected=v.expected,
)
}
}