///|
/// One syntactic node in a schema inventory.
pub(all) struct SchemaNodeInfo {
path : JsonPointer
form : String
shape : String
nullable : Bool
child_count : Int
} derive(Eq, Debug)
///|
pub fn SchemaNodeInfo::path(self : SchemaNodeInfo) -> JsonPointer {
self.path
}
///|
pub fn SchemaNodeInfo::form(self : SchemaNodeInfo) -> String {
self.form
}
///|
pub fn SchemaNodeInfo::shape(self : SchemaNodeInfo) -> String {
self.shape
}
///|
pub fn SchemaNodeInfo::is_nullable(self : SchemaNodeInfo) -> Bool {
self.nullable
}
///|
pub fn SchemaNodeInfo::child_count(self : SchemaNodeInfo) -> Int {
self.child_count
}
///|
fn direct_child_count(schema : Schema) -> Int {
match schema.form() {
ElementsForm(_) | ValuesForm(_) => 1
PropertiesForm(required, optional, _) =>
required.length() + optional.length()
DiscriminatorForm(_, mapping) => mapping.length()
EmptyForm | RefForm(_) | TypeForm(_) | EnumForm(_) => 0
}
}
///|
fn inventory_node(
schema : Schema,
path : JsonPointer,
output : Array[SchemaNodeInfo],
) -> Unit {
output.push({
path,
form: schema.form_name(),
shape: schema_shape(schema),
nullable: schema.is_nullable(),
child_count: direct_child_count(schema),
})
match schema.form() {
ElementsForm(element) =>
inventory_node(element, path.property("elements"), output)
ValuesForm(value) => inventory_node(value, path.property("values"), output)
PropertiesForm(required, optional, _) => {
for name, child in required {
inventory_node(
child,
path.property("properties").property(name),
output,
)
}
for name, child in optional {
inventory_node(
child,
path.property("optionalProperties").property(name),
output,
)
}
}
DiscriminatorForm(_, mapping) =>
for name, child in mapping {
inventory_node(child, path.property("mapping").property(name), output)
}
EmptyForm | RefForm(_) | TypeForm(_) | EnumForm(_) => ()
}
}
///|
/// Enumerate each syntactic schema node without following refs.
pub fn schema_inventory(document : SchemaDocument) -> Array[SchemaNodeInfo] {
let output : Array[SchemaNodeInfo] = []
inventory_node(document.root(), JsonPointer::root(), output)
for name, schema in document.definitions() {
inventory_node(
schema,
JsonPointer::root().property("definitions").property(name),
output,
)
}
output
}
///|
pub fn diagnostic_to_json(diagnostic : Diagnostic) -> Json {
Json::object(
Map([
("code", Json::string(diagnostic.code().name())),
("message", Json::string(diagnostic.message())),
("instancePath", Json::string(diagnostic.instance_path().to_string())),
("schemaPath", Json::string(diagnostic.schema_path().to_string())),
]),
)
}
///|
pub fn diagnostics_to_json(diagnostics : Array[Diagnostic]) -> Json {
Json::array(diagnostics.map(diagnostic_to_json))
}
///|
pub fn diagnostics_to_string(
diagnostics : Array[Diagnostic],
pretty? : Bool = false,
) -> String {
let value = diagnostics_to_json(diagnostics)
if pretty {
let output = StringBuilder()
write_report_pretty_json(output, value, 0, 2)
output.to_string()
} else {
value.stringify(escape_slash=false)
}
}
///|
fn report_indent(output : StringBuilder, depth : Int, width : Int) -> Unit {
for index = 0; index < depth * width; index = index + 1 {
output.write_char(' ')
}
}
///|
fn write_report_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 {
report_indent(output, depth + 1, width)
write_report_pretty_json(output, value, depth + 1, width)
if index + 1 < values.length() {
output.write_char(',')
}
output.write_char('\n')
}
report_indent(output, depth, width)
output.write_char(']')
}
///|
fn write_report_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 {
report_indent(output, depth + 1, width)
output.write_string(Json::string(name).stringify(escape_slash=false))
output.write_string(": ")
write_report_pretty_json(output, value, depth + 1, width)
index += 1
if index < object.length() {
output.write_char(',')
}
output.write_char('\n')
}
report_indent(output, depth, width)
output.write_char('}')
}
///|
fn write_report_pretty_json(
output : StringBuilder,
value : Json,
depth : Int,
width : Int,
) -> Unit {
match value {
Array(values) => write_report_array(output, values, depth, width)
Object(object) => write_report_object(output, object, depth, width)
other => output.write_string(other.stringify(escape_slash=false))
}
}
///|
fn stat_number(value : Int) -> Json {
Json::number(value.to_double(), repr=value.to_string())
}
///|
pub fn schema_stats_to_json(stats : SchemaStats) -> Json {
Json::object(
Map([
("nodeCount", stat_number(stats.node_count())),
("maxDepth", stat_number(stats.max_depth())),
("definitionCount", stat_number(stats.definition_count())),
("referenceCount", stat_number(stats.reference_count())),
("nullableCount", stat_number(stats.nullable_count())),
("emptyCount", stat_number(stats.empty_count())),
("typeCount", stat_number(stats.type_count())),
("enumCount", stat_number(stats.enum_count())),
("elementsCount", stat_number(stats.elements_count())),
("propertiesCount", stat_number(stats.properties_count())),
("valuesCount", stat_number(stats.values_count())),
("discriminatorCount", stat_number(stats.discriminator_count())),
("requiredPropertyCount", stat_number(stats.required_property_count())),
("optionalPropertyCount", stat_number(stats.optional_property_count())),
(
"discriminatorBranchCount",
stat_number(stats.discriminator_branch_count()),
),
]),
)
}
///|
pub fn schema_node_info_to_json(info : SchemaNodeInfo) -> Json {
Json::object(
Map([
("path", Json::string(info.path().to_string())),
("form", Json::string(info.form())),
("shape", Json::string(info.shape())),
("nullable", Json::boolean(info.is_nullable())),
("childCount", stat_number(info.child_count())),
]),
)
}
///|
pub fn validation_report_to_json(report : ValidationReport) -> Json {
Json::object(
Map([
("valid", Json::boolean(report.is_valid())),
("truncated", Json::boolean(report.is_truncated())),
("visitedNodes", stat_number(report.visited_nodes())),
("errors", diagnostics_to_json(report.errors())),
]),
)
}
///|
pub fn schema_report_json(document : SchemaDocument) -> Json {
let nodes = schema_inventory(document).map(schema_node_info_to_json)
let unused = unused_definitions(document).map(name => Json::string(name))
Json::object(
Map([
("specification", Json::string(SPECIFICATION)),
("rootForm", Json::string(document.root().form_name())),
("rootShape", Json::string(schema_shape(document.root()))),
("statistics", schema_stats_to_json(analyze_schema(document))),
("nodes", Json::array(nodes)),
("unusedDefinitions", Json::array(unused)),
("lint", diagnostics_to_json(lint_schema(document))),
]),
)
}
///|
pub fn schema_report_string(
document : SchemaDocument,
pretty? : Bool = true,
) -> String {
let value = schema_report_json(document)
if pretty {
let output = StringBuilder()
write_report_pretty_json(output, value, 0, 2)
output.to_string()
} else {
value.stringify(escape_slash=false)
}
}
///|
pub fn schema_report_markdown(document : SchemaDocument) -> String {
let stats = analyze_schema(document)
let output = StringBuilder()
output.write_string("# MoonJTD schema report\n\n")
output.write_string("- Specification: ")
output.write_string(SPECIFICATION)
output.write_string("\n- Root form: ")
output.write_string(document.root().form_name())
output.write_string("\n- Root shape: `")
output.write_string(schema_shape(document.root()))
output.write_string("`\n- Schema nodes: ")
output.write_string(stats.node_count().to_string())
output.write_string("\n- Maximum depth: ")
output.write_string(stats.max_depth().to_string())
output.write_string("\n- Definitions: ")
output.write_string(stats.definition_count().to_string())
output.write_string("\n- References: ")
output.write_string(stats.reference_count().to_string())
output.write_string("\n\n## Node inventory\n\n")
output.write_string("| Path | Form | Shape | Nullable | Children |\n")
output.write_string("|---|---|---|---:|---:|\n")
for info in schema_inventory(document) {
let path = if info.path().is_root() {
"``"
} else {
"`" + info.path().to_string() + "`"
}
output.write_string("| ")
output.write_string(path)
output.write_string(" | ")
output.write_string(info.form())
output.write_string(" | `")
output.write_string(info.shape())
output.write_string("` | ")
output.write_string(if info.is_nullable() { "yes" } else { "no" })
output.write_string(" | ")
output.write_string(info.child_count().to_string())
output.write_string(" |\n")
}
let lint = lint_schema(document)
output.write_string("\n## Lint\n\n")
if lint.is_empty() {
output.write_string("No lint findings.\n")
} else {
for diagnostic in lint {
output.write_string("- `")
output.write_string(diagnostic.code().name())
output.write_string("`: ")
output.write_string(diagnostic.message())
output.write_string("\n")
}
}
output.to_string()
}