use crate::system_check::category::CheckCategory;
use crate::system_check::id::CheckId;
use crate::system_check::severity::CheckSeverity;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CheckStatus {
Pass,
Fail,
Skip,
}
impl CheckStatus {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Pass => "pass",
Self::Fail => "fail",
Self::Skip => "skip",
}
}
}
impl std::fmt::Display for CheckStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CheckResult {
pub id: CheckId,
pub category: CheckCategory,
pub severity: CheckSeverity,
pub status: CheckStatus,
pub description: &'static str,
pub context: String,
pub fix_hint: Option<String>,
}
#[cfg(feature = "serde")]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct CheckResultJson {
pub id: String,
pub category: String,
pub severity: String,
pub status: String,
pub description: String,
pub context: String,
pub fix_hint: Option<String>,
}
#[cfg(feature = "serde")]
impl From<&CheckResult> for CheckResultJson {
fn from(result: &CheckResult) -> Self {
Self {
id: result.id.as_str().to_string(),
category: result.category.as_str().to_string(),
severity: result.severity.as_str().to_string(),
status: result.status.as_str().to_string(),
description: result.description.to_string(),
context: result.context.clone(),
fix_hint: result.fix_hint.clone(),
}
}
}
#[derive(Debug, Clone, Default)]
pub struct SystemCheckReport {
pub results: Vec<CheckResult>,
}
impl SystemCheckReport {
#[must_use]
pub fn has_error(&self) -> bool {
self.results
.iter()
.any(|r| r.status == CheckStatus::Fail && r.severity == CheckSeverity::Error)
}
#[must_use]
pub fn has_failure(&self) -> bool {
self.results.iter().any(|r| r.status == CheckStatus::Fail)
}
#[must_use]
pub fn len(&self) -> usize {
self.results.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.results.is_empty()
}
#[cfg(feature = "serde")]
#[must_use]
pub fn to_json_view(&self) -> SystemCheckReportJson {
SystemCheckReportJson {
checks: self.results.iter().map(CheckResultJson::from).collect(),
}
}
}
#[cfg(feature = "serde")]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SystemCheckReportJson {
pub checks: Vec<CheckResultJson>,
}
#[cfg(test)]
mod tests {
use super::*;
fn result(status: CheckStatus, severity: CheckSeverity) -> CheckResult {
CheckResult {
id: CheckId::new_unchecked("ARC0001"),
category: CheckCategory::Framework,
severity,
status,
description: "test",
context: "ctx".to_string(),
fix_hint: None,
}
}
#[test]
fn status_strings_are_stable_lowercase() {
assert_eq!(CheckStatus::Pass.as_str(), "pass");
assert_eq!(CheckStatus::Fail.as_str(), "fail");
assert_eq!(CheckStatus::Skip.as_str(), "skip");
}
#[cfg(feature = "serde")]
#[test]
fn json_view_flattens_typed_fields_to_strings() {
let r = result(CheckStatus::Fail, CheckSeverity::Warning);
let view = CheckResultJson::from(&r);
assert_eq!(view.id, "ARC0001");
assert_eq!(view.category, "framework");
assert_eq!(view.severity, "warning");
assert_eq!(view.status, "fail");
assert_eq!(view.description, "test");
assert_eq!(view.context, "ctx");
assert_eq!(view.fix_hint, None);
}
#[cfg(feature = "serde")]
#[test]
fn json_view_serializes_fix_hint_as_null_when_absent() {
let r = result(CheckStatus::Pass, CheckSeverity::Info);
let view = CheckResultJson::from(&r);
let json = serde_json::to_value(&view).expect("serialize");
assert!(json.get("fix_hint").expect("fix_hint present").is_null());
}
#[cfg(feature = "serde")]
#[test]
fn json_view_serializes_fix_hint_when_present() {
let mut r = result(CheckStatus::Fail, CheckSeverity::Error);
r.fix_hint = Some("set DATABASE_URL".to_string());
let view = CheckResultJson::from(&r);
assert_eq!(view.fix_hint.as_deref(), Some("set DATABASE_URL"));
}
#[test]
fn report_has_error_only_when_error_severity_fails() {
let mut report = SystemCheckReport::default();
assert!(!report.has_error());
report
.results
.push(result(CheckStatus::Pass, CheckSeverity::Error));
assert!(!report.has_error());
report
.results
.push(result(CheckStatus::Fail, CheckSeverity::Warning));
assert!(!report.has_error());
assert!(report.has_failure());
report
.results
.push(result(CheckStatus::Fail, CheckSeverity::Error));
assert!(report.has_error());
}
#[cfg(feature = "serde")]
#[test]
fn report_to_json_view_round_trips() {
let mut report = SystemCheckReport::default();
report
.results
.push(result(CheckStatus::Pass, CheckSeverity::Info));
report
.results
.push(result(CheckStatus::Skip, CheckSeverity::Info));
let view = report.to_json_view();
let json = serde_json::to_string(&view).expect("serialize");
assert!(json.contains("\"status\":\"pass\""));
assert!(json.contains("\"status\":\"skip\""));
let back: SystemCheckReportJson = serde_json::from_str(&json).expect("deserialize");
assert_eq!(back.checks.len(), 2);
}
#[test]
fn report_len_and_empty() {
let report = SystemCheckReport::default();
assert!(report.is_empty());
assert_eq!(report.len(), 0);
}
}