///|
/// A typed table used in engineering reports.
pub(all) struct ReportTable {
title : String
columns : Array[String]
rows : Array[Array[String]]
numeric_columns : Array[Bool]
} derive(Debug, Eq, ToJson)
///|
pub fn ReportTable::new(
title~ : String,
columns~ : ArrayView[String],
numeric_columns? : ArrayView[Bool] = [],
) -> ReportTable raise @domain.ExtractionError {
if title.trim().length() == 0 || columns.length() == 0 {
raise @domain.ExtractionError::InvalidArgument(
message="report table title and columns are required",
)
}
if numeric_columns.length() != 0 &&
numeric_columns.length() != columns.length() {
raise @domain.ExtractionError::InvalidArgument(
message="numeric-column flags do not match columns",
)
}
let numeric = if numeric_columns.length() == 0 {
[
for _ in columns => false
]
} else {
numeric_columns.to_owned()
}
{ title, columns: columns.to_owned(), rows: [], numeric_columns: numeric }
}
///|
pub fn ReportTable::add_row(
self : ReportTable,
row : ArrayView[String],
) -> ReportTable raise @domain.ExtractionError {
if row.length() != self.columns.length() {
raise @domain.ExtractionError::InvalidArgument(
message="report row width does not match columns",
)
}
let rows = self.rows.copy()
rows.push(row.to_owned())
{
title: self.title,
columns: self.columns,
rows,
numeric_columns: self.numeric_columns,
}
}
///|
pub fn ReportTable::row_count(self : ReportTable) -> Int {
self.rows.length()
}
///|
pub fn ReportTable::column_count(self : ReportTable) -> Int {
self.columns.length()
}
///|
pub fn ReportTable::rows(self : ReportTable) -> Array[Array[String]] {
[
for row in self.rows => row.copy()
]
}
///|
pub fn ReportTable::to_markdown(self : ReportTable) -> String {
let lines = [
"### {self.title}",
"",
"| " + self.columns.join(" | ") + " |",
"|" +
[
for numeric in self.numeric_columns => {
if numeric {
"---:"
} else {
"---"
}
}
].join("|") +
"|",
]
for row in self.rows {
lines.push("| " + row.join(" | ") + " |")
}
lines.join("\n")
}
///|
fn csv_escape(value : String) -> String {
if value.contains(",") || value.contains("\n") || value.contains("\"") {
"\"" + value.replace(old="\"", new="\"\"") + "\""
} else {
value
}
}
///|
pub fn ReportTable::to_csv(self : ReportTable) -> String {
let lines = [self.columns.map(csv_escape).join(",")]
for row in self.rows {
lines.push(row.map(csv_escape).join(","))
}
lines.join("\n")
}
///|
pub(all) enum ReportBlock {
Heading(level~ : Int, text~ : String)
Paragraph(text~ : String)
Bullet(items~ : Array[String])
Table(table~ : ReportTable)
Finding(finding~ : Finding)
Metric(name~ : String, value~ : Double, unit~ : String)
Divider
} derive(Debug, Eq, ToJson)
///|
pub(all) struct ReportDocument {
title : String
subtitle : String
blocks : Array[ReportBlock]
generated_by : String
} derive(Debug, Eq, ToJson)
///|
pub fn ReportDocument::new(
title~ : String,
subtitle? : String = "",
generated_by? : String = "moonbit-extractor",
) -> ReportDocument raise @domain.ExtractionError {
if title.trim().length() == 0 {
raise @domain.ExtractionError::InvalidArgument(
message="report title cannot be blank",
)
}
{ title, subtitle, blocks: [], generated_by }
}
///|
pub fn ReportDocument::add(
self : ReportDocument,
block : ReportBlock,
) -> ReportDocument {
let blocks = self.blocks.copy()
blocks.push(block)
{
title: self.title,
subtitle: self.subtitle,
blocks,
generated_by: self.generated_by,
}
}
///|
pub fn ReportDocument::heading(
self : ReportDocument,
level~ : Int,
text~ : String,
) -> ReportDocument raise @domain.ExtractionError {
if level < 1 || level > 6 || text.trim().length() == 0 {
raise @domain.ExtractionError::InvalidArgument(
message="report heading level or text is invalid",
)
}
self.add(Heading(level~, text~))
}
///|
pub fn ReportDocument::paragraph(
self : ReportDocument,
text~ : String,
) -> ReportDocument {
self.add(Paragraph(text~))
}
///|
pub fn ReportDocument::bullets(
self : ReportDocument,
items~ : Array[String],
) -> ReportDocument {
self.add(Bullet(items~))
}
///|
pub fn ReportDocument::table(
self : ReportDocument,
table~ : ReportTable,
) -> ReportDocument {
self.add(Table(table~))
}
///|
pub fn ReportDocument::metric(
self : ReportDocument,
name~ : String,
value~ : Double,
unit~ : String,
) -> ReportDocument raise @domain.ExtractionError {
@domain.require_finite("report metric", value)
self.add(Metric(name~, value~, unit~))
}
///|
pub fn ReportDocument::finding(
self : ReportDocument,
finding~ : Finding,
) -> ReportDocument {
self.add(ReportBlock::Finding(finding~))
}
///|
pub fn ReportDocument::block_count(self : ReportDocument) -> Int {
self.blocks.length()
}
///|
fn heading_prefix(level : Int) -> String {
let mut result = ""
for _ in 0.. String {
let severity = match finding.severity {
Pass => "PASS"
Notice => "NOTICE"
Fail => "FAIL"
}
"- **\{severity} \{finding.code}** \{finding.title}: \{finding.detail} (measured=\{finding.measured}, limit=\{finding.limit})"
}
///|
pub fn ReportDocument::to_markdown(self : ReportDocument) -> String {
let lines = ["# \{self.title}", ""]
if self.subtitle.trim() != "" {
lines.push(self.subtitle)
lines.push("")
}
for block in self.blocks {
match block {
Heading(level~, text~) => {
lines.push(heading_prefix(level) + " " + text)
lines.push("")
}
Paragraph(text~) => {
lines.push(text)
lines.push("")
}
Bullet(items~) => {
for item in items {
lines.push("- " + item)
}
lines.push("")
}
Table(table~) => {
lines.push(table.to_markdown())
lines.push("")
}
Finding(finding~) => {
lines.push(finding_markdown(finding))
lines.push("")
}
Metric(name~, value~, unit~) => {
lines.push("**\{name}**: \{value} \{unit}")
lines.push("")
}
Divider => {
lines.push("---")
lines.push("")
}
}
}
lines.push("Generated by \{self.generated_by}")
lines.join("\n")
}
///|
pub fn ReportDocument::summary(self : ReportDocument) -> String {
let mut headings = 0
let mut tables = 0
let mut findings = 0
for block in self.blocks {
match block {
Heading(..) => headings += 1
Table(_) => tables += 1
Finding(_) => findings += 1
_ => ()
}
}
"title=\{self.title},blocks=\{self.blocks.length()},headings=\{headings},tables=\{tables},findings=\{findings}"
}
///|
pub fn report_from_audit(
title~ : String,
audit : AuditReport,
) -> ReportDocument raise @domain.ExtractionError {
let document = ReportDocument::new(title~)
let result_text = if audit.passed { "PASS" } else { "FAIL" }
let with_summary = document.paragraph(
text="Audit result: \{result_text}; score=\{audit.score}",
)
let mut result = with_summary
for finding in audit.findings {
result = result.finding(finding~)
}
result
}
///|
pub(all) struct ReportIndex {
title : String
section_count : Int
metric_count : Int
table_count : Int
failure_count : Int
} derive(Debug, Eq, ToJson)
///|
pub fn ReportDocument::index(self : ReportDocument) -> ReportIndex {
let mut metrics = 0
let mut tables = 0
let mut failures = 0
for block in self.blocks {
match block {
Metric(_) => metrics += 1
Table(_) => tables += 1
Finding(finding~) =>
match finding.severity {
Fail => failures += 1
_ => ()
}
_ => ()
}
}
{
title: self.title,
section_count: self.blocks.length(),
metric_count: metrics,
table_count: tables,
failure_count: failures,
}
}