///|
fn string_set(ctx : Context, value : Node, name : String) -> Array[String] {
let result : Array[String] = []
match field(value.raw, name) {
None => ()
Some(Array(items)) => {
if items.is_empty() {
ctx.diagnose(
"invalid-schema",
"error",
value,
name + " must not be empty.",
)
}
for item in items {
match item {
String(s) =>
if result.contains(s) {
ctx.diagnose(
"invalid-schema",
"error",
value,
name + " contains duplicates.",
)
} else {
result.push(s)
}
_ =>
ctx.diagnose(
"unsupported-schema",
"unsupported",
value,
name + " must contain strings in the V1 profile.",
)
}
}
}
Some(_) =>
ctx.diagnose(
"invalid-schema",
"error",
value,
name + " must be an array.",
)
}
result.sort()
result
}
///|
fn validate_bool(ctx : Context, value : Node, key : String) -> Unit {
match field(value.raw, key) {
None | Some(True) | Some(False) => ()
_ =>
ctx.diagnose(
"invalid-boolean",
"error",
value,
key + " must be a boolean.",
)
}
}
///|
fn scan_schema(ctx : Context, raw : Node, depth : Int) -> Unit {
guard ctx.visit(raw) else { return }
if depth > 64 {
ctx.diagnose(
"schema-depth-limit", "unsupported", raw, "Schema traversal exceeds 64 levels.",
)
return
}
guard ctx.resolve(raw) is Some(value) else { return }
guard value.raw is Object(_) else {
ctx.diagnose("invalid-schema", "error", value, "Schema must be an object.")
return
}
for key in keys(value.raw) {
if ["allOf", "anyOf", "oneOf", "not", "discriminator", "items"].contains(
key,
) {
ctx.diagnose(
"unsupported-schema",
"unsupported",
value,
"Unsupported Schema keyword: " + key,
)
} else if !ignored_key(key) &&
![
"type", "properties", "required", "enum", "default", "nullable", "readOnly",
"writeOnly", "additionalProperties", "format", "minimum", "maximum", "exclusiveMinimum",
"exclusiveMaximum", "multipleOf", "minLength", "maxLength", "pattern", "minProperties",
"maxProperties", "xml",
].contains(key) {
ctx.diagnose(
"unsupported-schema",
"unsupported",
value,
"Unknown Schema keyword: " + key,
)
}
}
let kind = string_value(field(value.raw, "type"))
numeric_constraints(ctx, value)
if ![
Some("object"),
Some("string"),
Some("number"),
Some("integer"),
Some("boolean"),
].contains(kind) {
ctx.diagnose(
"unsupported-schema", "unsupported", value, "V1 requires an explicit scalar or object type after reference resolution.",
)
}
for
key in [
"nullable", "readOnly", "writeOnly", "exclusiveMinimum", "exclusiveMaximum",
] {
validate_bool(ctx, value, key)
}
if is_true(field(value.raw, "readOnly")) ||
is_true(field(value.raw, "writeOnly")) {
ctx.diagnose(
"unsupported-schema", "unsupported", value, "readOnly/writeOnly request projection is outside V1.",
)
}
if field(value.raw, "enum") is Some(_) {
if kind != Some("string") {
ctx.diagnose(
"unsupported-enum", "unsupported", value, "V1 checks only string enums.",
)
}
ignore(string_set(ctx, value, "enum"))
}
match field(value.raw, "additionalProperties") {
None | Some(True) | Some(False) => ()
Some(Object(_)) =>
ctx.diagnose(
"unsupported-schema", "unsupported", value, "Schema-valued additionalProperties is outside V1.",
)
_ =>
ctx.diagnose(
"invalid-schema", "error", value, "additionalProperties must be boolean or a Schema.",
)
}
let required = string_set(ctx, value, "required")
let props = field(value.raw, "properties").unwrap_or(Json::empty_object())
if !(props is Object(_)) {
ctx.diagnose(
"invalid-schema", "error", value, "properties must be an object.",
)
}
if kind != Some("object") &&
(field(value.raw, "properties") is Some(_) || !required.is_empty()) {
ctx.diagnose(
"unsupported-schema", "unsupported", value, "Object constraints require type object in V1.",
)
}
for name in required {
if field(props, name) is None {
ctx.diagnose(
"unsupported-schema",
"unsupported",
value,
"Required names without declared properties are outside V1: " + name,
)
}
}
let properties = value.child("properties", props)
for name in keys(props) {
if ctx.budget.exhausted {
break
}
scan_schema(
ctx,
properties.child(name, field(props, name).unwrap()),
depth + 1,
)
}
}
///|
fn properties(value : Node) -> Map[String, Node] {
let result : Map[String, Node] = Map([])
if field(value.raw, "properties") is Some(raw) {
let parent = value.child("properties", raw)
for name in keys(raw) {
result[name] = parent.child(name, field(raw, name).unwrap())
}
}
result
}