use crate::catalogue;
use crate::report::{CategorySummary, ComplianceReport, Violation};
use serde_json::{json, Map, Value};
pub fn to_payload(report: &ComplianceReport) -> Value {
let mut sorted = report.violations.clone();
crate::report::sort_violations(&mut sorted);
let violations: Vec<Value> = sorted.iter().map(violation_value).collect();
let categories: Vec<Value> = report.categories.iter().map(category_value).collect();
let skipped_rules: Vec<Value> = report
.skipped_rules
.iter()
.map(|s| json!({"rule_id": s.rule_id, "reason": s.reason}))
.collect();
json!({
"tool_version": report.tool_version,
"journal_id": report.journal_id,
"compliance_percentage": report.compliance_percentage,
"categories": categories,
"violations": violations,
"skipped_rules": skipped_rules,
})
}
fn violation_value(v: &Violation) -> Value {
let meta = catalogue::lookup(&v.rule_id);
let guide_section = meta.map(|m| m.guide_section).unwrap_or("");
let guide_url = meta.and_then(|m| m.guide_url);
let confidence = meta.map(|m| m.confidence).unwrap_or("high");
json!({
"file": v.file,
"line": v.line,
"column": v.column,
"rule_id": v.rule_id,
"severity": v.severity.as_str(),
"message": v.message,
"suggestion": v.suggestion,
"fix": Value::Null,
"guide_section": guide_section,
"guide_url": guide_url,
"confidence": confidence,
})
}
fn category_value(c: &CategorySummary) -> Value {
json!({
"category_id": c.category_id,
"title": c.title,
"status": c.status.as_str(),
"rules_applied": c.rules_applied,
"rules_passed": c.rules_passed,
})
}
pub fn render(report: &ComplianceReport) -> String {
let payload = to_payload(report);
let mut out = String::new();
write_value(&payload, 0, &mut out);
out.push('\n');
out
}
pub(crate) fn write_value(value: &Value, indent: usize, out: &mut String) {
match value {
Value::Null => out.push_str("null"),
Value::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
Value::Number(n) => out.push_str(&n.to_string()),
Value::String(s) => write_py_string(s, out),
Value::Array(items) => write_array(items, indent, out),
Value::Object(map) => write_object(map, indent, out),
}
}
fn write_array(items: &[Value], indent: usize, out: &mut String) {
if items.is_empty() {
out.push_str("[]");
return;
}
out.push('[');
let inner_indent = indent + 2;
for (i, item) in items.iter().enumerate() {
out.push('\n');
out.push_str(&" ".repeat(inner_indent));
write_value(item, inner_indent, out);
if i + 1 < items.len() {
out.push(',');
}
}
out.push('\n');
out.push_str(&" ".repeat(indent));
out.push(']');
}
fn write_object(map: &Map<String, Value>, indent: usize, out: &mut String) {
if map.is_empty() {
out.push_str("{}");
return;
}
let mut keys: Vec<&String> = map.keys().collect();
keys.sort();
out.push('{');
let inner_indent = indent + 2;
for (i, key) in keys.iter().enumerate() {
out.push('\n');
out.push_str(&" ".repeat(inner_indent));
write_py_string(key, out);
out.push_str(": ");
write_value(&map[*key], inner_indent, out);
if i + 1 < keys.len() {
out.push(',');
}
}
out.push('\n');
out.push_str(&" ".repeat(indent));
out.push('}');
}
fn write_py_string(s: &str, out: &mut String) {
out.push('"');
for c in s.chars() {
match c {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
'\x08' => out.push_str("\\b"),
'\x0c' => out.push_str("\\f"),
c if (' '..='~').contains(&c) => out.push(c),
c => {
let cp = c as u32;
if cp <= 0xFFFF {
out.push_str(&format!("\\u{cp:04x}"));
} else {
let cp = cp - 0x10000;
let high = 0xD800 + (cp >> 10);
let low = 0xDC00 + (cp & 0x3FF);
out.push_str(&format!("\\u{high:04x}\\u{low:04x}"));
}
}
}
}
out.push('"');
}