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