1use std::path::Path;
2
3use crate::model::Config;
4use serde::Serialize;
5
6#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
7#[serde(rename_all = "snake_case")]
8pub enum CheckStatus {
9 Ok,
10 Warn,
11 Error,
12}
13
14#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
15pub struct Check {
16 pub name: String,
17 pub status: CheckStatus,
18 pub detail: String,
19}
20
21#[derive(Debug, Clone, Serialize)]
22pub struct DiagResult {
23 pub checks: Vec<Check>,
24}
25
26impl DiagResult {
27 pub fn is_healthy(&self) -> bool {
28 self.checks
29 .iter()
30 .all(|c| c.status == CheckStatus::Ok || c.status == CheckStatus::Warn)
31 }
32}
33
34pub fn diagnose<F>(config_path: &Path, config: Option<&Config>, command_exists: F) -> DiagResult
39where
40 F: Fn(&str) -> bool,
41{
42 let mut checks = Vec::new();
43
44 let config_exists = config_path.exists();
46 checks.push(Check {
47 name: "config_file".into(),
48 status: if config_exists {
49 CheckStatus::Ok
50 } else {
51 CheckStatus::Error
52 },
53 detail: if config_exists {
54 format!("found: {}", config_path.display())
55 } else {
56 format!("not found: {}", config_path.display())
57 },
58 });
59
60 checks.push(Check {
62 name: "config_parse".into(),
63 status: if config.is_some() {
64 CheckStatus::Ok
65 } else {
66 CheckStatus::Error
67 },
68 detail: if config.is_some() {
69 "config loaded successfully".into()
70 } else {
71 "failed to load config".into()
72 },
73 });
74
75 if let Some(cfg) = config {
77 for abbr in &cfg.abbr {
78 if let Some(cmds) = &abbr.when_command_exists {
79 for cmd in cmds {
80 let exists = command_exists(cmd);
81 checks.push(Check {
82 name: format!("command:{cmd}"),
83 status: if exists {
84 CheckStatus::Ok
85 } else {
86 CheckStatus::Warn
87 },
88 detail: if exists {
89 format!("'{cmd}' found (required by '{}')", abbr.key)
90 } else {
91 format!("'{cmd}' not found (required by '{}')", abbr.key)
92 },
93 });
94 }
95 }
96 }
97 }
98
99 DiagResult { checks }
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105 use crate::model::{Abbr, Config};
106 use std::io::Write;
107
108 fn test_config(abbrs: Vec<Abbr>) -> Config {
109 Config {
110 version: 1,
111 keybind: crate::model::KeybindConfig::default(),
112 abbr: abbrs,
113 }
114 }
115
116 fn abbr_when(key: &str, exp: &str, cmds: Vec<&str>) -> Abbr {
117 Abbr {
118 key: key.into(),
119 expand: exp.into(),
120 when_command_exists: Some(cmds.into_iter().map(String::from).collect()),
121 }
122 }
123
124 #[test]
125 fn all_healthy() {
126 let dir = tempfile::tempdir().unwrap();
127 let path = dir.path().join("config.toml");
128 let mut f = std::fs::File::create(&path).unwrap();
129 writeln!(f, "version = 1").unwrap();
130
131 let cfg = test_config(vec![abbr_when("ls", "lsd", vec!["lsd"])]);
132 let result = diagnose(&path, Some(&cfg), |_| true);
133
134 assert!(result.is_healthy());
135 assert_eq!(result.checks[0].status, CheckStatus::Ok); assert_eq!(result.checks[1].status, CheckStatus::Ok); assert_eq!(result.checks[2].status, CheckStatus::Ok); }
139
140 #[test]
141 fn config_file_missing() {
142 let path = std::path::PathBuf::from("/nonexistent/config.toml");
143 let result = diagnose(&path, None, |_| true);
144
145 assert!(!result.is_healthy());
146 assert_eq!(result.checks[0].status, CheckStatus::Error);
147 assert_eq!(result.checks[1].status, CheckStatus::Error);
148 }
149
150 #[test]
151 fn command_not_found_is_warn() {
152 let dir = tempfile::tempdir().unwrap();
153 let path = dir.path().join("config.toml");
154 std::fs::write(&path, "version = 1").unwrap();
155
156 let cfg = test_config(vec![abbr_when("ls", "lsd", vec!["lsd"])]);
157 let result = diagnose(&path, Some(&cfg), |_| false);
158
159 assert!(result.is_healthy());
161 assert_eq!(result.checks[2].status, CheckStatus::Warn);
162 assert!(result.checks[2].detail.contains("not found"));
163 }
164
165 #[test]
166 fn diag_result_is_healthy_with_error() {
167 let result = DiagResult {
168 checks: vec![Check {
169 name: "test".into(),
170 status: CheckStatus::Error,
171 detail: "bad".into(),
172 }],
173 };
174 assert!(!result.is_healthy());
175 }
176}