#![deny(warnings)]
use serde_json::Value;
#[derive(Debug, Clone)]
pub enum ReportData {
Table {
headers: Vec<String>,
rows: Vec<Vec<String>>,
},
KeyValue {
pairs: Vec<(String, String)>,
},
Raw(Value),
}
pub fn sort_by_id(rows: &mut [Vec<String>], id_col: usize) {
rows.sort_by(|a, b| {
let ak = a.get(id_col).map(|s| s.to_lowercase()).unwrap_or_default();
let bk = b.get(id_col).map(|s| s.to_lowercase()).unwrap_or_default();
ak.cmp(&bk)
});
}
pub fn format_json(value: &Value, pretty: bool) -> String {
if pretty {
serde_json::to_string_pretty(value).unwrap_or_else(|_| "{}".to_string())
} else {
serde_json::to_string(value).unwrap_or_else(|_| "{}".to_string())
}
}
pub fn format_markdown(report: &ReportData) -> String {
match report {
ReportData::Table { headers, rows } => {
let mut out = String::new();
if headers.is_empty() {
return out;
}
out.push('|');
for h in headers {
out.push(' ');
out.push_str(&escape_md_cell(h));
out.push_str(" |");
}
out.push('\n');
out.push('|');
for _ in headers {
out.push_str(" --- |");
}
out.push('\n');
for row in rows {
out.push('|');
for (i, _) in headers.iter().enumerate() {
let cell = row.get(i).map(|s| s.as_str()).unwrap_or("");
out.push(' ');
out.push_str(&escape_md_cell(cell));
out.push_str(" |");
}
out.push('\n');
}
out
}
ReportData::KeyValue { pairs } => {
let mut out = String::new();
out.push_str("| Key | Value |\n");
out.push_str("| --- | --- |\n");
for (k, v) in pairs {
out.push_str(&format!(
"| {} | {} |\n",
escape_md_cell(k),
escape_md_cell(v)
));
}
out
}
ReportData::Raw(value) => {
let json = format_json(value, true);
format!("```json\n{json}\n```\n")
}
}
}
fn escape_md_cell(s: &str) -> String {
s.replace('|', "\\|")
}
pub fn format_table(report: &ReportData) -> String {
match report {
ReportData::Table { headers, rows } => {
if headers.is_empty() {
return String::new();
}
let ncols = headers.len();
let mut widths: Vec<usize> = headers.iter().map(|h| h.len()).collect();
for row in rows {
for (i, cell) in row.iter().enumerate() {
if i < ncols && cell.len() > widths[i] {
widths[i] = cell.len();
}
}
}
let mut out = String::new();
let header_line: Vec<String> = headers
.iter()
.enumerate()
.map(|(i, h)| format!("{:width$}", h, width = widths[i]))
.collect();
out.push_str(&header_line.join(" "));
out.push('\n');
let sep: Vec<String> = widths.iter().map(|&w| "-".repeat(w)).collect();
out.push_str(&sep.join(" "));
out.push('\n');
for row in rows {
let cells: Vec<String> = (0..ncols)
.map(|i| {
let cell = row.get(i).map(|s| s.as_str()).unwrap_or("");
format!("{:width$}", cell, width = widths[i])
})
.collect();
out.push_str(&cells.join(" "));
out.push('\n');
}
out
}
ReportData::KeyValue { pairs } => {
if pairs.is_empty() {
return String::new();
}
let key_width = pairs.iter().map(|(k, _)| k.len()).max().unwrap_or(3);
let mut out = String::new();
let header = format!("{:kw$} {}", "Key", "Value", kw = key_width);
out.push_str(&header);
out.push('\n');
out.push_str(&"-".repeat(key_width));
out.push_str(" ");
out.push_str(&"-".repeat(5));
out.push('\n');
for (k, v) in pairs {
out.push_str(&format!("{:kw$} {}\n", k, v, kw = key_width));
}
out
}
ReportData::Raw(value) => format_json(value, true),
}
}
pub const STATUS_PASS: &str = "[PASS]";
pub const STATUS_FAIL: &str = "[FAIL]";
pub const STATUS_WARN: &str = "[WARN]";
pub const STATUS_DRIFT: &str = "[DRIFT]";
pub const STATUS_STALE: &str = "[STALE]";
pub const STATUS_MISSING: &str = "[MISSING]";
pub fn with_status(indicator: &str, text: &str) -> String {
format!("{indicator} {text}")
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
#[test]
fn format_json_compact_roundtrip() {
let v = serde_json::json!({"a": 1, "b": [true, null]});
let s = format_json(&v, false);
let back: Value = serde_json::from_str(&s).expect("should parse back");
assert_eq!(v, back);
}
#[test]
fn format_json_pretty_contains_newlines() {
let v = serde_json::json!({"x": 42});
let s = format_json(&v, true);
assert!(s.contains('\n'));
}
#[test]
fn format_json_compact_has_no_newlines() {
let v = serde_json::json!({"x": 42, "y": [1, 2, 3]});
let s = format_json(&v, false);
assert!(!s.contains('\n'));
}
#[test]
fn markdown_table_empty_headers_empty_output() {
let data = ReportData::Table {
headers: vec![],
rows: vec![vec!["a".into()]],
};
let out = format_markdown(&data);
assert!(out.is_empty());
}
#[test]
fn markdown_table_renders_header_separator_rows() {
let data = ReportData::Table {
headers: vec!["ID".into(), "Status".into()],
rows: vec![
vec!["agent-a".into(), "[PASS] ok".into()],
vec!["agent-b".into(), "[FAIL] missing".into()],
],
};
let out = format_markdown(&data);
assert!(out.contains("| ID |"));
assert!(out.contains("| --- |"));
assert!(out.contains("agent-a"));
assert!(out.contains("[PASS]"));
assert!(out.contains("[FAIL]"));
}
#[test]
fn markdown_key_value_renders() {
let data = ReportData::KeyValue {
pairs: vec![
("total_agents".into(), "42".into()),
("coverage".into(), "95.0%".into()),
],
};
let out = format_markdown(&data);
assert!(out.contains("| Key | Value |"));
assert!(out.contains("total_agents"));
assert!(out.contains("coverage"));
}
#[test]
fn markdown_raw_renders_json_fence() {
let data = ReportData::Raw(serde_json::json!({"k": "v"}));
let out = format_markdown(&data);
assert!(out.starts_with("```json\n"));
assert!(out.ends_with("```\n"));
}
#[test]
fn markdown_pipe_in_cell_escaped() {
let data = ReportData::Table {
headers: vec!["Col".into()],
rows: vec![vec!["a|b".into()]],
};
let out = format_markdown(&data);
assert!(out.contains("a\\|b"));
}
#[test]
fn ascii_table_empty_output_for_empty_headers() {
let data = ReportData::Table {
headers: vec![],
rows: vec![],
};
assert!(format_table(&data).is_empty());
}
#[test]
fn ascii_table_renders_aligned_columns() {
let data = ReportData::Table {
headers: vec!["ID".into(), "Status".into()],
rows: vec![
vec!["short".into(), "[PASS] ok".into()],
vec!["a-much-longer-id".into(), "[FAIL] missing".into()],
],
};
let out = format_table(&data);
assert!(out.contains("ID"));
assert!(out.contains("--"));
assert!(out.contains("short"));
assert!(out.contains("a-much-longer-id"));
}
#[test]
fn ascii_table_key_value() {
let data = ReportData::KeyValue {
pairs: vec![("agents".into(), "15".into())],
};
let out = format_table(&data);
assert!(out.contains("agents"));
assert!(out.contains("15"));
}
#[test]
fn ascii_table_empty_key_value() {
let data = ReportData::KeyValue { pairs: vec![] };
assert!(format_table(&data).is_empty());
}
#[test]
fn ascii_table_raw_json() {
let data = ReportData::Raw(serde_json::json!({"z": 1}));
let out = format_table(&data);
let back: Value = serde_json::from_str(&out).expect("should parse");
assert_eq!(back["z"], 1);
}
#[test]
fn sort_by_id_basic() {
let mut rows = vec![
vec!["Zebra".into(), "x".into()],
vec!["apple".into(), "y".into()],
vec!["Mango".into(), "z".into()],
];
sort_by_id(&mut rows, 0);
assert_eq!(rows[0][0], "apple");
assert_eq!(rows[1][0], "Mango");
assert_eq!(rows[2][0], "Zebra");
}
#[test]
fn sort_by_id_already_sorted_is_idempotent() {
let mut rows = vec![
vec!["alpha".into()],
vec!["beta".into()],
vec!["gamma".into()],
];
let original = rows.clone();
sort_by_id(&mut rows, 0);
assert_eq!(rows, original);
}
#[test]
fn sort_by_id_empty() {
let mut rows: Vec<Vec<String>> = vec![];
sort_by_id(&mut rows, 0);
assert!(rows.is_empty());
}
#[test]
fn status_indicators_present() {
for indicator in &[
STATUS_PASS,
STATUS_FAIL,
STATUS_WARN,
STATUS_DRIFT,
STATUS_STALE,
STATUS_MISSING,
] {
assert!(indicator.starts_with('['));
assert!(indicator.ends_with(']'));
}
}
#[test]
fn with_status_concatenates() {
let s = with_status(STATUS_PASS, "all good");
assert_eq!(s, "[PASS] all good");
}
proptest! {
#![proptest_config(proptest::test_runner::Config::with_cases(256))]
#[test]
fn prop27_sort_idempotent(
ids in prop::collection::vec("[a-zA-Z0-9_-]{1,20}", 0..30)
) {
let mut rows: Vec<Vec<String>> = ids.iter().map(|id| vec![id.clone()]).collect();
sort_by_id(&mut rows, 0);
let after_first = rows.clone();
sort_by_id(&mut rows, 0);
prop_assert_eq!(rows, after_first, "sort must be idempotent");
}
#[test]
fn prop27_sort_case_insensitive(
id_a in "[A-Z][a-z]{2,8}", id_b in "[a-z]{2,9}", ) {
let mut rows = vec![
vec![id_a.clone()],
vec![id_b.clone()],
];
sort_by_id(&mut rows, 0);
let lower_a = id_a.to_lowercase();
let lower_b = id_b.to_lowercase();
if lower_b <= lower_a {
prop_assert_eq!(&rows[0][0], &id_b);
} else {
prop_assert_eq!(&rows[0][0], &id_a);
}
}
}
proptest! {
#![proptest_config(proptest::test_runner::Config::with_cases(256))]
#[test]
fn prop32_status_indicators_always_present(
text in "[a-z ]{0,40}",
) {
for indicator in &[
STATUS_PASS, STATUS_FAIL, STATUS_WARN,
STATUS_DRIFT, STATUS_STALE, STATUS_MISSING,
] {
let rendered = with_status(indicator, &text);
prop_assert!(
rendered.starts_with('['),
"status row must start with '['; got: {rendered:?}"
);
prop_assert!(
rendered.contains(indicator),
"rendered text must contain the indicator '{indicator}'"
);
}
}
#[test]
fn prop32_markdown_preserves_status_indicator(
indicator in prop_oneof![
Just(STATUS_PASS),
Just(STATUS_FAIL),
Just(STATUS_WARN),
Just(STATUS_DRIFT),
Just(STATUS_STALE),
Just(STATUS_MISSING),
],
id in "[a-z]{3,10}",
) {
let cell = with_status(indicator, &id);
let data = ReportData::Table {
headers: vec!["ID".into(), "Status".into()],
rows: vec![vec![id.clone(), cell.clone()]],
};
let md = format_markdown(&data);
prop_assert!(
md.contains(indicator),
"Markdown output must contain the status indicator '{indicator}'"
);
}
#[test]
fn prop32_table_preserves_status_indicator(
indicator in prop_oneof![
Just(STATUS_PASS),
Just(STATUS_FAIL),
Just(STATUS_WARN),
Just(STATUS_DRIFT),
Just(STATUS_STALE),
Just(STATUS_MISSING),
],
id in "[a-z]{3,10}",
) {
let cell = with_status(indicator, &id);
let data = ReportData::Table {
headers: vec!["ID".into(), "Status".into()],
rows: vec![vec![id.clone(), cell]],
};
let tbl = format_table(&data);
prop_assert!(
tbl.contains(indicator),
"Table output must contain the status indicator '{indicator}'"
);
}
}
}