use serde::Serialize;
use crate::LoadError;
use crate::validation::ValidationContext;
#[derive(Debug, Clone, Serialize)]
pub struct ReportEntry {
pub kind: String,
pub file: String,
pub entity: Option<String>,
pub message: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct ValidationReport {
pub error_count: usize,
pub warning_count: usize,
pub errors: Vec<ReportEntry>,
pub warnings: Vec<ReportEntry>,
}
impl ValidationReport {
pub fn to_json(&self) -> Result<String, LoadError> {
serde_json::to_string_pretty(self)
.map_err(|e| LoadError::parse("<report>", format!("JSON serialization failed: {e}")))
}
}
#[must_use]
pub fn generate_report(ctx: &ValidationContext) -> ValidationReport {
let errors: Vec<ReportEntry> = ctx
.errors()
.into_iter()
.map(|entry| ReportEntry {
kind: format!("{:?}", entry.kind),
file: entry.file.display().to_string(),
entity: entry.entity.clone(),
message: entry.message.clone(),
})
.collect();
let warnings: Vec<ReportEntry> = ctx
.warnings()
.into_iter()
.map(|entry| ReportEntry {
kind: format!("{:?}", entry.kind),
file: entry.file.display().to_string(),
entity: entry.entity.clone(),
message: entry.message.clone(),
})
.collect();
let error_count = errors.len();
let warning_count = warnings.len();
ValidationReport {
error_count,
warning_count,
errors,
warnings,
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use crate::validation::{ErrorKind, ValidationContext};
fn make_context_with_errors_and_warnings() -> ValidationContext {
let mut ctx = ValidationContext::new();
ctx.add_error(
ErrorKind::FileNotFound,
"system/hydros.json",
None::<&str>,
"required file is missing",
);
ctx.add_error(
ErrorKind::ParseError,
"stages.json",
Some("stage_001"),
"malformed JSON at line 42",
);
ctx.add_warning(
ErrorKind::UnusedEntity,
"system/thermals.json",
Some("T1"),
"max_generation=0 for all stages",
);
ctx
}
#[test]
fn test_generate_report_errors_and_warnings() {
let ctx = make_context_with_errors_and_warnings();
let report = generate_report(&ctx);
assert_eq!(report.error_count, 2);
assert_eq!(report.warning_count, 1);
assert_eq!(report.errors.len(), 2);
assert_eq!(report.warnings.len(), 1);
}
#[test]
fn test_generate_report_empty_context() {
let ctx = ValidationContext::new();
let report = generate_report(&ctx);
assert_eq!(report.error_count, 0);
assert_eq!(report.warning_count, 0);
assert!(report.errors.is_empty());
assert!(report.warnings.is_empty());
}
#[test]
fn test_report_to_json_valid() {
let ctx = make_context_with_errors_and_warnings();
let report = generate_report(&ctx);
let json = report.to_json().unwrap();
assert!(json.contains("\"error_count\""));
assert!(json.contains("\"errors\""));
assert!(json.contains("\"warnings\""));
assert!(json.contains("\"warning_count\""));
}
#[test]
fn test_report_entry_fields() {
let mut ctx = ValidationContext::new();
ctx.add_error(
ErrorKind::FileNotFound,
"system/hydros.json",
Some("hydro_042"),
"required file is missing",
);
let report = generate_report(&ctx);
assert_eq!(report.errors.len(), 1);
let entry = &report.errors[0];
assert_eq!(entry.kind, "FileNotFound");
assert!(entry.file.contains("system/hydros.json"));
assert_eq!(entry.entity.as_deref(), Some("hydro_042"));
assert_eq!(entry.message, "required file is missing");
}
#[test]
fn test_generate_report_does_not_consume_context() {
let mut ctx = ValidationContext::new();
ctx.add_error(
ErrorKind::FileNotFound,
"system/hydros.json",
None::<&str>,
"missing",
);
let report = generate_report(&ctx);
assert_eq!(report.error_count, 1);
assert!(ctx.into_result().is_err());
}
}