shortcuts_tui/error/
mod.rs1use std::path::PathBuf;
4use thiserror::Error;
5
6pub type Result<T> = std::result::Result<T, ShortcutsError>;
8
9#[derive(Error, Debug)]
11pub enum ShortcutsError {
12 #[error(transparent)]
14 Config(#[from] ConfigError),
15
16 #[error(transparent)]
18 Ui(#[from] UiError),
19
20 #[error("IO error: {0}")]
22 Io(#[from] std::io::Error),
23
24 #[error("{0}")]
26 Generic(String),
27}
28
29#[derive(Error, Debug)]
31pub enum ConfigError {
32 #[error("Configuration file not found: {0}")]
34 FileNotFound(PathBuf),
35
36 #[error("No configuration file found in search paths: {}", format_paths(.0))]
38 NoConfigFound(Vec<PathBuf>),
39
40 #[error("IO error accessing {0}: {1}")]
42 IoError(PathBuf, std::io::Error),
43
44 #[error("TOML parsing error: {0}")]
46 TomlParseError(#[from] toml::de::Error),
47
48 #[error("TOML serialization error: {0}")]
50 TomlSerializeError(#[from] toml::ser::Error),
51
52 #[error("JSON parsing error: {0}")]
54 JsonParseError(serde_json::Error),
55
56 #[error("JSON serialization error: {0}")]
58 JsonSerializeError(serde_json::Error),
59
60 #[error("Configuration validation failed: {}", format_validation_errors(.0))]
62 ValidationError(Vec<String>),
63
64 #[error("Invalid configuration format. Expected TOML or JSON")]
66 InvalidFormat,
67}
68
69#[derive(Error, Debug)]
71pub enum UiError {
72 #[error("Failed to setup terminal: {0}")]
74 TerminalSetup(#[from] std::io::Error),
75
76 #[error("Failed to restore terminal: {0}")]
78 TerminalRestore(std::io::Error),
79
80 #[error("Rendering error: {0}")]
82 Render(String),
83
84 #[error("Event handling error: {0}")]
86 EventHandling(String),
87
88 #[error("Widget error: {0}")]
90 Widget(String),
91}
92
93fn format_paths(paths: &[PathBuf]) -> String {
95 paths
96 .iter()
97 .map(|p| p.display().to_string())
98 .collect::<Vec<_>>()
99 .join(", ")
100}
101
102fn format_validation_errors(errors: &[String]) -> String {
104 errors.join("; ")
105}
106
107impl From<String> for ShortcutsError {
108 fn from(msg: String) -> Self {
109 ShortcutsError::Generic(msg)
110 }
111}
112
113impl From<&str> for ShortcutsError {
114 fn from(msg: &str) -> Self {
115 ShortcutsError::Generic(msg.to_string())
116 }
117}
118
119impl From<serde_json::Error> for ConfigError {
121 fn from(err: serde_json::Error) -> Self {
122 if err.is_syntax() || err.is_data() {
123 ConfigError::JsonParseError(err)
124 } else {
125 ConfigError::JsonSerializeError(err)
126 }
127 }
128}
129
130pub trait ErrorContext<T> {
132 fn with_context<F>(self, f: F) -> Result<T>
134 where
135 F: FnOnce() -> String;
136
137 fn context(self, msg: &str) -> Result<T>;
139}
140
141impl<T, E> ErrorContext<T> for std::result::Result<T, E>
142where
143 E: Into<ShortcutsError>,
144{
145 fn with_context<F>(self, f: F) -> Result<T>
146 where
147 F: FnOnce() -> String,
148 {
149 self.map_err(|e| {
150 let base_error = e.into();
151 let context = f();
152 ShortcutsError::Generic(format!("{}: {}", context, base_error))
153 })
154 }
155
156 fn context(self, msg: &str) -> Result<T> {
157 self.with_context(|| msg.to_string())
158 }
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164 use std::path::PathBuf;
165
166 #[test]
167 fn test_config_error_display() {
168 let error = ConfigError::FileNotFound(PathBuf::from("/test/path"));
169 assert!(error.to_string().contains("Configuration file not found"));
170 assert!(error.to_string().contains("/test/path"));
171 }
172
173 #[test]
174 fn test_validation_error_display() {
175 let errors = vec![
176 "Duplicate tool ID: test".to_string(),
177 "Invalid category reference".to_string(),
178 ];
179 let error = ConfigError::ValidationError(errors);
180 let error_string = error.to_string();
181 assert!(error_string.contains("Configuration validation failed"));
182 assert!(error_string.contains("Duplicate tool ID: test"));
183 assert!(error_string.contains("Invalid category reference"));
184 }
185
186 #[test]
187 fn test_no_config_found_display() {
188 let paths = vec![
189 PathBuf::from("/path1"),
190 PathBuf::from("/path2"),
191 ];
192 let error = ConfigError::NoConfigFound(paths);
193 let error_string = error.to_string();
194 assert!(error_string.contains("No configuration file found"));
195 assert!(error_string.contains("/path1"));
196 assert!(error_string.contains("/path2"));
197 }
198
199 #[test]
200 fn test_shortcuts_error_from_config_error() {
201 let config_error = ConfigError::InvalidFormat;
202 let shortcuts_error: ShortcutsError = config_error.into();
203 match shortcuts_error {
204 ShortcutsError::Config(ConfigError::InvalidFormat) => {},
205 _ => panic!("Expected Config error"),
206 }
207 }
208
209 #[test]
210 fn test_shortcuts_error_from_string() {
211 let error: ShortcutsError = "Test error".into();
212 match error {
213 ShortcutsError::Generic(msg) => assert_eq!(msg, "Test error"),
214 _ => panic!("Expected Generic error"),
215 }
216 }
217
218 #[test]
219 fn test_error_context() {
220 let result: std::result::Result<(), std::io::Error> =
221 Err(std::io::Error::new(std::io::ErrorKind::NotFound, "file not found"));
222
223 let with_context = result.context("Failed to read configuration");
224 assert!(with_context.is_err());
225
226 let error_msg = with_context.unwrap_err().to_string();
227 assert!(error_msg.contains("Failed to read configuration"));
228 assert!(error_msg.contains("file not found"));
229 }
230
231 #[test]
232 fn test_error_with_context_closure() {
233 let result: std::result::Result<(), std::io::Error> =
234 Err(std::io::Error::new(std::io::ErrorKind::PermissionDenied, "access denied"));
235
236 let file_path = "/important/file";
237 let with_context = result.with_context(|| format!("Failed to access {}", file_path));
238 assert!(with_context.is_err());
239
240 let error_msg = with_context.unwrap_err().to_string();
241 assert!(error_msg.contains("Failed to access /important/file"));
242 assert!(error_msg.contains("access denied"));
243 }
244
245 #[test]
246 fn test_ui_error_display() {
247 let io_error = std::io::Error::new(std::io::ErrorKind::BrokenPipe, "broken pipe");
248 let ui_error = UiError::TerminalSetup(io_error);
249 assert!(ui_error.to_string().contains("Failed to setup terminal"));
250 assert!(ui_error.to_string().contains("broken pipe"));
251 }
252
253 #[test]
254 fn test_format_paths() {
255 let paths = vec![
256 PathBuf::from("/home/user"),
257 PathBuf::from("/etc/config"),
258 PathBuf::from("./local"),
259 ];
260 let formatted = format_paths(&paths);
261 assert!(formatted.contains("/home/user"));
262 assert!(formatted.contains("/etc/config"));
263 assert!(formatted.contains("./local"));
264 assert!(formatted.contains(", "));
265 }
266
267 #[test]
268 fn test_format_validation_errors() {
269 let errors = vec![
270 "Error 1".to_string(),
271 "Error 2".to_string(),
272 "Error 3".to_string(),
273 ];
274 let formatted = format_validation_errors(&errors);
275 assert_eq!(formatted, "Error 1; Error 2; Error 3");
276 }
277}