codeprism_core/
error.rs

1//! Error handling for the codeprism library
2
3use std::fmt;
4use std::path::PathBuf;
5use std::time::Duration;
6
7/// Result type alias for codeprism operations
8pub type Result<T> = std::result::Result<T, Error>;
9
10/// Error severity levels for classification and handling
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
12pub enum ErrorSeverity {
13    /// Low severity - informational, can continue normally
14    Info,
15    /// Medium severity - warning, might affect functionality but not critical
16    Warning,
17    /// High severity - error that affects functionality but system can continue
18    Error,
19    /// Critical severity - system stability at risk, immediate attention needed
20    Critical,
21}
22
23/// Error recovery strategy for different error types
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum RecoveryStrategy {
26    /// Error is recoverable with retry
27    Retry,
28    /// Error requires fallback mechanism
29    Fallback,
30    /// Error requires graceful degradation
31    Degrade,
32    /// Error requires user intervention
33    UserIntervention,
34    /// Error is not recoverable
35    Fatal,
36}
37
38/// Error context for better debugging and tracing
39#[derive(Debug, Clone)]
40pub struct ErrorContext {
41    /// Request ID for correlation
42    pub request_id: Option<String>,
43    /// Operation being performed when error occurred
44    pub operation: Option<String>,
45    /// Additional context data
46    pub metadata: std::collections::HashMap<String, serde_json::Value>,
47}
48
49impl ErrorContext {
50    /// Create a new error context
51    pub fn new() -> Self {
52        Self {
53            request_id: None,
54            operation: None,
55            metadata: std::collections::HashMap::new(),
56        }
57    }
58
59    /// Set request ID for correlation
60    pub fn with_request_id(mut self, request_id: String) -> Self {
61        self.request_id = Some(request_id);
62        self
63    }
64
65    /// Set operation context
66    pub fn with_operation(mut self, operation: String) -> Self {
67        self.operation = Some(operation);
68        self
69    }
70
71    /// Add metadata
72    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/// The main error type for the codeprism library
85#[derive(Debug)]
86pub enum Error {
87    /// Input/output related errors
88    Io(std::io::Error),
89
90    /// JSON serialization/deserialization errors
91    Json(serde_json::Error),
92
93    /// Configuration related errors
94    Config {
95        /// Configuration key or path that caused the error
96        key: String,
97        /// Error message
98        message: String,
99        /// Error context for correlation and debugging
100        context: Option<Box<ErrorContext>>,
101    },
102
103    /// Parsing related errors
104    Parse {
105        /// File that failed to parse
106        file: PathBuf,
107        /// Error message
108        message: String,
109        /// Line number where the error occurred (if available)
110        line: Option<usize>,
111        /// Error context for correlation and debugging
112        context: Option<Box<ErrorContext>>,
113    },
114
115    /// Network related errors
116    Network {
117        /// Error message
118        message: String,
119        /// Error context for correlation and debugging
120        context: Option<Box<ErrorContext>>,
121    },
122
123    /// Database related errors
124    Database {
125        /// Error message
126        message: String,
127        /// Error context for correlation and debugging
128        context: Option<Box<ErrorContext>>,
129    },
130
131    /// Resource exhaustion errors
132    ResourceExhausted {
133        /// Resource type that was exhausted
134        resource: String,
135        /// Error message
136        message: String,
137        /// Error context for correlation and debugging
138        context: Option<Box<ErrorContext>>,
139    },
140
141    /// Timeout errors
142    Timeout {
143        /// Operation that timed out
144        operation: String,
145        /// Timeout duration
146        timeout: Duration,
147        /// Error context for correlation and debugging
148        context: Option<Box<ErrorContext>>,
149    },
150
151    /// Operation was cancelled
152    Cancelled {
153        /// Operation that was cancelled
154        operation: String,
155        /// Error context for correlation and debugging
156        context: Option<Box<ErrorContext>>,
157    },
158
159    /// Permission/authorization errors
160    Permission {
161        /// Resource or operation that was denied
162        resource: String,
163        /// Error message
164        message: String,
165        /// Error context for correlation and debugging
166        context: Option<Box<ErrorContext>>,
167    },
168
169    /// Input validation errors
170    Validation {
171        /// Field that failed validation
172        field: String,
173        /// Error message
174        message: String,
175        /// Error context for correlation and debugging
176        context: Option<Box<ErrorContext>>,
177    },
178
179    /// Generic errors for cases not covered by specific types
180    Generic {
181        /// Error message
182        message: String,
183        /// Error severity
184        severity: ErrorSeverity,
185        /// Recovery strategy suggestion
186        recovery_strategy: RecoveryStrategy,
187        /// Error context for correlation and debugging
188        context: Option<Box<ErrorContext>>,
189    },
190}
191
192impl Clone for Error {
193    fn clone(&self) -> Self {
194        match self {
195            Self::Io(e) => {
196                // std::io::Error doesn't implement Clone, so we create a new one
197                Self::Io(std::io::Error::other(e.to_string()))
198            }
199            Self::Json(e) => {
200                // serde_json::Error doesn't implement Clone, so we create a new one
201                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    /// Get error code for logging and monitoring
288    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    /// Get error severity
306    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    /// Get recovery strategy
324    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    /// Check if error is recoverable
344    pub fn is_recoverable(&self) -> bool {
345        !matches!(self.get_recovery_strategy(), RecoveryStrategy::Fatal)
346    }
347
348    /// Get error context if available
349    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    /// Create a parse error with context
366    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    /// Create a simple parse error
380    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    /// Create a configuration error
390    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    /// Create a permission error
399    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    /// Create a timeout error
408    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    /// Create a network error
417    pub fn network(message: impl Into<String>) -> Self {
418        Self::Network {
419            message: message.into(),
420            context: None,
421        }
422    }
423
424    /// Create a validation error
425    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    /// Create a generic error
434    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    /// Create a resource exhausted error
448    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    /// Create a cancelled error
457    pub fn cancelled(operation: impl Into<String>) -> Self {
458        Self::Cancelled {
459            operation: operation.into(),
460            context: None,
461        }
462    }
463
464    /// Create a database error
465    pub fn database(message: impl Into<String>) -> Self {
466        Self::Database {
467            message: message.into(),
468            context: None,
469        }
470    }
471
472    /// Create an IO error from a string message
473    pub fn io(message: impl Into<String>) -> Self {
474        Self::Io(std::io::Error::other(message.into()))
475    }
476
477    /// Create a storage error (mapped to database error)
478    pub fn storage(message: impl Into<String>) -> Self {
479        Self::Database {
480            message: format!("Storage: {}", message.into()),
481            context: None,
482        }
483    }
484
485    /// Create a watcher error (mapped to generic error)
486    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    /// Create an indexing error (mapped to generic error)
496    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    /// Create an unsupported language error (mapped to validation error)
506    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    /// Create a node not found error (mapped to validation error)
515    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    /// Create a generic other error
524    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    /// Check if this error should trigger a retry
534    pub fn should_retry(&self) -> bool {
535        matches!(self.get_recovery_strategy(), RecoveryStrategy::Retry)
536    }
537
538    /// Get the severity level of this error (for backward compatibility)
539    pub fn severity(&self) -> ErrorSeverity {
540        self.get_severity()
541    }
542
543    /// Get the recovery strategy for this error (for backward compatibility)
544    pub fn recovery_strategy(&self) -> RecoveryStrategy {
545        self.get_recovery_strategy()
546    }
547
548    /// Get error code for JSON-RPC responses (for backward compatibility)
549    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, // Internal error
563        }
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}