1use std::path::PathBuf;
14use thiserror::Error;
15
16#[derive(Debug, Error)]
18pub enum EnhancedError {
19 #[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 #[error("File not found: {path}\n Suggestion: {suggestion}")]
30 FileNotFound { path: PathBuf, suggestion: String },
31
32 #[error("Parse error at {location}: {message}\n Context: {context}")]
34 Parse {
35 location: String,
36 message: String,
37 context: String,
38 },
39
40 #[error("Validation error: {message}\n Field: {field}\n Suggestion: {suggestion}")]
42 Validation {
43 field: String,
44 message: String,
45 suggestion: String,
46 },
47
48 #[error("IO error for {path}: {message}")]
50 Io {
51 path: PathBuf,
52 message: String,
53 #[source]
54 source: std::io::Error,
55 },
56
57 #[error("Template error in '{template}': {message}\n Suggestion: {suggestion}")]
59 Template {
60 template: String,
61 message: String,
62 suggestion: String,
63 },
64
65 #[error("Sync error for {editor}: {message}\n Suggestion: {suggestion}")]
67 Sync {
68 editor: String,
69 message: String,
70 suggestion: String,
71 },
72
73 #[error("Security error: {message}\n Action required: {action}")]
75 Security { message: String, action: String },
76
77 #[error("Binary format error: {message}\n Expected: {expected}, Got: {actual}")]
79 BinaryFormat {
80 message: String,
81 expected: String,
82 actual: String,
83 },
84
85 #[error("Hook error for '{hook_id}': {message}")]
87 Hook { hook_id: String, message: String },
88
89 #[error("DCP protocol error: {message}\n Code: {code}")]
91 Protocol { code: i32, message: String },
92}
93
94impl EnhancedError {
95 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 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 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 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 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 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 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 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 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 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 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 pub fn protocol(code: i32, message: impl Into<String>) -> Self {
218 Self::Protocol {
219 code,
220 message: message.into(),
221 }
222 }
223
224 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 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
253pub type EnhancedResult<T> = std::result::Result<T, EnhancedError>;
255
256pub trait ErrorContext<T> {
258 fn with_context(self, context: impl FnOnce() -> String) -> EnhancedResult<T>;
260
261 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 proptest! {
379 #[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 #[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 prop_assert_eq!(err1.error_code(), err2.error_code());
402 }
403
404 #[test]
406 fn prop_recoverable_errors(
407 message in ".{1,100}",
408 ) {
409 let config_err = EnhancedError::config(&message, "fix it");
411 prop_assert!(config_err.is_recoverable());
412
413 let security_err = EnhancedError::security(&message, "action");
415 prop_assert!(!security_err.is_recoverable());
416 }
417
418 #[test]
420 fn prop_error_display_no_panic(
421 message in ".*",
422 path in "[a-zA-Z0-9/._-]*",
423 ) {
424 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}