///|
/// JSON Schema export.
///
/// The export_schema operation turns an inferred or hand-maintained
/// engine schema (dotted path to type name) into a JSON Schema draft
/// 2020-12 document: nested objects become `properties` trees, dotted
/// segments after an array index are collapsed into `items`, and
/// engine type names map to JSON Schema types one-to-one (`null` to
/// `{"type": "null"}`, arrays to untyped `items`). The result is a
/// standard document external validators accept without adaptation.
///|
let json_schema_types : Map[String, String] = {
"boolean": "boolean",
"number": "number",
"string": "string",
"null": "null",
}
///|
fn schema_node() -> Map[String, Value] {
let node : Map[String, Value] = Map([])
node["type"] = String("object")
node["properties"] = Object(Map([]))
node
}
///|
fn insert_schema_node(
root : Map[String, Value],
segments : Array[String],
kind : String,
) -> Unit {
let mut current = root
for i = 0; i < segments.length(); i = i + 1 {
let segment = segments[i]
let is_last = i == segments.length() - 1
let props = match current.get("properties") {
Some(Object(fields)) => fields
_ => {
let fresh : Map[String, Value] = Map([])
current["properties"] = Object(fresh)
fresh
}
}
if is_last {
match json_schema_types.get(kind) {
Some(json_type) => {
let leaf : Map[String, Value] = Map([])
leaf["type"] = String(json_type)
props[segment] = Object(leaf)
}
None => {
// array and object keep structural types
let structural : Map[String, Value] = Map([])
if kind == "array" {
structural["type"] = String("array")
} else {
structural["type"] = String("object")
}
props[segment] = Object(structural)
}
}
} else {
match props.get(segment) {
Some(Object(existing)) => current = existing
_ => {
let nested = schema_node()
props[segment] = Object(nested)
current = nested
}
}
}
}
}
///|
fn export_schema_request(request : Value) -> Value raise {
let schema = match get(request, "schema") {
Null => get(infer_request(request), "schema")
other => other
}
let root = schema_node()
match schema {
Object(fields) =>
for path, value in fields {
let kind = str(value)
let segments : Array[String] = []
for view in path.split(".") {
segments.push(view.to_owned())
}
insert_schema_node(root, segments, kind)
}
_ => raise input_error("E3009", "schema must be an object")
}
let document : Map[String, Value] = Map([])
document["$schema"] = String("https://json-schema.org/draft/2020-12/schema")
document["type"] = String("object")
match root.get("properties") {
Some(properties) => document["properties"] = properties
None => ()
}
let path_count = match schema {
Object(fields) => fields.keys().collect().length()
_ => 0
}
record([
("json_schema", Object(document)),
("paths", Number(path_count.to_double())),
("draft", String("2020-12")),
])
}