use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::Producer;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub enum DoctorSchema {
#[serde(rename = "mant.doctor/v1")]
V1,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "kebab-case")]
pub enum DoctorOutcome {
Healthy,
Warning,
Error,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "kebab-case")]
pub enum DoctorCheckStatus {
Ok,
Info,
Warning,
Error,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct DoctorEnvironment {
pub os: String,
pub arch: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub data_root: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub config_path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub documents_root: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sources_root: Option<String>,
pub manual_roots: Vec<String>,
pub tldr_roots: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct DoctorCheck {
pub code: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub subject: Option<String>,
pub status: DoctorCheckStatus,
pub message: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub details: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub remediation: Option<String>,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct DoctorSummary {
pub ok: u32,
pub info: u32,
pub warnings: u32,
pub errors: u32,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[schemars(extend("$id" = "urn:mant:doctor:v1"))]
pub struct DoctorReport {
pub schema: DoctorSchema,
pub producer: Producer,
pub outcome: DoctorOutcome,
pub environment: DoctorEnvironment,
pub checks: Vec<DoctorCheck>,
pub summary: DoctorSummary,
}
impl DoctorReport {
#[must_use]
pub fn new(
producer: Producer,
environment: DoctorEnvironment,
checks: Vec<DoctorCheck>,
) -> Self {
let mut summary = DoctorSummary::default();
for check in &checks {
match check.status {
DoctorCheckStatus::Ok => summary.ok += 1,
DoctorCheckStatus::Info => summary.info += 1,
DoctorCheckStatus::Warning => summary.warnings += 1,
DoctorCheckStatus::Error => summary.errors += 1,
}
}
let outcome = if summary.errors > 0 {
DoctorOutcome::Error
} else if summary.warnings > 0 {
DoctorOutcome::Warning
} else {
DoctorOutcome::Healthy
};
Self {
schema: DoctorSchema::V1,
producer,
outcome,
environment,
checks,
summary,
}
}
#[must_use]
pub const fn has_errors(&self) -> bool {
self.summary.errors > 0
}
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::{DoctorCheck, DoctorCheckStatus, DoctorEnvironment, DoctorOutcome, DoctorReport};
use crate::Producer;
fn environment() -> DoctorEnvironment {
DoctorEnvironment {
os: "linux".to_owned(),
arch: "x86_64".to_owned(),
data_root: Some("/data/mant".to_owned()),
config_path: Some("/data/mant/sources.toml".to_owned()),
documents_root: Some("/data/mant/documents".to_owned()),
sources_root: Some("/data/mant/sources".to_owned()),
manual_roots: vec!["/usr/share/man".to_owned()],
tldr_roots: Vec::new(),
}
}
fn producer() -> Producer {
Producer {
name: "mant".to_owned(),
version: "0.8.0".to_owned(),
engine: None,
}
}
#[test]
fn report_derives_warning_outcome_and_counts() {
let report = DoctorReport::new(
producer(),
environment(),
vec![
DoctorCheck {
code: "runtime.libmandoc".to_owned(),
subject: None,
status: DoctorCheckStatus::Ok,
message: "parser probe succeeded".to_owned(),
details: Vec::new(),
remediation: None,
},
DoctorCheck {
code: "sources.not-installed".to_owned(),
subject: Some("team".to_owned()),
status: DoctorCheckStatus::Warning,
message: "configured source is not installed".to_owned(),
details: Vec::new(),
remediation: Some("mant --update-docs".to_owned()),
},
],
);
assert_eq!(report.outcome, DoctorOutcome::Warning);
assert_eq!(report.summary.ok, 1);
assert_eq!(report.summary.warnings, 1);
assert!(!report.has_errors());
assert_eq!(
serde_json::to_value(report).expect("doctor report"),
json!({
"schema": "mant.doctor/v1",
"producer": { "name": "mant", "version": "0.8.0" },
"outcome": "warning",
"environment": {
"os": "linux",
"arch": "x86_64",
"dataRoot": "/data/mant",
"configPath": "/data/mant/sources.toml",
"documentsRoot": "/data/mant/documents",
"sourcesRoot": "/data/mant/sources",
"manualRoots": ["/usr/share/man"],
"tldrRoots": []
},
"checks": [
{
"code": "runtime.libmandoc",
"status": "ok",
"message": "parser probe succeeded"
},
{
"code": "sources.not-installed",
"subject": "team",
"status": "warning",
"message": "configured source is not installed",
"remediation": "mant --update-docs"
}
],
"summary": { "ok": 1, "info": 0, "warnings": 1, "errors": 0 }
})
);
}
#[test]
fn any_error_makes_the_report_fail() {
let report = DoctorReport::new(
producer(),
environment(),
vec![DoctorCheck {
code: "paths.data-root".to_owned(),
subject: None,
status: DoctorCheckStatus::Error,
message: "data root is unavailable".to_owned(),
details: Vec::new(),
remediation: Some("set HOME".to_owned()),
}],
);
assert_eq!(report.outcome, DoctorOutcome::Error);
assert!(report.has_errors());
}
}