///|
/// Counts and shape measurements for one JSON document.
pub(all) struct JsonStats {
mut value_count : Int
mut object_count : Int
mut array_count : Int
mut string_count : Int
mut number_count : Int
mut boolean_count : Int
mut null_count : Int
mut max_depth : Int
mut max_array_length : Int
mut max_object_members : Int
}
///|
/// The result of inspecting a JSON document. A failed report still contains
/// the zero stats value so callers can render one stable response shape.
pub(all) struct AnalysisReport {
valid : Bool
stats : JsonStats
canonical : String?
digest : String?
error : String?
}
///|
pub fn JsonStats::empty() -> JsonStats {
{
value_count: 0,
object_count: 0,
array_count: 0,
string_count: 0,
number_count: 0,
boolean_count: 0,
null_count: 0,
max_depth: 0,
max_array_length: 0,
max_object_members: 0,
}
}
///|
/// Parse, analyze, canonicalize, and hash one JSON document in one call.
pub fn inspect_json(input : String) -> AnalysisReport {
let value = match parse(input) {
Ok(value) => value
Err(error) =>
return {
valid: false,
stats: JsonStats::empty(),
canonical: None,
digest: None,
error: Some(error.message()),
}
}
let stats = JsonStats::empty()
collect_stats(value, 0, stats)
let canonical = match canonicalize(value) {
Ok(serialized) => serialized
Err(error) =>
return {
valid: false,
stats,
canonical: None,
digest: None,
error: Some(error.message()),
}
}
let digest = match canonical_digest(value) {
Ok(digest) => digest
Err(error) =>
return {
valid: false,
stats,
canonical: Some(canonical),
digest: None,
error: Some(error.message()),
}
}
{
valid: true,
stats,
canonical: Some(canonical),
digest: Some(digest),
error: None,
}
}
///|
fn collect_stats(value : JsonValue, depth : Int, stats : JsonStats) -> Unit {
stats.value_count = stats.value_count + 1
if depth > stats.max_depth {
stats.max_depth = depth
}
match value {
Null => stats.null_count = stats.null_count + 1
Bool(_) => stats.boolean_count = stats.boolean_count + 1
Number(_) => stats.number_count = stats.number_count + 1
String(_) => stats.string_count = stats.string_count + 1
Array(values) => {
stats.array_count = stats.array_count + 1
if values.length() > stats.max_array_length {
stats.max_array_length = values.length()
}
for child in values {
collect_stats(child, depth + 1, stats)
}
}
Object(entries) => {
stats.object_count = stats.object_count + 1
if entries.length() > stats.max_object_members {
stats.max_object_members = entries.length()
}
for entry in entries {
collect_stats(entry.1, depth + 1, stats)
}
}
}
}
///|
/// Render the metrics as a deterministic JSON object for scripts and CI.
pub fn JsonStats::to_json(self : JsonStats) -> String {
"{\"value_count\":" +
self.value_count.to_string() +
",\"object_count\":" +
self.object_count.to_string() +
",\"array_count\":" +
self.array_count.to_string() +
",\"string_count\":" +
self.string_count.to_string() +
",\"number_count\":" +
self.number_count.to_string() +
",\"boolean_count\":" +
self.boolean_count.to_string() +
",\"null_count\":" +
self.null_count.to_string() +
",\"max_depth\":" +
self.max_depth.to_string() +
",\"max_array_length\":" +
self.max_array_length.to_string() +
",\"max_object_members\":" +
self.max_object_members.to_string() +
"}"
}
///|
/// Render a report as a stable, line-oriented summary suitable for terminals.
pub fn AnalysisReport::to_text(self : AnalysisReport) -> String {
let status = if self.valid { "valid" } else { "invalid" }
let mut output = "status: " + status + "\n"
output = output + "stats: " + self.stats.to_json() + "\n"
match self.digest {
Some(digest) => output = output + "sha256: " + digest + "\n"
None => ()
}
match self.error {
Some(error) => output = output + "error: " + error + "\n"
None => ()
}
output
}
///|
/// Render a report as JSON without relying on a host JSON implementation.
pub fn AnalysisReport::to_json(self : AnalysisReport) -> String {
let canonical = match self.canonical {
Some(value) => canonical_string(value)
None => "null"
}
let digest = match self.digest {
Some(value) => canonical_string(value)
None => "null"
}
let error = match self.error {
Some(value) => canonical_string(value)
None => "null"
}
"{\"valid\":" +
(if self.valid { "true" } else { "false" }) +
",\"stats\":" +
self.stats.to_json() +
",\"canonical\":" +
canonical +
",\"digest\":" +
digest +
",\"error\":" +
error +
"}"
}