///|
/// Severity used by a machine-readable analysis document.
pub enum DocumentSeverity {
Notice
Success
Warning
Failure
} derive(Debug, Eq)
///|
/// Standard sections emitted by a verification report.
pub enum DocumentSectionKind {
Overview
Input
Validation
Performance
Coverage
Assertions
Protocol
Findings
Artifacts
} derive(Debug, Eq)
///|
/// A named scalar or textual report metric.
pub struct DocumentMetric {
name : String
value : String
unit : String?
severity : DocumentSeverity
} derive(Debug)
///|
/// A report section containing metrics and explanatory lines.
pub struct DocumentSection {
kind : DocumentSectionKind
title : String
summary : String
metrics : Array[DocumentMetric]
lines : Array[String]
} derive(Debug)
///|
/// A portable analysis document that can be rendered in several formats.
pub struct AnalysisDocument {
title : String
version : String
verdict : DocumentSeverity
sections : Array[DocumentSection]
generated_by : String
} derive(Debug)
///|
/// List report severities.
pub fn document_severity_catalog() -> Array[DocumentSeverity] {
[Notice, Success, Warning, Failure]
}
///|
/// List report section kinds.
pub fn document_section_catalog() -> Array[DocumentSectionKind] {
[
Overview,
Input,
Validation,
Performance,
Coverage,
Assertions,
Protocol,
Findings,
Artifacts,
]
}
///|
/// Start an empty analysis document.
pub fn analysis_document(title : String) -> AnalysisDocument {
{
title,
version: "1.0",
verdict: Notice,
sections: [],
generated_by: "moonbit-vcd",
}
}
///|
/// Set the document version and top-level verdict.
pub fn AnalysisDocument::with_verdict(
self : AnalysisDocument,
version : String,
verdict : DocumentSeverity,
) -> AnalysisDocument {
{ ..self, version, verdict }
}
///|
/// Add a section, replacing an existing section of the same kind.
pub fn AnalysisDocument::add_section(
self : AnalysisDocument,
section : DocumentSection,
) -> AnalysisDocument {
let sections = self.sections.filter(fn(item) { item.kind != section.kind })
sections.push(section)
{ ..self, sections, }
}
///|
/// Add a metric to a section, creating the section when necessary.
pub fn AnalysisDocument::add_metric(
self : AnalysisDocument,
kind : DocumentSectionKind,
metric : DocumentMetric,
) -> AnalysisDocument {
let sections = self.sections.copy()
for index, section in sections {
if section.kind == kind {
let metrics = section.metrics.copy()
metrics.push(metric)
sections[index] = { ..section, metrics, }
return { ..self, sections, }
}
}
sections.push({
kind,
title: document_section_name(kind),
summary: "",
metrics: [metric],
lines: [],
})
{ ..self, sections, }
}
///|
/// Add an explanatory line to a section.
pub fn AnalysisDocument::add_line(
self : AnalysisDocument,
kind : DocumentSectionKind,
line : String,
) -> AnalysisDocument {
let sections = self.sections.copy()
for index, section in sections {
if section.kind == kind {
let lines = section.lines.copy()
lines.push(line)
sections[index] = { ..section, lines, }
return { ..self, sections, }
}
}
sections.push({
kind,
title: document_section_name(kind),
summary: line,
metrics: [],
lines: [line],
})
{ ..self, sections, }
}
///|
/// Construct a section directly.
pub fn document_section(
kind : DocumentSectionKind,
title : String,
summary : String,
) -> DocumentSection {
{ kind, title, summary, metrics: [], lines: [] }
}
///|
/// Construct a metric.
pub fn document_metric(
name : String,
value : String,
unit : String?,
severity : DocumentSeverity,
) -> DocumentMetric {
{ name, value, unit, severity }
}
///|
/// Return the number of report metrics.
pub fn AnalysisDocument::metric_count(self : AnalysisDocument) -> Int {
self.sections.fold(init=0, fn(total, item) { total + item.metrics.length() })
}
///|
/// Return all metrics with warning or failure status.
pub fn AnalysisDocument::risk_metrics(
self : AnalysisDocument,
) -> Array[DocumentMetric] {
let result : Array[DocumentMetric] = []
for section in self.sections {
for metric in section.metrics {
match metric.severity {
Warning | Failure => result.push(metric)
_ => ()
}
}
}
result
}
///|
/// Render a readable Markdown report.
pub fn AnalysisDocument::to_markdown(self : AnalysisDocument) -> String {
let out = StringBuilder()
out.write_string("# " + self.title + "\n\n")
out.write_string("- Version: " + self.version + "\n")
out.write_string("- Verdict: " + document_severity_name(self.verdict) + "\n")
out.write_string("- Generated by: " + self.generated_by + "\n\n")
for section in self.sections {
out.write_string("## " + section.title + "\n\n")
if section.summary.length() > 0 {
out.write_string(section.summary + "\n\n")
}
if section.metrics.length() > 0 {
out.write_string(
"| Metric | Value | Unit | Status |\n| --- | --- | --- | --- |\n",
)
for metric in section.metrics {
out.write_string(
"| " +
metric.name +
" | " +
metric.value +
" | " +
document_optional_text(metric.unit) +
" | " +
document_severity_name(metric.severity) +
" |\n",
)
}
out.write_char('\n')
}
for line in section.lines {
out.write_string("- " + line + "\n")
}
if section.lines.length() > 0 {
out.write_char('\n')
}
}
out.to_string()
}
///|
/// Render a simple HTML document for web artifacts.
pub fn AnalysisDocument::to_html(self : AnalysisDocument) -> String {
let out = StringBuilder()
out.write_string("")
out.write_string(document_html_escape(self.title))
out.write_string(" ")
out.write_string(
document_html_escape(self.title) +
"
Verdict: " +
document_severity_name(self.verdict) +
"
",
)
for section in self.sections {
out.write_string(
"" +
document_html_escape(section.title) +
"
" +
document_html_escape(section.summary) +
"
",
)
if section.metrics.length() > 0 {
out.write_string(
"Metric Value Unit Status ",
)
for metric in section.metrics {
out.write_string(
"" +
document_html_escape(metric.name) +
" " +
document_html_escape(metric.value) +
" " +
document_html_escape(document_optional_text(metric.unit)) +
" " +
document_severity_name(metric.severity) +
" ",
)
}
out.write_string("
")
}
if section.lines.length() > 0 {
out.write_string("")
for line in section.lines {
out.write_string("- " + document_html_escape(line) + "
")
}
out.write_string("
")
}
out.write_string(" ")
}
out.write_string("")
out.to_string()
}
///|
/// Render report sections as JUnit XML test cases.
pub fn AnalysisDocument::to_junit(self : AnalysisDocument) -> String {
let failures = self
.risk_metrics()
.filter(fn(metric) { metric.severity is Failure })
.length()
let out = StringBuilder()
out.write_string(
"",
)
for section in self.sections {
out.write_string(
"",
)
let section_failure = section.metrics.any(fn(metric) {
metric.severity is Failure
})
if section_failure {
out.write_string(
"" +
document_html_escape(section.summary) +
" ",
)
}
out.write_string(" ")
}
out.write_string(" ")
out.to_string()
}
///|
/// Render a compact one-line verdict.
pub fn AnalysisDocument::verdict_line(self : AnalysisDocument) -> String {
self.title +
" verdict=" +
document_severity_name(self.verdict) +
" sections=" +
self.sections.length().to_string() +
" metrics=" +
self.metric_count().to_string()
}
///|
/// Stable severity names for all output formats.
pub fn document_severity_name(value : DocumentSeverity) -> String {
match value {
Notice => "notice"
Success => "success"
Warning => "warning"
Failure => "failure"
}
}
///|
/// Stable section names for headings and automation.
pub fn document_section_name(value : DocumentSectionKind) -> String {
match value {
Overview => "Overview"
Input => "Input"
Validation => "Validation"
Performance => "Performance"
Coverage => "Coverage"
Assertions => "Assertions"
Protocol => "Protocol"
Findings => "Findings"
Artifacts => "Artifacts"
}
}
///|
/// Encode an optional text field.
fn document_optional_text(value : String?) -> String {
match value {
Some(item) => item
None => ""
}
}
///|
/// Escape the characters that are significant in HTML and XML.
fn document_html_escape(value : String) -> String {
value
.replace_all(old="&", new="&")
.replace_all(old="<", new="<")
.replace_all(old=">", new=">")
.replace_all(old="\"", new=""")
}