///|
/// Structural metrics for a complete JTD schema document.
pub(all) struct SchemaStats {
node_count : Int
max_depth : Int
definition_count : Int
reference_count : Int
nullable_count : Int
empty_count : Int
type_count : Int
enum_count : Int
elements_count : Int
properties_count : Int
values_count : Int
discriminator_count : Int
required_property_count : Int
optional_property_count : Int
discriminator_branch_count : Int
} derive(Eq, Debug)
///|
fn stat_value(counts : Map[String, Int], name : String) -> Int {
match counts.get(name) {
Some(value) => value
None => 0
}
}
///|
fn increment_stat(counts : Map[String, Int], name : String) -> Unit {
counts.set(name, stat_value(counts, name) + 1)
}
///|
fn add_stat(counts : Map[String, Int], name : String, amount : Int) -> Unit {
counts.set(name, stat_value(counts, name) + amount)
}
///|
fn analyze_node(
schema : Schema,
depth : Int,
counts : Map[String, Int],
max_depth : Ref[Int],
) -> Unit {
increment_stat(counts, "nodes")
if depth > max_depth.val {
max_depth.val = depth
}
if schema.is_nullable() {
increment_stat(counts, "nullable")
}
match schema.form() {
EmptyForm => increment_stat(counts, "empty")
RefForm(_) => increment_stat(counts, "ref")
TypeForm(_) => increment_stat(counts, "type")
EnumForm(_) => increment_stat(counts, "enum")
ElementsForm(element) => {
increment_stat(counts, "elements")
analyze_node(element, depth + 1, counts, max_depth)
}
ValuesForm(value) => {
increment_stat(counts, "values")
analyze_node(value, depth + 1, counts, max_depth)
}
PropertiesForm(required, optional, _) => {
increment_stat(counts, "properties")
add_stat(counts, "required-properties", required.length())
add_stat(counts, "optional-properties", optional.length())
for _, child in required {
analyze_node(child, depth + 1, counts, max_depth)
}
for _, child in optional {
analyze_node(child, depth + 1, counts, max_depth)
}
}
DiscriminatorForm(_, mapping) => {
increment_stat(counts, "discriminator")
add_stat(counts, "branches", mapping.length())
for _, child in mapping {
analyze_node(child, depth + 1, counts, max_depth)
}
}
}
}
///|
/// Count schema structure without following references, so recursive definitions
/// are safe and each syntactic node is counted exactly once.
pub fn analyze_schema(document : SchemaDocument) -> SchemaStats {
let counts : Map[String, Int] = Map([])
let max_depth = Ref(0)
analyze_node(document.root(), 0, counts, max_depth)
for _, schema in document.definitions() {
analyze_node(schema, 1, counts, max_depth)
}
{
node_count: stat_value(counts, "nodes"),
max_depth: max_depth.val,
definition_count: document.definition_count(),
reference_count: stat_value(counts, "ref"),
nullable_count: stat_value(counts, "nullable"),
empty_count: stat_value(counts, "empty"),
type_count: stat_value(counts, "type"),
enum_count: stat_value(counts, "enum"),
elements_count: stat_value(counts, "elements"),
properties_count: stat_value(counts, "properties"),
values_count: stat_value(counts, "values"),
discriminator_count: stat_value(counts, "discriminator"),
required_property_count: stat_value(counts, "required-properties"),
optional_property_count: stat_value(counts, "optional-properties"),
discriminator_branch_count: stat_value(counts, "branches"),
}
}
///|
pub fn SchemaStats::node_count(self : SchemaStats) -> Int {
self.node_count
}
///|
pub fn SchemaStats::max_depth(self : SchemaStats) -> Int {
self.max_depth
}
///|
pub fn SchemaStats::definition_count(self : SchemaStats) -> Int {
self.definition_count
}
///|
pub fn SchemaStats::reference_count(self : SchemaStats) -> Int {
self.reference_count
}
///|
pub fn SchemaStats::nullable_count(self : SchemaStats) -> Int {
self.nullable_count
}
///|
pub fn SchemaStats::empty_count(self : SchemaStats) -> Int {
self.empty_count
}
///|
pub fn SchemaStats::type_count(self : SchemaStats) -> Int {
self.type_count
}
///|
pub fn SchemaStats::enum_count(self : SchemaStats) -> Int {
self.enum_count
}
///|
pub fn SchemaStats::elements_count(self : SchemaStats) -> Int {
self.elements_count
}
///|
pub fn SchemaStats::properties_count(self : SchemaStats) -> Int {
self.properties_count
}
///|
pub fn SchemaStats::values_count(self : SchemaStats) -> Int {
self.values_count
}
///|
pub fn SchemaStats::discriminator_count(self : SchemaStats) -> Int {
self.discriminator_count
}
///|
pub fn SchemaStats::required_property_count(self : SchemaStats) -> Int {
self.required_property_count
}
///|
pub fn SchemaStats::optional_property_count(self : SchemaStats) -> Int {
self.optional_property_count
}
///|
pub fn SchemaStats::discriminator_branch_count(self : SchemaStats) -> Int {
self.discriminator_branch_count
}
///|
pub fn SchemaStats::to_string(self : SchemaStats) -> String {
"nodes=" +
self.node_count.to_string() +
", maxDepth=" +
self.max_depth.to_string() +
", definitions=" +
self.definition_count.to_string() +
", refs=" +
self.reference_count.to_string() +
", nullable=" +
self.nullable_count.to_string() +
", forms={empty:" +
self.empty_count.to_string() +
",type:" +
self.type_count.to_string() +
",enum:" +
self.enum_count.to_string() +
",elements:" +
self.elements_count.to_string() +
",properties:" +
self.properties_count.to_string() +
",values:" +
self.values_count.to_string() +
",discriminator:" +
self.discriminator_count.to_string() +
"}"
}
///|
fn collect_refs_node(schema : Schema, refs : Map[String, Bool]) -> Unit {
match schema.form() {
RefForm(name) => refs.set(name, true)
ElementsForm(element) | ValuesForm(element) =>
collect_refs_node(element, refs)
PropertiesForm(required, optional, _) => {
for _, child in required {
collect_refs_node(child, refs)
}
for _, child in optional {
collect_refs_node(child, refs)
}
}
DiscriminatorForm(_, mapping) =>
for _, child in mapping {
collect_refs_node(child, refs)
}
EmptyForm | TypeForm(_) | EnumForm(_) => ()
}
}
///|
/// Collect every syntactically referenced definition name.
pub fn referenced_definitions(document : SchemaDocument) -> Array[String] {
let refs : Map[String, Bool] = Map([])
collect_refs_node(document.root(), refs)
for _, schema in document.definitions() {
collect_refs_node(schema, refs)
}
let names : Array[String] = []
for name, _ in refs {
names.push(name)
}
names
}
///|
/// Definitions that are not transitively reachable from the root schema.
pub fn unused_definitions(document : SchemaDocument) -> Array[String] {
let reachable : Map[String, Bool] = Map([])
collect_refs_node(document.root(), reachable)
let mut changed = true
while changed {
changed = false
let snapshot : Array[String] = []
for name, _ in reachable {
snapshot.push(name)
}
for name in snapshot {
match document.definition(name) {
None => ()
Some(schema) => {
let before = reachable.length()
collect_refs_node(schema, reachable)
if reachable.length() != before {
changed = true
}
}
}
}
}
let unused : Array[String] = []
for name, _ in document.definitions() {
if !reachable.contains(name) {
unused.push(name)
}
}
unused
}
///|
/// Produce human-readable lint diagnostics that are not RFC correctness errors.
pub fn lint_schema(document : SchemaDocument) -> Array[Diagnostic] {
let diagnostics : Array[Diagnostic] = []
for name in unused_definitions(document) {
diagnostics.push(
Diagnostic::new(
UnknownReference,
"definition '" + name + "' is never reached from the root schema",
schema_path=JsonPointer::root().property("definitions").property(name),
),
)
}
let stats = analyze_schema(document)
if stats.node_count() > 10000 {
diagnostics.push(
Diagnostic::new(
ResourceLimitExceeded,
"large schema contains " + stats.node_count().to_string() + " nodes",
),
)
}
diagnostics
}