shortcuts_tui/error/
mod.rs

1//! Error handling for the shortcuts-tui application.
2
3use std::path::PathBuf;
4use thiserror::Error;
5
6/// Result type alias for the application
7pub type Result<T> = std::result::Result<T, ShortcutsError>;
8
9/// Main error type for the application
10#[derive(Error, Debug)]
11pub enum ShortcutsError {
12    /// Configuration-related errors
13    #[error(transparent)]
14    Config(#[from] ConfigError),
15    
16    /// UI-related errors
17    #[error(transparent)]
18    Ui(#[from] UiError),
19    
20    /// IO errors
21    #[error("IO error: {0}")]
22    Io(#[from] std::io::Error),
23    
24    /// Generic error with message
25    #[error("{0}")]
26    Generic(String),
27}
28
29/// Configuration-related errors
30#[derive(Error, Debug)]
31pub enum ConfigError {
32    /// Configuration file not found
33    #[error("Configuration file not found: {0}")]
34    FileNotFound(PathBuf),
35    
36    /// No configuration file found in search paths
37    #[error("No configuration file found in search paths: {}", format_paths(.0))]
38    NoConfigFound(Vec<PathBuf>),
39    
40    /// IO error when reading/writing configuration
41    #[error("IO error accessing {0}: {1}")]
42    IoError(PathBuf, std::io::Error),
43    
44    /// TOML parsing error
45    #[error("TOML parsing error: {0}")]
46    TomlParseError(#[from] toml::de::Error),
47    
48    /// TOML serialization error
49    #[error("TOML serialization error: {0}")]
50    TomlSerializeError(#[from] toml::ser::Error),
51    
52    /// JSON parsing error
53    #[error("JSON parsing error: {0}")]
54    JsonParseError(serde_json::Error),
55    
56    /// JSON serialization error
57    #[error("JSON serialization error: {0}")]
58    JsonSerializeError(serde_json::Error),
59    
60    /// Configuration validation error
61    #[error("Configuration validation failed: {}", format_validation_errors(.0))]
62    ValidationError(Vec<String>),
63    
64    /// Invalid configuration format
65    #[error("Invalid configuration format. Expected TOML or JSON")]
66    InvalidFormat,
67}
68
69/// UI-related errors
70#[derive(Error, Debug)]
71pub enum UiError {
72    /// Terminal setup error
73    #[error("Failed to setup terminal: {0}")]
74    TerminalSetup(#[from] std::io::Error),
75    
76    /// Terminal restoration error
77    #[error("Failed to restore terminal: {0}")]
78    TerminalRestore(std::io::Error),
79    
80    /// Rendering error
81    #[error("Rendering error: {0}")]
82    Render(String),
83    
84    /// Event handling error
85    #[error("Event handling error: {0}")]
86    EventHandling(String),
87    
88    /// Widget error
89    #[error("Widget error: {0}")]
90    Widget(String),
91}
92
93/// Format a list of paths for error display
94fn format_paths(paths: &[PathBuf]) -> String {
95    paths
96        .iter()
97        .map(|p| p.display().to_string())
98        .collect::<Vec<_>>()
99        .join(", ")
100}
101
102/// Format validation errors for display
103fn 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
119// Custom From implementations for ConfigError
120impl 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
130/// Extension trait for Results to provide additional error context
131pub trait ErrorContext<T> {
132    /// Add context to an error
133    fn with_context<F>(self, f: F) -> Result<T>
134    where
135        F: FnOnce() -> String;
136    
137    /// Add static context to an error
138    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}