use crate::drift::DriftReport;
use crate::manifest::Manifest;
pub fn render_check(report: &DriftReport) {
print!("{}", render_check_to_string(report));
}
pub fn render_check_to_string(report: &DriftReport) -> String {
let mut out = String::from("service,severity,kind,message,detail\n");
for item in &report.drifts {
let kind = serde_json::to_value(&item.kind)
.ok()
.and_then(|v| v.as_str().map(str::to_owned))
.unwrap_or_default();
let severity = serde_json::to_value(&item.severity)
.ok()
.and_then(|v| v.as_str().map(str::to_owned))
.unwrap_or_default();
out.push_str(&format!(
"{},{},{},{},{}\n",
csv_field(&item.service),
severity,
kind,
csv_field(&item.message),
csv_field(item.detail.as_deref().unwrap_or(""))
));
}
out
}
pub fn render_export(manifest: &Manifest) {
print!("{}", render_export_to_string(manifest));
}
pub fn render_export_to_string(manifest: &Manifest) -> String {
let mut out = String::from("name,language,platform,role,url,team,oncall\n");
for svc in &manifest.services {
out.push_str(&format!(
"{},{},{},{},{},{},{}\n",
csv_field(&svc.name),
csv_field(svc.language.as_deref().unwrap_or("")),
csv_field(svc.platform.as_deref().unwrap_or("")),
csv_field(svc.role.as_deref().unwrap_or("")),
csv_field(svc.url.as_deref().unwrap_or("")),
csv_field(svc.team.as_deref().unwrap_or("")),
csv_field(svc.oncall.as_deref().unwrap_or(""))
));
}
out
}
pub fn csv_field(s: &str) -> String {
if s.contains(',') || s.contains('"') || s.contains('\n') || s.contains('\r') {
format!("\"{}\"", s.replace('"', "\"\""))
} else {
s.to_owned()
}
}
#[cfg(test)]
mod tests {
use super::{csv_field, render_check_to_string};
use crate::drift::DriftReport;
#[test]
fn leaves_plain_values_unquoted() {
assert_eq!(csv_field("service-a"), "service-a");
}
#[test]
fn quotes_and_escapes_special_values() {
assert_eq!(csv_field("a,b"), "\"a,b\"");
assert_eq!(csv_field("a\"b"), "\"a\"\"b\"");
assert_eq!(csv_field("a\nb"), "\"a\nb\"");
assert_eq!(csv_field("a\rb"), "\"a\rb\"");
}
#[test]
fn render_check_to_string_has_header_and_rows() {
let report: DriftReport = serde_json::from_value(serde_json::json!({
"manifest": "services.yaml",
"declared": 1,
"discovered": 0,
"drifts": [
{
"kind": "declared_missing_from_repo",
"severity": "error",
"service": "api,svc",
"message": "missing \"directory\"",
"detail": null
}
]
}))
.unwrap();
let csv = render_check_to_string(&report);
assert!(csv.starts_with("service,severity,kind,message,detail\n"));
assert!(csv.contains(
"\"api,svc\",error,declared_missing_from_repo,\"missing \"\"directory\"\"\","
));
}
#[test]
fn render_check_to_string_with_no_drift_keeps_header_only() {
let report: DriftReport = serde_json::from_value(serde_json::json!({
"manifest": "services.yaml",
"declared": 1,
"discovered": 1,
"drifts": []
}))
.unwrap();
let csv = render_check_to_string(&report);
assert_eq!(csv, "service,severity,kind,message,detail\n");
}
#[test]
fn render_check_to_string_quotes_multiline_detail() {
let report: DriftReport = serde_json::from_value(serde_json::json!({
"manifest": "services.yaml",
"declared": 1,
"discovered": 0,
"drifts": [
{
"kind": "declared_missing_from_repo",
"severity": "error",
"service": "api",
"message": "missing service directory",
"detail": "line one\nline two"
}
]
}))
.unwrap();
let csv = render_check_to_string(&report);
assert!(csv.contains(
"api,error,declared_missing_from_repo,missing service directory,\"line one\nline two\""
));
}
}