1use std::fmt;
4use std::path::PathBuf;
5use std::time::Duration;
6
7pub type Result<T> = std::result::Result<T, Error>;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
12pub enum ErrorSeverity {
13 Info,
15 Warning,
17 Error,
19 Critical,
21}
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum RecoveryStrategy {
26 Retry,
28 Fallback,
30 Degrade,
32 UserIntervention,
34 Fatal,
36}
37
38#[derive(Debug, Clone)]
40pub struct ErrorContext {
41 pub request_id: Option<String>,
43 pub operation: Option<String>,
45 pub metadata: std::collections::HashMap<String, serde_json::Value>,
47}
48
49impl ErrorContext {
50 pub fn new() -> Self {
52 Self {
53 request_id: None,
54 operation: None,
55 metadata: std::collections::HashMap::new(),
56 }
57 }
58
59 pub fn with_request_id(mut self, request_id: String) -> Self {
61 self.request_id = Some(request_id);
62 self
63 }
64
65 pub fn with_operation(mut self, operation: String) -> Self {
67 self.operation = Some(operation);
68 self
69 }
70
71 pub fn with_metadata(mut self, key: String, value: serde_json::Value) -> Self {
73 self.metadata.insert(key, value);
74 self
75 }
76}
77
78impl Default for ErrorContext {
79 fn default() -> Self {
80 Self::new()
81 }
82}
83
84#[derive(Debug)]
86pub enum Error {
87 Io(std::io::Error),
89
90 Json(serde_json::Error),
92
93 Config {
95 key: String,
97 message: String,
99 context: Option<Box<ErrorContext>>,
101 },
102
103 Parse {
105 file: PathBuf,
107 message: String,
109 line: Option<usize>,
111 context: Option<Box<ErrorContext>>,
113 },
114
115 Network {
117 message: String,
119 context: Option<Box<ErrorContext>>,
121 },
122
123 Database {
125 message: String,
127 context: Option<Box<ErrorContext>>,
129 },
130
131 ResourceExhausted {
133 resource: String,
135 message: String,
137 context: Option<Box<ErrorContext>>,
139 },
140
141 Timeout {
143 operation: String,
145 timeout: Duration,
147 context: Option<Box<ErrorContext>>,
149 },
150
151 Cancelled {
153 operation: String,
155 context: Option<Box<ErrorContext>>,
157 },
158
159 Permission {
161 resource: String,
163 message: String,
165 context: Option<Box<ErrorContext>>,
167 },
168
169 Validation {
171 field: String,
173 message: String,
175 context: Option<Box<ErrorContext>>,
177 },
178
179 Generic {
181 message: String,
183 severity: ErrorSeverity,
185 recovery_strategy: RecoveryStrategy,
187 context: Option<Box<ErrorContext>>,
189 },
190}
191
192impl Clone for Error {
193 fn clone(&self) -> Self {
194 match self {
195 Self::Io(e) => {
196 Self::Io(std::io::Error::other(e.to_string()))
198 }
199 Self::Json(e) => {
200 Self::Json(serde_json::Error::io(std::io::Error::other(e.to_string())))
202 }
203 Self::Config {
204 key,
205 message,
206 context,
207 } => Self::Config {
208 key: key.clone(),
209 message: message.clone(),
210 context: context.clone(),
211 },
212 Self::Parse {
213 file,
214 message,
215 line,
216 context,
217 } => Self::Parse {
218 file: file.clone(),
219 message: message.clone(),
220 line: *line,
221 context: context.clone(),
222 },
223 Self::Network { message, context } => Self::Network {
224 message: message.clone(),
225 context: context.clone(),
226 },
227 Self::Database { message, context } => Self::Database {
228 message: message.clone(),
229 context: context.clone(),
230 },
231 Self::ResourceExhausted {
232 resource,
233 message,
234 context,
235 } => Self::ResourceExhausted {
236 resource: resource.clone(),
237 message: message.clone(),
238 context: context.clone(),
239 },
240 Self::Timeout {
241 operation,
242 timeout,
243 context,
244 } => Self::Timeout {
245 operation: operation.clone(),
246 timeout: *timeout,
247 context: context.clone(),
248 },
249 Self::Cancelled { operation, context } => Self::Cancelled {
250 operation: operation.clone(),
251 context: context.clone(),
252 },
253 Self::Permission {
254 resource,
255 message,
256 context,
257 } => Self::Permission {
258 resource: resource.clone(),
259 message: message.clone(),
260 context: context.clone(),
261 },
262 Self::Validation {
263 field,
264 message,
265 context,
266 } => Self::Validation {
267 field: field.clone(),
268 message: message.clone(),
269 context: context.clone(),
270 },
271 Self::Generic {
272 message,
273 severity,
274 recovery_strategy,
275 context,
276 } => Self::Generic {
277 message: message.clone(),
278 severity: *severity,
279 recovery_strategy: *recovery_strategy,
280 context: context.clone(),
281 },
282 }
283 }
284}
285
286impl Error {
287 pub fn get_error_code(&self) -> &'static str {
289 match self {
290 Self::Io(_) => "ERR_IO",
291 Self::Json(_) => "ERR_JSON",
292 Self::Config { .. } => "ERR_CONFIG",
293 Self::Parse { .. } => "ERR_PARSE",
294 Self::Network { .. } => "ERR_NETWORK",
295 Self::Database { .. } => "ERR_DATABASE",
296 Self::ResourceExhausted { .. } => "ERR_RESOURCE_EXHAUSTED",
297 Self::Timeout { .. } => "ERR_TIMEOUT",
298 Self::Cancelled { .. } => "ERR_CANCELLED",
299 Self::Permission { .. } => "ERR_PERMISSION",
300 Self::Validation { .. } => "ERR_VALIDATION",
301 Self::Generic { .. } => "ERR_GENERIC",
302 }
303 }
304
305 pub fn get_severity(&self) -> ErrorSeverity {
307 match self {
308 Self::Io(_) => ErrorSeverity::Error,
309 Self::Json(_) => ErrorSeverity::Error,
310 Self::Config { .. } => ErrorSeverity::Error,
311 Self::Parse { .. } => ErrorSeverity::Error,
312 Self::Network { .. } => ErrorSeverity::Warning,
313 Self::Database { .. } => ErrorSeverity::Critical,
314 Self::ResourceExhausted { .. } => ErrorSeverity::Critical,
315 Self::Timeout { .. } => ErrorSeverity::Warning,
316 Self::Cancelled { .. } => ErrorSeverity::Info,
317 Self::Permission { .. } => ErrorSeverity::Error,
318 Self::Validation { .. } => ErrorSeverity::Warning,
319 Self::Generic { severity, .. } => *severity,
320 }
321 }
322
323 pub fn get_recovery_strategy(&self) -> RecoveryStrategy {
325 match self {
326 Self::Io(_) => RecoveryStrategy::Retry,
327 Self::Json(_) => RecoveryStrategy::UserIntervention,
328 Self::Config { .. } => RecoveryStrategy::UserIntervention,
329 Self::Parse { .. } => RecoveryStrategy::UserIntervention,
330 Self::Network { .. } => RecoveryStrategy::Retry,
331 Self::Database { .. } => RecoveryStrategy::Retry,
332 Self::ResourceExhausted { .. } => RecoveryStrategy::Degrade,
333 Self::Timeout { .. } => RecoveryStrategy::Retry,
334 Self::Cancelled { .. } => RecoveryStrategy::UserIntervention,
335 Self::Permission { .. } => RecoveryStrategy::UserIntervention,
336 Self::Validation { .. } => RecoveryStrategy::UserIntervention,
337 Self::Generic {
338 recovery_strategy, ..
339 } => *recovery_strategy,
340 }
341 }
342
343 pub fn is_recoverable(&self) -> bool {
345 !matches!(self.get_recovery_strategy(), RecoveryStrategy::Fatal)
346 }
347
348 pub fn get_context(&self) -> Option<&ErrorContext> {
350 match self {
351 Self::Config { context, .. } => context.as_deref(),
352 Self::Parse { context, .. } => context.as_deref(),
353 Self::Network { context, .. } => context.as_deref(),
354 Self::Database { context, .. } => context.as_deref(),
355 Self::ResourceExhausted { context, .. } => context.as_deref(),
356 Self::Timeout { context, .. } => context.as_deref(),
357 Self::Cancelled { context, .. } => context.as_deref(),
358 Self::Permission { context, .. } => context.as_deref(),
359 Self::Validation { context, .. } => context.as_deref(),
360 Self::Generic { context, .. } => context.as_deref(),
361 _ => None,
362 }
363 }
364
365 pub fn parse_with_context(
367 file: impl Into<PathBuf>,
368 message: impl Into<String>,
369 context: ErrorContext,
370 ) -> Self {
371 Self::Parse {
372 file: file.into(),
373 message: message.into(),
374 line: None,
375 context: Some(Box::new(context)),
376 }
377 }
378
379 pub fn parse(file: impl Into<PathBuf>, message: impl Into<String>) -> Self {
381 Self::Parse {
382 file: file.into(),
383 message: message.into(),
384 line: None,
385 context: None,
386 }
387 }
388
389 pub fn config(key: impl Into<String>, message: impl Into<String>) -> Self {
391 Self::Config {
392 key: key.into(),
393 message: message.into(),
394 context: None,
395 }
396 }
397
398 pub fn permission(resource: impl Into<String>, message: impl Into<String>) -> Self {
400 Self::Permission {
401 resource: resource.into(),
402 message: message.into(),
403 context: None,
404 }
405 }
406
407 pub fn timeout(operation: impl Into<String>, timeout: Duration) -> Self {
409 Self::Timeout {
410 operation: operation.into(),
411 timeout,
412 context: None,
413 }
414 }
415
416 pub fn network(message: impl Into<String>) -> Self {
418 Self::Network {
419 message: message.into(),
420 context: None,
421 }
422 }
423
424 pub fn validation(field: impl Into<String>, message: impl Into<String>) -> Self {
426 Self::Validation {
427 field: field.into(),
428 message: message.into(),
429 context: None,
430 }
431 }
432
433 pub fn generic(
435 message: impl Into<String>,
436 severity: ErrorSeverity,
437 recovery_strategy: RecoveryStrategy,
438 ) -> Self {
439 Self::Generic {
440 message: message.into(),
441 severity,
442 recovery_strategy,
443 context: None,
444 }
445 }
446
447 pub fn resource_exhausted(resource: impl Into<String>, message: impl Into<String>) -> Self {
449 Self::ResourceExhausted {
450 resource: resource.into(),
451 message: message.into(),
452 context: None,
453 }
454 }
455
456 pub fn cancelled(operation: impl Into<String>) -> Self {
458 Self::Cancelled {
459 operation: operation.into(),
460 context: None,
461 }
462 }
463
464 pub fn database(message: impl Into<String>) -> Self {
466 Self::Database {
467 message: message.into(),
468 context: None,
469 }
470 }
471
472 pub fn io(message: impl Into<String>) -> Self {
474 Self::Io(std::io::Error::other(message.into()))
475 }
476
477 pub fn storage(message: impl Into<String>) -> Self {
479 Self::Database {
480 message: format!("Storage: {}", message.into()),
481 context: None,
482 }
483 }
484
485 pub fn watcher(message: impl Into<String>) -> Self {
487 Self::Generic {
488 message: format!("File watcher: {}", message.into()),
489 severity: ErrorSeverity::Warning,
490 recovery_strategy: RecoveryStrategy::Retry,
491 context: None,
492 }
493 }
494
495 pub fn indexing(message: impl Into<String>) -> Self {
497 Self::Generic {
498 message: format!("Indexing: {}", message.into()),
499 severity: ErrorSeverity::Warning,
500 recovery_strategy: RecoveryStrategy::Fallback,
501 context: None,
502 }
503 }
504
505 pub fn unsupported_language(language: impl Into<String>) -> Self {
507 Self::Validation {
508 field: "language".to_string(),
509 message: format!("Unsupported language: {}", language.into()),
510 context: None,
511 }
512 }
513
514 pub fn node_not_found(node_id: impl Into<String>) -> Self {
516 Self::Validation {
517 field: "node_id".to_string(),
518 message: format!("Node not found: {}", node_id.into()),
519 context: None,
520 }
521 }
522
523 pub fn other(message: impl Into<String>) -> Self {
525 Self::Generic {
526 message: message.into(),
527 severity: ErrorSeverity::Error,
528 recovery_strategy: RecoveryStrategy::Fatal,
529 context: None,
530 }
531 }
532
533 pub fn should_retry(&self) -> bool {
535 matches!(self.get_recovery_strategy(), RecoveryStrategy::Retry)
536 }
537
538 pub fn severity(&self) -> ErrorSeverity {
540 self.get_severity()
541 }
542
543 pub fn recovery_strategy(&self) -> RecoveryStrategy {
545 self.get_recovery_strategy()
546 }
547
548 pub fn error_code(&self) -> i32 {
550 match self {
551 Self::Io(_) => -32000,
552 Self::Json(_) => -32005,
553 Self::Config { .. } => -32006,
554 Self::Parse { .. } => -32001,
555 Self::Network { .. } => -32015,
556 Self::Database { .. } => -32004,
557 Self::ResourceExhausted { .. } => -32013,
558 Self::Timeout { .. } => -32012,
559 Self::Cancelled { .. } => -32014,
560 Self::Permission { .. } => -32016,
561 Self::Validation { .. } => -32017,
562 Self::Generic { .. } => -32603, }
564 }
565}
566
567impl fmt::Display for Error {
568 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
569 match self {
570 Self::Io(e) => write!(f, "IO error: {e}"),
571 Self::Json(e) => write!(f, "JSON error: {e}"),
572 Self::Config { key, message, .. } => {
573 write!(f, "Config error in '{key}': {message}")
574 }
575 Self::Parse {
576 file,
577 message,
578 line,
579 ..
580 } => {
581 if let Some(line) = line {
582 write!(f, "Parse error in {}:{}: {}", file.display(), line, message)
583 } else {
584 write!(f, "Parse error in {}: {}", file.display(), message)
585 }
586 }
587 Self::Network { message, .. } => write!(f, "Network error: {message}"),
588 Self::Database { message, .. } => write!(f, "Database error: {message}"),
589 Self::ResourceExhausted {
590 resource, message, ..
591 } => {
592 write!(f, "Resource exhausted ({resource}): {message}")
593 }
594 Self::Timeout {
595 operation, timeout, ..
596 } => {
597 write!(f, "Timeout in '{operation}' after {timeout:?}")
598 }
599 Self::Cancelled { operation, .. } => {
600 write!(f, "Operation '{operation}' was cancelled")
601 }
602 Self::Permission {
603 resource, message, ..
604 } => {
605 write!(f, "Permission denied for '{resource}': {message}")
606 }
607 Self::Validation { field, message, .. } => {
608 write!(f, "Validation error in '{field}': {message}")
609 }
610 Self::Generic { message, .. } => write!(f, "{message}"),
611 }
612 }
613}
614
615impl std::error::Error for Error {
616 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
617 match self {
618 Self::Io(e) => Some(e),
619 Self::Json(e) => Some(e),
620 _ => None,
621 }
622 }
623}
624
625#[cfg(test)]
626mod tests {
627 use super::*;
628
629 #[test]
630 fn test_error_severity() {
631 let error = Error::parse("test.rs", "test error");
632 assert_eq!(error.get_severity(), ErrorSeverity::Error);
633
634 let error = Error::config("test_key", "test config error");
635 assert_eq!(error.get_severity(), ErrorSeverity::Error);
636 }
637
638 #[test]
639 fn test_recovery_strategy() {
640 let error = Error::database("test database error");
641 assert_eq!(error.get_recovery_strategy(), RecoveryStrategy::Retry);
642 assert!(error.is_recoverable());
643
644 let error = Error::parse("test.rs", "test error");
645 assert_eq!(
646 error.get_recovery_strategy(),
647 RecoveryStrategy::UserIntervention
648 );
649 assert!(error.is_recoverable());
650 }
651
652 #[test]
653 fn test_parse_error() {
654 let error = Error::parse("test.rs", "syntax error");
655 assert_eq!(error.get_error_code(), "ERR_PARSE");
656 assert!(error.is_recoverable());
657 }
658
659 #[test]
660 fn test_error_codes() {
661 let error = Error::parse("test.rs", "test error");
662 assert_eq!(error.get_error_code(), "ERR_PARSE");
663
664 let error = Error::timeout("test_operation", std::time::Duration::from_secs(30));
665 assert_eq!(error.get_error_code(), "ERR_TIMEOUT");
666 }
667
668 #[test]
669 fn test_error_context() {
670 let context = ErrorContext::new()
671 .with_request_id("req-123".to_string())
672 .with_operation("test-op".to_string())
673 .with_metadata(
674 "file_size".to_string(),
675 serde_json::Value::Number(1024.into()),
676 );
677
678 let error = Error::parse_with_context("test.rs", "test error", context);
679 assert!(
680 error.get_context().is_some(),
681 "Error should have context when created with context"
682 );
683
684 let error_context = error.get_context().unwrap();
685 assert_eq!(
686 error_context.request_id,
687 Some("req-123".to_string()),
688 "Context should preserve request_id"
689 );
690 assert_eq!(
691 error_context.operation,
692 Some("test-op".to_string()),
693 "Context should preserve operation"
694 );
695 }
696
697 #[test]
698 fn test_error_recoverability() {
699 let recoverable_error = Error::network("connection failed");
700 assert!(recoverable_error.is_recoverable());
701 assert_eq!(
702 recoverable_error.get_recovery_strategy(),
703 RecoveryStrategy::Retry
704 );
705
706 let validation_error = Error::validation("field", "invalid value");
707 assert!(validation_error.is_recoverable());
708 assert_eq!(
709 validation_error.get_recovery_strategy(),
710 RecoveryStrategy::UserIntervention
711 );
712 }
713}