1use schemars::JsonSchema;
4use serde::{Deserialize, Serialize};
5
6use crate::Producer;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
10pub enum DoctorSchema {
11 #[serde(rename = "mant.doctor/v1")]
13 V1,
14}
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
18#[serde(rename_all = "kebab-case")]
19pub enum DoctorOutcome {
20 Healthy,
22 Warning,
24 Error,
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
30#[serde(rename_all = "kebab-case")]
31pub enum DoctorCheckStatus {
32 Ok,
34 Info,
36 Warning,
38 Error,
40}
41
42#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
44#[serde(rename_all = "camelCase")]
45pub struct DoctorEnvironment {
46 pub os: String,
48 pub arch: String,
50 #[serde(skip_serializing_if = "Option::is_none")]
52 pub data_root: Option<String>,
53 #[serde(skip_serializing_if = "Option::is_none")]
55 pub config_path: Option<String>,
56 #[serde(skip_serializing_if = "Option::is_none")]
58 pub documents_root: Option<String>,
59 #[serde(skip_serializing_if = "Option::is_none")]
61 pub sources_root: Option<String>,
62 pub manual_roots: Vec<String>,
64 pub tldr_roots: Vec<String>,
66}
67
68#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
70#[serde(rename_all = "camelCase")]
71pub struct DoctorCheck {
72 pub code: String,
74 #[serde(skip_serializing_if = "Option::is_none")]
76 pub subject: Option<String>,
77 pub status: DoctorCheckStatus,
79 pub message: String,
81 #[serde(default, skip_serializing_if = "Vec::is_empty")]
83 pub details: Vec<String>,
84 #[serde(skip_serializing_if = "Option::is_none")]
86 pub remediation: Option<String>,
87}
88
89#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
91#[serde(rename_all = "camelCase")]
92pub struct DoctorSummary {
93 pub ok: u32,
95 pub info: u32,
97 pub warnings: u32,
99 pub errors: u32,
101}
102
103#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
105#[serde(rename_all = "camelCase")]
106#[schemars(extend("$id" = "urn:mant:doctor:v1"))]
107pub struct DoctorReport {
108 pub schema: DoctorSchema,
110 pub producer: Producer,
112 pub outcome: DoctorOutcome,
114 pub environment: DoctorEnvironment,
116 pub checks: Vec<DoctorCheck>,
118 pub summary: DoctorSummary,
120}
121
122impl DoctorReport {
123 #[must_use]
126 pub fn new(
127 producer: Producer,
128 environment: DoctorEnvironment,
129 checks: Vec<DoctorCheck>,
130 ) -> Self {
131 let mut summary = DoctorSummary::default();
132 for check in &checks {
133 match check.status {
134 DoctorCheckStatus::Ok => summary.ok += 1,
135 DoctorCheckStatus::Info => summary.info += 1,
136 DoctorCheckStatus::Warning => summary.warnings += 1,
137 DoctorCheckStatus::Error => summary.errors += 1,
138 }
139 }
140 let outcome = if summary.errors > 0 {
141 DoctorOutcome::Error
142 } else if summary.warnings > 0 {
143 DoctorOutcome::Warning
144 } else {
145 DoctorOutcome::Healthy
146 };
147 Self {
148 schema: DoctorSchema::V1,
149 producer,
150 outcome,
151 environment,
152 checks,
153 summary,
154 }
155 }
156
157 #[must_use]
159 pub const fn has_errors(&self) -> bool {
160 self.summary.errors > 0
161 }
162}
163
164#[cfg(test)]
165mod tests {
166 use serde_json::json;
167
168 use super::{DoctorCheck, DoctorCheckStatus, DoctorEnvironment, DoctorOutcome, DoctorReport};
169 use crate::Producer;
170
171 fn environment() -> DoctorEnvironment {
172 DoctorEnvironment {
173 os: "linux".to_owned(),
174 arch: "x86_64".to_owned(),
175 data_root: Some("/data/mant".to_owned()),
176 config_path: Some("/data/mant/sources.toml".to_owned()),
177 documents_root: Some("/data/mant/documents".to_owned()),
178 sources_root: Some("/data/mant/sources".to_owned()),
179 manual_roots: vec!["/usr/share/man".to_owned()],
180 tldr_roots: Vec::new(),
181 }
182 }
183
184 fn producer() -> Producer {
185 Producer {
186 name: "mant".to_owned(),
187 version: "0.9.0".to_owned(),
188 engine: None,
189 }
190 }
191
192 #[test]
193 fn report_derives_warning_outcome_and_counts() {
194 let report = DoctorReport::new(
195 producer(),
196 environment(),
197 vec![
198 DoctorCheck {
199 code: "runtime.libmandoc".to_owned(),
200 subject: None,
201 status: DoctorCheckStatus::Ok,
202 message: "parser probe succeeded".to_owned(),
203 details: Vec::new(),
204 remediation: None,
205 },
206 DoctorCheck {
207 code: "sources.not-installed".to_owned(),
208 subject: Some("team".to_owned()),
209 status: DoctorCheckStatus::Warning,
210 message: "configured source is not installed".to_owned(),
211 details: Vec::new(),
212 remediation: Some("mant --update-docs".to_owned()),
213 },
214 ],
215 );
216
217 assert_eq!(report.outcome, DoctorOutcome::Warning);
218 assert_eq!(report.summary.ok, 1);
219 assert_eq!(report.summary.warnings, 1);
220 assert!(!report.has_errors());
221 assert_eq!(
222 serde_json::to_value(report).expect("doctor report"),
223 json!({
224 "schema": "mant.doctor/v1",
225 "producer": { "name": "mant", "version": "0.9.0" },
226 "outcome": "warning",
227 "environment": {
228 "os": "linux",
229 "arch": "x86_64",
230 "dataRoot": "/data/mant",
231 "configPath": "/data/mant/sources.toml",
232 "documentsRoot": "/data/mant/documents",
233 "sourcesRoot": "/data/mant/sources",
234 "manualRoots": ["/usr/share/man"],
235 "tldrRoots": []
236 },
237 "checks": [
238 {
239 "code": "runtime.libmandoc",
240 "status": "ok",
241 "message": "parser probe succeeded"
242 },
243 {
244 "code": "sources.not-installed",
245 "subject": "team",
246 "status": "warning",
247 "message": "configured source is not installed",
248 "remediation": "mant --update-docs"
249 }
250 ],
251 "summary": { "ok": 1, "info": 0, "warnings": 1, "errors": 0 }
252 })
253 );
254 }
255
256 #[test]
257 fn any_error_makes_the_report_fail() {
258 let report = DoctorReport::new(
259 producer(),
260 environment(),
261 vec![DoctorCheck {
262 code: "paths.data-root".to_owned(),
263 subject: None,
264 status: DoctorCheckStatus::Error,
265 message: "data root is unavailable".to_owned(),
266 details: Vec::new(),
267 remediation: Some("set HOME".to_owned()),
268 }],
269 );
270
271 assert_eq!(report.outcome, DoctorOutcome::Error);
272 assert!(report.has_errors());
273 }
274}