Skip to main content

things3_core/
error.rs

1//! Error types for the Things Core library
2
3use thiserror::Error;
4
5/// Result type alias for Things operations
6pub type Result<T> = std::result::Result<T, ThingsError>;
7
8/// Main error type for Things operations
9#[non_exhaustive]
10#[derive(Error, Debug)]
11pub enum ThingsError {
12    #[error("Database error: {0}")]
13    Database(String),
14
15    #[error("Serialization error: {0}")]
16    Serialization(#[from] serde_json::Error),
17
18    #[error("IO error: {0}")]
19    Io(#[from] std::io::Error),
20
21    #[error("Database not found: {path}. Ensure Things 3 is installed and has been opened at least once, or specify a custom database path.")]
22    DatabaseNotFound { path: String },
23
24    #[error("Invalid UUID: {uuid}. UUIDs must be in format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx")]
25    InvalidUuid { uuid: String },
26
27    #[error("Invalid date: {date}")]
28    InvalidDate { date: String },
29
30    #[error("Date validation failed: {0}")]
31    DateValidation(#[from] crate::database::DateValidationError),
32
33    #[error("Date conversion failed: {0}")]
34    DateConversion(#[from] crate::database::DateConversionError),
35
36    #[error("Task not found: {uuid}. The task may have been deleted or moved. Try searching by title instead.")]
37    TaskNotFound { uuid: String },
38
39    #[error("Project not found: {uuid}. The project may have been deleted. Verify the UUID or list all projects to find the correct one.")]
40    ProjectNotFound { uuid: String },
41
42    #[error("Area not found: {uuid}. The area may have been deleted. Verify the UUID or list all areas to find the correct one.")]
43    AreaNotFound { uuid: String },
44
45    #[error("Validation error: {message}")]
46    Validation { message: String },
47
48    #[error("Invalid cursor: {0}")]
49    InvalidCursor(String),
50
51    #[error("Configuration error: {message}")]
52    Configuration { message: String },
53
54    #[error("AppleScript automation failed: {message}")]
55    AppleScript { message: String },
56
57    #[error("Unknown error: {message}")]
58    Unknown { message: String },
59}
60
61impl ThingsError {
62    /// Create a validation error
63    pub fn validation(message: impl Into<String>) -> Self {
64        Self::Validation {
65            message: message.into(),
66        }
67    }
68
69    /// Create a configuration error
70    pub fn configuration(message: impl Into<String>) -> Self {
71        Self::Configuration {
72            message: message.into(),
73        }
74    }
75
76    /// Create an unknown error
77    pub fn unknown(message: impl Into<String>) -> Self {
78        Self::Unknown {
79            message: message.into(),
80        }
81    }
82
83    /// Create an AppleScript error
84    pub fn applescript(message: impl Into<String>) -> Self {
85        Self::AppleScript {
86            message: message.into(),
87        }
88    }
89}
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94    use std::io;
95
96    #[test]
97    fn test_database_error_from_rusqlite() {
98        // Skip this test since rusqlite is not available in this build
99        // This test would verify rusqlite error conversion if the dependency was available
100    }
101
102    #[test]
103    fn test_serialization_error_from_serde() {
104        let json_error = serde_json::from_str::<serde_json::Value>("invalid json").unwrap_err();
105        let things_error: ThingsError = json_error.into();
106
107        match things_error {
108            ThingsError::Serialization(_) => (),
109            _ => panic!("Expected Serialization error"),
110        }
111    }
112
113    #[test]
114    fn test_io_error_from_std() {
115        let io_error = io::Error::new(io::ErrorKind::NotFound, "file not found");
116        let things_error: ThingsError = io_error.into();
117
118        match things_error {
119            ThingsError::Io(_) => (),
120            _ => panic!("Expected Io error"),
121        }
122    }
123
124    #[test]
125    fn test_database_not_found_error() {
126        let error = ThingsError::DatabaseNotFound {
127            path: "/path/to/db".to_string(),
128        };
129
130        assert!(error.to_string().contains("Database not found"));
131        assert!(error.to_string().contains("/path/to/db"));
132    }
133
134    #[test]
135    fn test_invalid_uuid_error() {
136        let error = ThingsError::InvalidUuid {
137            uuid: "invalid-uuid".to_string(),
138        };
139
140        assert!(error.to_string().contains("Invalid UUID"));
141        assert!(error.to_string().contains("invalid-uuid"));
142    }
143
144    #[test]
145    fn test_invalid_date_error() {
146        let error = ThingsError::InvalidDate {
147            date: "2023-13-45".to_string(),
148        };
149
150        assert!(error.to_string().contains("Invalid date"));
151        assert!(error.to_string().contains("2023-13-45"));
152    }
153
154    #[test]
155    fn test_task_not_found_error() {
156        let error = ThingsError::TaskNotFound {
157            uuid: "task-uuid-123".to_string(),
158        };
159
160        assert!(error.to_string().contains("Task not found"));
161        assert!(error.to_string().contains("task-uuid-123"));
162    }
163
164    #[test]
165    fn test_project_not_found_error() {
166        let error = ThingsError::ProjectNotFound {
167            uuid: "project-uuid-456".to_string(),
168        };
169
170        assert!(error.to_string().contains("Project not found"));
171        assert!(error.to_string().contains("project-uuid-456"));
172    }
173
174    #[test]
175    fn test_area_not_found_error() {
176        let error = ThingsError::AreaNotFound {
177            uuid: "area-uuid-789".to_string(),
178        };
179
180        assert!(error.to_string().contains("Area not found"));
181        assert!(error.to_string().contains("area-uuid-789"));
182    }
183
184    #[test]
185    fn test_validation_error() {
186        let error = ThingsError::Validation {
187            message: "Invalid input data".to_string(),
188        };
189
190        assert!(error.to_string().contains("Validation error"));
191        assert!(error.to_string().contains("Invalid input data"));
192    }
193
194    #[test]
195    fn test_configuration_error() {
196        let error = ThingsError::Configuration {
197            message: "Missing required config".to_string(),
198        };
199
200        assert!(error.to_string().contains("Configuration error"));
201        assert!(error.to_string().contains("Missing required config"));
202    }
203
204    #[test]
205    fn test_unknown_error() {
206        let error = ThingsError::Unknown {
207            message: "Something went wrong".to_string(),
208        };
209
210        assert!(error.to_string().contains("Unknown error"));
211        assert!(error.to_string().contains("Something went wrong"));
212    }
213
214    #[test]
215    fn test_applescript_error() {
216        let error = ThingsError::AppleScript {
217            message: "macOS Automation permission denied".to_string(),
218        };
219
220        assert!(error.to_string().contains("AppleScript automation failed"));
221        assert!(error
222            .to_string()
223            .contains("macOS Automation permission denied"));
224    }
225
226    #[test]
227    fn test_applescript_helper() {
228        let error = ThingsError::applescript("osascript not available");
229
230        match error {
231            ThingsError::AppleScript { message } => {
232                assert_eq!(message, "osascript not available");
233            }
234            _ => panic!("Expected AppleScript error"),
235        }
236    }
237
238    #[test]
239    fn test_validation_helper() {
240        let error = ThingsError::validation("Test validation message");
241
242        match error {
243            ThingsError::Validation { message } => {
244                assert_eq!(message, "Test validation message");
245            }
246            _ => panic!("Expected Validation error"),
247        }
248    }
249
250    #[test]
251    fn test_validation_helper_with_string() {
252        let message = "Test validation message".to_string();
253        let error = ThingsError::validation(message);
254
255        match error {
256            ThingsError::Validation { message } => {
257                assert_eq!(message, "Test validation message");
258            }
259            _ => panic!("Expected Validation error"),
260        }
261    }
262
263    #[test]
264    fn test_configuration_helper() {
265        let error = ThingsError::configuration("Test config message");
266
267        match error {
268            ThingsError::Configuration { message } => {
269                assert_eq!(message, "Test config message");
270            }
271            _ => panic!("Expected Configuration error"),
272        }
273    }
274
275    #[test]
276    fn test_configuration_helper_with_string() {
277        let message = "Test config message".to_string();
278        let error = ThingsError::configuration(message);
279
280        match error {
281            ThingsError::Configuration { message } => {
282                assert_eq!(message, "Test config message");
283            }
284            _ => panic!("Expected Configuration error"),
285        }
286    }
287
288    #[test]
289    fn test_unknown_helper() {
290        let error = ThingsError::unknown("Test unknown message");
291
292        match error {
293            ThingsError::Unknown { message } => {
294                assert_eq!(message, "Test unknown message");
295            }
296            _ => panic!("Expected Unknown error"),
297        }
298    }
299
300    #[test]
301    fn test_unknown_helper_with_string() {
302        let message = "Test unknown message".to_string();
303        let error = ThingsError::unknown(message);
304
305        match error {
306            ThingsError::Unknown { message } => {
307                assert_eq!(message, "Test unknown message");
308            }
309            _ => panic!("Expected Unknown error"),
310        }
311    }
312
313    #[test]
314    fn test_error_display_formatting() {
315        let errors = vec![
316            ThingsError::DatabaseNotFound {
317                path: "test.db".to_string(),
318            },
319            ThingsError::InvalidUuid {
320                uuid: "bad-uuid".to_string(),
321            },
322            ThingsError::InvalidDate {
323                date: "bad-date".to_string(),
324            },
325            ThingsError::TaskNotFound {
326                uuid: "task-123".to_string(),
327            },
328            ThingsError::ProjectNotFound {
329                uuid: "project-456".to_string(),
330            },
331            ThingsError::AreaNotFound {
332                uuid: "area-789".to_string(),
333            },
334            ThingsError::Validation {
335                message: "validation failed".to_string(),
336            },
337            ThingsError::Configuration {
338                message: "config error".to_string(),
339            },
340            ThingsError::Unknown {
341                message: "unknown error".to_string(),
342            },
343        ];
344
345        for error in errors {
346            let error_string = error.to_string();
347            assert!(!error_string.is_empty());
348            assert!(error_string.len() > 10); // Should have meaningful content
349        }
350    }
351
352    #[test]
353    fn test_error_debug_formatting() {
354        let error = ThingsError::Validation {
355            message: "test message".to_string(),
356        };
357
358        let debug_string = format!("{error:?}");
359        assert!(debug_string.contains("Validation"));
360        assert!(debug_string.contains("test message"));
361    }
362
363    #[test]
364    fn test_result_type_alias() {
365        // Test that the Result type alias works correctly
366        fn returns_result() -> String {
367            "success".to_string()
368        }
369
370        fn returns_error() -> Result<String> {
371            Err(ThingsError::validation("test error"))
372        }
373
374        assert_eq!(returns_result(), "success");
375        assert!(returns_error().is_err());
376
377        match returns_error() {
378            Err(ThingsError::Validation { message }) => {
379                assert_eq!(message, "test error");
380            }
381            _ => panic!("Expected Validation error"),
382        }
383    }
384}