///|
fn string_array_json(values : Array[String]) -> Json {
Json::array(values.map(value => Json::string(value)))
}
///|
fn schema_map_json(schemas : Map[String, Schema]) -> Json {
let object : Map[String, Json] = Map([])
for name, schema in schemas {
object.set(name, schema_to_json_node(schema))
}
Json::object(object)
}
///|
fn add_shared_members(schema : Schema, object : Map[String, Json]) -> Unit {
if schema.is_nullable() {
object.set("nullable", Json::boolean(true))
}
let metadata = schema.metadata()
if !metadata.is_empty() {
object.set("metadata", Json::object(metadata))
}
}
///|
fn properties_form_json(
required : Map[String, Schema],
optional : Map[String, Schema],
additional : Bool,
object : Map[String, Json],
) -> Unit {
if !required.is_empty() || optional.is_empty() {
object.set("properties", schema_map_json(required))
}
if !optional.is_empty() {
object.set("optionalProperties", schema_map_json(optional))
}
if additional {
object.set("additionalProperties", Json::boolean(true))
}
}
///|
fn discriminator_mapping_json(mapping : Map[String, Schema]) -> Json {
let object : Map[String, Json] = Map([])
for name, schema in mapping {
object.set(name, schema_to_json_node(schema))
}
Json::object(object)
}
///|
fn schema_to_json_node(schema : Schema) -> Json {
let object : Map[String, Json] = Map([])
match schema.form() {
EmptyForm => ()
RefForm(name) => object.set("ref", Json::string(name))
TypeForm(kind) => object.set("type", Json::string(kind.name()))
EnumForm(values) => object.set("enum", string_array_json(values))
ElementsForm(element) =>
object.set("elements", schema_to_json_node(element))
PropertiesForm(required, optional, additional) =>
properties_form_json(required, optional, additional, object)
ValuesForm(value) => object.set("values", schema_to_json_node(value))
DiscriminatorForm(tag, mapping) => {
object.set("discriminator", Json::string(tag))
object.set("mapping", discriminator_mapping_json(mapping))
}
}
add_shared_members(schema, object)
Json::object(object)
}
///|
/// Convert a schema document into its JSON data model.
pub fn schema_to_json(document : SchemaDocument) -> Json {
guard schema_to_json_node(document.root()) is Object(root) else {
abort("schema serialization invariant")
}
let definitions = document.definitions()
if !definitions.is_empty() {
root.set("definitions", schema_map_json(definitions))
}
Json::object(root)
}
///|
/// Serialize a schema document as compact JSON.
pub fn schema_to_string(document : SchemaDocument) -> String {
schema_to_json(document).stringify(escape_slash=false)
}
///|
fn write_indent(output : StringBuilder, depth : Int, width : Int) -> Unit {
for index = 0; index < depth * width; index = index + 1 {
output.write_char(' ')
}
}
///|
fn write_json_string(output : StringBuilder, value : String) -> Unit {
output.write_string(Json::string(value).stringify(escape_slash=false))
}
///|
fn write_pretty_array(
output : StringBuilder,
values : Array[Json],
depth : Int,
width : Int,
) -> Unit {
if values.is_empty() {
output.write_string("[]")
return
}
output.write_string("[\n")
for index, value in values {
write_indent(output, depth + 1, width)
write_pretty_json(output, value, depth + 1, width)
if index + 1 < values.length() {
output.write_char(',')
}
output.write_char('\n')
}
write_indent(output, depth, width)
output.write_char(']')
}
///|
fn write_pretty_object(
output : StringBuilder,
object : Map[String, Json],
depth : Int,
width : Int,
) -> Unit {
if object.is_empty() {
output.write_string("{}")
return
}
output.write_string("{\n")
let mut index = 0
for name, value in object {
write_indent(output, depth + 1, width)
write_json_string(output, name)
output.write_string(": ")
write_pretty_json(output, value, depth + 1, width)
index += 1
if index < object.length() {
output.write_char(',')
}
output.write_char('\n')
}
write_indent(output, depth, width)
output.write_char('}')
}
///|
fn write_pretty_json(
output : StringBuilder,
value : Json,
depth : Int,
width : Int,
) -> Unit {
match value {
Array(values) => write_pretty_array(output, values, depth, width)
Object(object) => write_pretty_object(output, object, depth, width)
other => output.write_string(other.stringify(escape_slash=false))
}
}
///|
/// Render indented JSON without depending on host filesystem or console APIs.
pub fn schema_to_pretty_string(
document : SchemaDocument,
indent? : Int = 2,
) -> String {
let width = if indent < 0 { 0 } else { indent }
let output = StringBuilder()
write_pretty_json(output, schema_to_json(document), 0, width)
output.to_string()
}
///|
/// Return a deep structural form signature useful in reports and tests.
pub fn schema_shape(schema : Schema) -> String {
match schema.form() {
EmptyForm => "any"
RefForm(name) => "ref(" + name + ")"
TypeForm(kind) => kind.name()
EnumForm(values) => "enum[" + values.length().to_string() + "]"
ElementsForm(element) => "array<" + schema_shape(element) + ">"
ValuesForm(value) => "map<" + schema_shape(value) + ">"
PropertiesForm(required, optional, additional) => {
let suffix = if additional { ",open" } else { ",closed" }
"object[required=" +
required.length().to_string() +
",optional=" +
optional.length().to_string() +
suffix +
"]"
}
DiscriminatorForm(tag, mapping) =>
"union[tag=" + tag + ",branches=" + mapping.length().to_string() + "]"
}
}