1use thiserror::Error;
4
5pub type Result<T> = std::result::Result<T, ThingsError>;
7
8#[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 pub fn validation(message: impl Into<String>) -> Self {
64 Self::Validation {
65 message: message.into(),
66 }
67 }
68
69 pub fn configuration(message: impl Into<String>) -> Self {
71 Self::Configuration {
72 message: message.into(),
73 }
74 }
75
76 pub fn unknown(message: impl Into<String>) -> Self {
78 Self::Unknown {
79 message: message.into(),
80 }
81 }
82
83 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 }
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); }
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 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}