Skip to main content

driven/
error.rs

1//! Comprehensive Error Handling Module
2//!
3//! This module provides enhanced error handling with actionable error messages
4//! and proper error propagation across the driven crate.
5//!
6//! ## Features
7//!
8//! - Structured error types with context
9//! - Actionable error messages with suggestions
10//! - Error chaining for debugging
11//! - No panics in library code
12
13use std::path::PathBuf;
14use thiserror::Error;
15
16/// Enhanced error type with actionable messages
17#[derive(Debug, Error)]
18pub enum EnhancedError {
19    /// Configuration error with suggestion
20    #[error("Configuration error: {message}\n  Suggestion: {suggestion}")]
21    Config {
22        message: String,
23        suggestion: String,
24        #[source]
25        source: Option<Box<dyn std::error::Error + Send + Sync>>,
26    },
27
28    /// File not found with path context
29    #[error("File not found: {path}\n  Suggestion: {suggestion}")]
30    FileNotFound { path: PathBuf, suggestion: String },
31
32    /// Parse error with location
33    #[error("Parse error at {location}: {message}\n  Context: {context}")]
34    Parse {
35        location: String,
36        message: String,
37        context: String,
38    },
39
40    /// Validation error with details
41    #[error("Validation error: {message}\n  Field: {field}\n  Suggestion: {suggestion}")]
42    Validation {
43        field: String,
44        message: String,
45        suggestion: String,
46    },
47
48    /// IO error with path context
49    #[error("IO error for {path}: {message}")]
50    Io {
51        path: PathBuf,
52        message: String,
53        #[source]
54        source: std::io::Error,
55    },
56
57    /// Template error with template name
58    #[error("Template error in '{template}': {message}\n  Suggestion: {suggestion}")]
59    Template {
60        template: String,
61        message: String,
62        suggestion: String,
63    },
64
65    /// Sync error with editor context
66    #[error("Sync error for {editor}: {message}\n  Suggestion: {suggestion}")]
67    Sync {
68        editor: String,
69        message: String,
70        suggestion: String,
71    },
72
73    /// Security error
74    #[error("Security error: {message}\n  Action required: {action}")]
75    Security { message: String, action: String },
76
77    /// Binary format error
78    #[error("Binary format error: {message}\n  Expected: {expected}, Got: {actual}")]
79    BinaryFormat {
80        message: String,
81        expected: String,
82        actual: String,
83    },
84
85    /// Hook error
86    #[error("Hook error for '{hook_id}': {message}")]
87    Hook { hook_id: String, message: String },
88
89    /// DCP protocol error
90    #[error("DCP protocol error: {message}\n  Code: {code}")]
91    Protocol { code: i32, message: String },
92}
93
94impl EnhancedError {
95    /// Create a configuration error
96    pub fn config(message: impl Into<String>, suggestion: impl Into<String>) -> Self {
97        Self::Config {
98            message: message.into(),
99            suggestion: suggestion.into(),
100            source: None,
101        }
102    }
103
104    /// Create a configuration error with source
105    pub fn config_with_source(
106        message: impl Into<String>,
107        suggestion: impl Into<String>,
108        source: impl std::error::Error + Send + Sync + 'static,
109    ) -> Self {
110        Self::Config {
111            message: message.into(),
112            suggestion: suggestion.into(),
113            source: Some(Box::new(source)),
114        }
115    }
116
117    /// Create a file not found error
118    pub fn file_not_found(path: impl Into<PathBuf>, suggestion: impl Into<String>) -> Self {
119        Self::FileNotFound {
120            path: path.into(),
121            suggestion: suggestion.into(),
122        }
123    }
124
125    /// Create a parse error
126    pub fn parse(
127        location: impl Into<String>,
128        message: impl Into<String>,
129        context: impl Into<String>,
130    ) -> Self {
131        Self::Parse {
132            location: location.into(),
133            message: message.into(),
134            context: context.into(),
135        }
136    }
137
138    /// Create a validation error
139    pub fn validation(
140        field: impl Into<String>,
141        message: impl Into<String>,
142        suggestion: impl Into<String>,
143    ) -> Self {
144        Self::Validation {
145            field: field.into(),
146            message: message.into(),
147            suggestion: suggestion.into(),
148        }
149    }
150
151    /// Create an IO error
152    pub fn io(path: impl Into<PathBuf>, source: std::io::Error) -> Self {
153        let message = source.to_string();
154        Self::Io {
155            path: path.into(),
156            message,
157            source,
158        }
159    }
160
161    /// Create a template error
162    pub fn template(
163        template: impl Into<String>,
164        message: impl Into<String>,
165        suggestion: impl Into<String>,
166    ) -> Self {
167        Self::Template {
168            template: template.into(),
169            message: message.into(),
170            suggestion: suggestion.into(),
171        }
172    }
173
174    /// Create a sync error
175    pub fn sync(
176        editor: impl Into<String>,
177        message: impl Into<String>,
178        suggestion: impl Into<String>,
179    ) -> Self {
180        Self::Sync {
181            editor: editor.into(),
182            message: message.into(),
183            suggestion: suggestion.into(),
184        }
185    }
186
187    /// Create a security error
188    pub fn security(message: impl Into<String>, action: impl Into<String>) -> Self {
189        Self::Security {
190            message: message.into(),
191            action: action.into(),
192        }
193    }
194
195    /// Create a binary format error
196    pub fn binary_format(
197        message: impl Into<String>,
198        expected: impl Into<String>,
199        actual: impl Into<String>,
200    ) -> Self {
201        Self::BinaryFormat {
202            message: message.into(),
203            expected: expected.into(),
204            actual: actual.into(),
205        }
206    }
207
208    /// Create a hook error
209    pub fn hook(hook_id: impl Into<String>, message: impl Into<String>) -> Self {
210        Self::Hook {
211            hook_id: hook_id.into(),
212            message: message.into(),
213        }
214    }
215
216    /// Create a protocol error
217    pub fn protocol(code: i32, message: impl Into<String>) -> Self {
218        Self::Protocol {
219            code,
220            message: message.into(),
221        }
222    }
223
224    /// Check if this is a recoverable error
225    pub fn is_recoverable(&self) -> bool {
226        matches!(
227            self,
228            Self::Config { .. }
229                | Self::Validation { .. }
230                | Self::Template { .. }
231                | Self::Sync { .. }
232        )
233    }
234
235    /// Get the error code for this error type
236    pub fn error_code(&self) -> &'static str {
237        match self {
238            Self::Config { .. } => "E001",
239            Self::FileNotFound { .. } => "E002",
240            Self::Parse { .. } => "E003",
241            Self::Validation { .. } => "E004",
242            Self::Io { .. } => "E005",
243            Self::Template { .. } => "E006",
244            Self::Sync { .. } => "E007",
245            Self::Security { .. } => "E008",
246            Self::BinaryFormat { .. } => "E009",
247            Self::Hook { .. } => "E010",
248            Self::Protocol { .. } => "E011",
249        }
250    }
251}
252
253/// Result type alias for enhanced errors
254pub type EnhancedResult<T> = std::result::Result<T, EnhancedError>;
255
256/// Extension trait for adding context to errors
257pub trait ErrorContext<T> {
258    /// Add context to an error
259    fn with_context(self, context: impl FnOnce() -> String) -> EnhancedResult<T>;
260
261    /// Add a suggestion to an error
262    fn with_suggestion(self, suggestion: impl Into<String>) -> EnhancedResult<T>;
263}
264
265impl<T, E: std::error::Error + Send + Sync + 'static> ErrorContext<T> for Result<T, E> {
266    fn with_context(self, context: impl FnOnce() -> String) -> EnhancedResult<T> {
267        self.map_err(|e| {
268            EnhancedError::config_with_source(
269                context(),
270                "Check the error details for more information",
271                e,
272            )
273        })
274    }
275
276    fn with_suggestion(self, suggestion: impl Into<String>) -> EnhancedResult<T> {
277        self.map_err(|e| EnhancedError::config_with_source(e.to_string(), suggestion, e))
278    }
279}
280
281#[cfg(test)]
282mod tests {
283    use super::*;
284
285    #[test]
286    fn test_config_error() {
287        let err = EnhancedError::config(
288            "Invalid editor configuration",
289            "Check that the editor name is one of: cursor, copilot, windsurf",
290        );
291
292        assert!(err.to_string().contains("Invalid editor configuration"));
293        assert!(err.to_string().contains("Suggestion:"));
294        assert_eq!(err.error_code(), "E001");
295        assert!(err.is_recoverable());
296    }
297
298    #[test]
299    fn test_file_not_found_error() {
300        let err = EnhancedError::file_not_found(
301            "/path/to/missing.drv",
302            "Run 'dx driven init' to create the configuration",
303        );
304
305        assert!(err.to_string().contains("missing.drv"));
306        assert_eq!(err.error_code(), "E002");
307    }
308
309    #[test]
310    fn test_parse_error() {
311        let err = EnhancedError::parse(
312            "line 42, column 10",
313            "Unexpected token",
314            "Expected ':' after key name",
315        );
316
317        assert!(err.to_string().contains("line 42"));
318        assert_eq!(err.error_code(), "E003");
319    }
320
321    #[test]
322    fn test_validation_error() {
323        let err = EnhancedError::validation(
324            "sync.debounce_ms",
325            "Value must be positive",
326            "Use a value between 100 and 5000",
327        );
328
329        assert!(err.to_string().contains("debounce_ms"));
330        assert_eq!(err.error_code(), "E004");
331        assert!(err.is_recoverable());
332    }
333
334    #[test]
335    fn test_security_error() {
336        let err = EnhancedError::security("Invalid signature", "Re-sign the file with a valid key");
337
338        assert!(err.to_string().contains("Invalid signature"));
339        assert_eq!(err.error_code(), "E008");
340        assert!(!err.is_recoverable());
341    }
342
343    #[test]
344    fn test_error_codes_unique() {
345        let errors = vec![
346            EnhancedError::config("", ""),
347            EnhancedError::file_not_found("", ""),
348            EnhancedError::parse("", "", ""),
349            EnhancedError::validation("", "", ""),
350            EnhancedError::template("", "", ""),
351            EnhancedError::sync("", "", ""),
352            EnhancedError::security("", ""),
353            EnhancedError::binary_format("", "", ""),
354            EnhancedError::hook("", ""),
355            EnhancedError::protocol(0, ""),
356        ];
357
358        let codes: Vec<_> = errors.iter().map(|e| e.error_code()).collect();
359        let unique_codes: std::collections::HashSet<_> = codes.iter().collect();
360
361        assert_eq!(
362            codes.len(),
363            unique_codes.len(),
364            "Error codes should be unique"
365        );
366    }
367}
368
369#[cfg(test)]
370mod property_tests {
371    use super::*;
372    use proptest::prelude::*;
373
374    // Property 15: Error Handling Consistency
375    // For any error condition, the error SHALL be properly propagated with
376    // a descriptive message and SHALL not cause panics in library code.
377
378    proptest! {
379        /// Property: All errors have non-empty messages
380        #[test]
381        fn prop_errors_have_messages(
382            message in ".+",
383            suggestion in ".+",
384        ) {
385            let err = EnhancedError::config(&message, &suggestion);
386            let display = err.to_string();
387
388            prop_assert!(!display.is_empty());
389            prop_assert!(display.contains(&message) || display.contains("Configuration"));
390        }
391
392        /// Property: Error codes are consistent
393        #[test]
394        fn prop_error_codes_consistent(
395            message in ".{1,100}",
396        ) {
397            let err1 = EnhancedError::config(&message, "suggestion");
398            let err2 = EnhancedError::config("different", "suggestion");
399
400            // Same error type should have same code
401            prop_assert_eq!(err1.error_code(), err2.error_code());
402        }
403
404        /// Property: Recoverable errors are correctly identified
405        #[test]
406        fn prop_recoverable_errors(
407            message in ".{1,100}",
408        ) {
409            // Config errors should be recoverable
410            let config_err = EnhancedError::config(&message, "fix it");
411            prop_assert!(config_err.is_recoverable());
412
413            // Security errors should not be recoverable
414            let security_err = EnhancedError::security(&message, "action");
415            prop_assert!(!security_err.is_recoverable());
416        }
417
418        /// Property: Error display never panics
419        #[test]
420        fn prop_error_display_no_panic(
421            message in ".*",
422            path in "[a-zA-Z0-9/._-]*",
423        ) {
424            // These should never panic
425            let _ = EnhancedError::config(&message, "suggestion").to_string();
426            let _ = EnhancedError::file_not_found(&path, "suggestion").to_string();
427            let _ = EnhancedError::parse("loc", &message, "ctx").to_string();
428            let _ = EnhancedError::validation("field", &message, "suggestion").to_string();
429            let _ = EnhancedError::template("tmpl", &message, "suggestion").to_string();
430            let _ = EnhancedError::sync("editor", &message, "suggestion").to_string();
431            let _ = EnhancedError::security(&message, "action").to_string();
432            let _ = EnhancedError::binary_format(&message, "exp", "act").to_string();
433            let _ = EnhancedError::hook("hook", &message).to_string();
434            let _ = EnhancedError::protocol(0, &message).to_string();
435        }
436    }
437}