chie_shared/
result.rs

1//! Generic result type with error context for better error handling
2//!
3//! This module provides a `ChieResult<T>` type that wraps errors with additional context,
4//! making it easier to debug and understand where errors occurred.
5
6use std::fmt;
7use std::sync::OnceLock;
8
9/// Global telemetry handler for error reporting
10///
11/// This is a thread-safe, lazy-initialized global handler that can be set once
12/// to report errors to external monitoring systems (e.g., `Sentry`, `DataDog`, `CloudWatch`).
13static TELEMETRY_HANDLER: OnceLock<fn(&ChieError)> = OnceLock::new();
14
15/// Set the global error telemetry handler
16///
17/// This function sets a global handler that will be called whenever an error
18/// is reported via `report_telemetry()` or `new_with_telemetry()`.
19///
20/// The handler can only be set once. Subsequent calls will be ignored.
21///
22/// # Example
23///
24/// ```
25/// use chie_shared::{set_telemetry_handler, ChieError};
26///
27/// fn my_telemetry_handler(error: &ChieError) {
28///     // Report to monitoring system
29///     eprintln!("Telemetry: {} - {}", error.kind, error.message);
30/// }
31///
32/// set_telemetry_handler(my_telemetry_handler);
33/// ```
34pub fn set_telemetry_handler(handler: fn(&ChieError)) {
35    let _ = TELEMETRY_HANDLER.set(handler);
36}
37
38/// Generic result type for CHIE operations
39///
40/// This type provides consistent error handling across the crate with
41/// optional error context for better debugging.
42pub type ChieResult<T> = Result<T, ChieError>;
43
44/// Enhanced error type with context information
45///
46/// This error type wraps the underlying error with additional context
47/// about where and why the error occurred.
48#[derive(Debug, Clone)]
49pub struct ChieError {
50    /// The kind of error that occurred
51    pub kind: ErrorKind,
52    /// Human-readable error message
53    pub message: String,
54    /// Optional context about where/why the error occurred
55    pub context: Vec<String>,
56}
57
58/// Categories of errors in the CHIE protocol
59#[derive(Debug, Clone, PartialEq, Eq)]
60pub enum ErrorKind {
61    /// Validation errors (invalid input, constraint violations)
62    Validation,
63    /// Network errors (connection failures, timeouts)
64    Network,
65    /// Serialization/deserialization errors
66    Serialization,
67    /// Cryptographic errors (signing, verification failures)
68    Cryptographic,
69    /// Storage errors (disk I/O, database issues)
70    Storage,
71    /// Resource exhaustion (quota exceeded, rate limited)
72    ResourceExhausted,
73    /// Not found errors (missing content, peer, etc.)
74    NotFound,
75    /// Already exists errors (duplicate content, etc.)
76    AlreadyExists,
77    /// Permission denied errors
78    PermissionDenied,
79    /// Internal errors (bugs, unexpected state)
80    Internal,
81}
82
83impl ChieError {
84    /// Create a new error with the specified kind and message
85    #[must_use]
86    pub fn new(kind: ErrorKind, message: impl Into<String>) -> Self {
87        Self {
88            kind,
89            message: message.into(),
90            context: Vec::new(),
91        }
92    }
93
94    /// Create a validation error
95    #[must_use]
96    pub fn validation(message: impl Into<String>) -> Self {
97        Self::new(ErrorKind::Validation, message)
98    }
99
100    /// Create a network error
101    #[must_use]
102    pub fn network(message: impl Into<String>) -> Self {
103        Self::new(ErrorKind::Network, message)
104    }
105
106    /// Create a serialization error
107    #[must_use]
108    pub fn serialization(message: impl Into<String>) -> Self {
109        Self::new(ErrorKind::Serialization, message)
110    }
111
112    /// Create a cryptographic error
113    #[must_use]
114    pub fn cryptographic(message: impl Into<String>) -> Self {
115        Self::new(ErrorKind::Cryptographic, message)
116    }
117
118    /// Create a storage error
119    #[must_use]
120    pub fn storage(message: impl Into<String>) -> Self {
121        Self::new(ErrorKind::Storage, message)
122    }
123
124    /// Create a resource exhausted error
125    #[must_use]
126    pub fn resource_exhausted(message: impl Into<String>) -> Self {
127        Self::new(ErrorKind::ResourceExhausted, message)
128    }
129
130    /// Create a not found error
131    #[must_use]
132    pub fn not_found(message: impl Into<String>) -> Self {
133        Self::new(ErrorKind::NotFound, message)
134    }
135
136    /// Create an already exists error
137    #[must_use]
138    pub fn already_exists(message: impl Into<String>) -> Self {
139        Self::new(ErrorKind::AlreadyExists, message)
140    }
141
142    /// Create a permission denied error
143    #[must_use]
144    pub fn permission_denied(message: impl Into<String>) -> Self {
145        Self::new(ErrorKind::PermissionDenied, message)
146    }
147
148    /// Create an internal error
149    #[must_use]
150    pub fn internal(message: impl Into<String>) -> Self {
151        Self::new(ErrorKind::Internal, message)
152    }
153
154    /// Add context to the error
155    ///
156    /// # Examples
157    ///
158    /// ```
159    /// use chie_shared::ChieError;
160    ///
161    /// let err = ChieError::validation("Invalid CID")
162    ///     .context("While validating content metadata")
163    ///     .context("In upload handler");
164    /// ```
165    #[must_use]
166    pub fn context(mut self, ctx: impl Into<String>) -> Self {
167        self.context.push(ctx.into());
168        self
169    }
170
171    /// Check if this is a transient error that might succeed on retry
172    #[must_use]
173    pub fn is_transient(&self) -> bool {
174        matches!(
175            self.kind,
176            ErrorKind::Network | ErrorKind::ResourceExhausted | ErrorKind::Storage
177        )
178    }
179
180    /// Check if this is a permanent error that won't succeed on retry
181    #[must_use]
182    pub fn is_permanent(&self) -> bool {
183        !self.is_transient()
184    }
185
186    /// Get the full error message with context
187    #[must_use]
188    pub fn full_message(&self) -> String {
189        if self.context.is_empty() {
190            self.message.clone()
191        } else {
192            let context = self.context.join(" -> ");
193            format!("{}: {}", context, self.message)
194        }
195    }
196
197    /// Report this error to telemetry if a handler is set
198    ///
199    /// This allows errors to be reported to external monitoring systems.
200    /// The global telemetry handler must be set using `set_telemetry_handler`.
201    pub fn report_telemetry(&self) {
202        if let Some(handler) = TELEMETRY_HANDLER.get() {
203            handler(self);
204        }
205    }
206
207    /// Create an error and immediately report it to telemetry
208    #[must_use]
209    pub fn new_with_telemetry(kind: ErrorKind, message: impl Into<String>) -> Self {
210        let error = Self::new(kind, message);
211        error.report_telemetry();
212        error
213    }
214}
215
216impl fmt::Display for ChieError {
217    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
218        write!(f, "{}", self.full_message())
219    }
220}
221
222impl std::error::Error for ChieError {}
223
224impl fmt::Display for ErrorKind {
225    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
226        match self {
227            Self::Validation => write!(f, "Validation"),
228            Self::Network => write!(f, "Network"),
229            Self::Serialization => write!(f, "Serialization"),
230            Self::Cryptographic => write!(f, "Cryptographic"),
231            Self::Storage => write!(f, "Storage"),
232            Self::ResourceExhausted => write!(f, "ResourceExhausted"),
233            Self::NotFound => write!(f, "NotFound"),
234            Self::AlreadyExists => write!(f, "AlreadyExists"),
235            Self::PermissionDenied => write!(f, "PermissionDenied"),
236            Self::Internal => write!(f, "Internal"),
237        }
238    }
239}
240
241/// Extension trait to add context to any result
242pub trait ResultExt<T> {
243    /// Add context to an error
244    ///
245    /// # Errors
246    ///
247    /// Returns the error with additional context if the result is `Err`
248    fn context(self, ctx: impl Into<String>) -> ChieResult<T>;
249
250    /// Add context using a closure (lazy evaluation)
251    ///
252    /// # Errors
253    ///
254    /// Returns the error with additional context if the result is `Err`
255    fn with_context<F>(self, f: F) -> ChieResult<T>
256    where
257        F: FnOnce() -> String;
258}
259
260impl<T> ResultExt<T> for ChieResult<T> {
261    fn context(self, ctx: impl Into<String>) -> ChieResult<T> {
262        self.map_err(|e| e.context(ctx))
263    }
264
265    fn with_context<F>(self, f: F) -> ChieResult<T>
266    where
267        F: FnOnce() -> String,
268    {
269        self.map_err(|e| e.context(f()))
270    }
271}
272
273/// Panic recovery utilities for safer error handling
274pub struct PanicRecovery;
275
276impl PanicRecovery {
277    /// Catch panics and convert them to `ChieError`
278    ///
279    /// # Errors
280    ///
281    /// Returns `ChieError` with `ErrorKind::Internal` if the function panics
282    ///
283    /// # Examples
284    ///
285    /// ```
286    /// use chie_shared::{PanicRecovery, ErrorKind};
287    ///
288    /// let result = PanicRecovery::catch_unwind(|| {
289    ///     // This would normally panic
290    ///     if false {
291    ///         panic!("Something went wrong!");
292    ///     }
293    ///     42
294    /// });
295    ///
296    /// assert!(result.is_ok());
297    /// assert_eq!(result.unwrap(), 42);
298    /// ```
299    pub fn catch_unwind<F, T>(f: F) -> ChieResult<T>
300    where
301        F: FnOnce() -> T + std::panic::UnwindSafe,
302    {
303        std::panic::catch_unwind(f).map_err(|panic_info| {
304            let panic_msg = if let Some(s) = panic_info.downcast_ref::<&str>() {
305                (*s).to_string()
306            } else if let Some(s) = panic_info.downcast_ref::<String>() {
307                s.clone()
308            } else {
309                "Unknown panic".to_string()
310            };
311
312            ChieError::internal(format!("Panic caught: {panic_msg}"))
313        })
314    }
315
316    /// Catch panics with custom error context
317    ///
318    /// # Errors
319    ///
320    /// Returns `ChieError` with the provided context if the function panics
321    pub fn catch_unwind_with_context<F, T>(f: F, context: impl Into<String>) -> ChieResult<T>
322    where
323        F: FnOnce() -> T + std::panic::UnwindSafe,
324    {
325        Self::catch_unwind(f).map_err(|e| e.context(context))
326    }
327
328    /// Retry a function that might panic, up to `max_attempts` times
329    ///
330    /// # Errors
331    ///
332    /// Returns `ChieError` if all attempts fail with panics
333    ///
334    /// # Examples
335    ///
336    /// ```
337    /// use chie_shared::PanicRecovery;
338    /// use std::sync::atomic::{AtomicUsize, Ordering};
339    ///
340    /// let attempt = AtomicUsize::new(0);
341    /// let result = PanicRecovery::retry_on_panic(3, || {
342    ///     let current = attempt.fetch_add(1, Ordering::SeqCst) + 1;
343    ///     if current < 3 {
344    ///         panic!("Not yet!");
345    ///     }
346    ///     "success"
347    /// });
348    ///
349    /// assert!(result.is_ok());
350    /// ```
351    pub fn retry_on_panic<F, T>(max_attempts: usize, mut f: F) -> ChieResult<T>
352    where
353        F: FnMut() -> T + std::panic::UnwindSafe,
354    {
355        let mut last_error = None;
356
357        for attempt in 1..=max_attempts {
358            match std::panic::catch_unwind(std::panic::AssertUnwindSafe(&mut f)) {
359                Ok(value) => return Ok(value),
360                Err(panic_info) => {
361                    let panic_msg = if let Some(s) = panic_info.downcast_ref::<&str>() {
362                        (*s).to_string()
363                    } else if let Some(s) = panic_info.downcast_ref::<String>() {
364                        s.clone()
365                    } else {
366                        "Unknown panic".to_string()
367                    };
368
369                    last_error = Some(ChieError::internal(format!(
370                        "Panic on attempt {attempt}/{max_attempts}: {panic_msg}"
371                    )));
372                }
373            }
374        }
375
376        Err(last_error.unwrap_or_else(|| ChieError::internal("All retry attempts failed")))
377    }
378
379    /// Execute function with panic barrier - isolates panics from caller
380    pub fn with_barrier<F, T>(f: F, fallback: T) -> T
381    where
382        F: FnOnce() -> T + std::panic::UnwindSafe,
383        T: Clone,
384    {
385        std::panic::catch_unwind(f).unwrap_or(fallback)
386    }
387}
388
389#[cfg(test)]
390mod tests {
391    use super::*;
392
393    #[test]
394    fn test_chie_error_creation() {
395        let err = ChieError::validation("Invalid CID");
396        assert_eq!(err.kind, ErrorKind::Validation);
397        assert_eq!(err.message, "Invalid CID");
398        assert!(err.context.is_empty());
399    }
400
401    #[test]
402    fn test_chie_error_with_context() {
403        let err = ChieError::validation("Invalid CID")
404            .context("While validating content")
405            .context("In upload handler");
406
407        assert_eq!(err.context.len(), 2);
408        assert_eq!(err.context[0], "While validating content");
409        assert_eq!(err.context[1], "In upload handler");
410    }
411
412    #[test]
413    fn test_error_full_message() {
414        let err = ChieError::validation("Invalid CID")
415            .context("While validating content")
416            .context("In upload handler");
417
418        let msg = err.full_message();
419        assert!(msg.contains("While validating content"));
420        assert!(msg.contains("In upload handler"));
421        assert!(msg.contains("Invalid CID"));
422    }
423
424    #[test]
425    fn test_error_display() {
426        let err = ChieError::validation("Invalid CID");
427        assert_eq!(err.to_string(), "Invalid CID");
428
429        let err_with_ctx = err.context("In validator");
430        assert_eq!(err_with_ctx.to_string(), "In validator: Invalid CID");
431    }
432
433    #[test]
434    fn test_is_transient() {
435        assert!(ChieError::network("Connection failed").is_transient());
436        assert!(ChieError::resource_exhausted("Quota exceeded").is_transient());
437        assert!(ChieError::storage("Disk full").is_transient());
438        assert!(!ChieError::validation("Invalid input").is_transient());
439        assert!(!ChieError::permission_denied("Access denied").is_transient());
440    }
441
442    #[test]
443    fn test_is_permanent() {
444        assert!(ChieError::validation("Invalid input").is_permanent());
445        assert!(ChieError::permission_denied("Access denied").is_permanent());
446        assert!(!ChieError::network("Connection failed").is_permanent());
447    }
448
449    #[test]
450    fn test_error_kinds() {
451        assert_eq!(ChieError::validation("").kind, ErrorKind::Validation);
452        assert_eq!(ChieError::network("").kind, ErrorKind::Network);
453        assert_eq!(ChieError::serialization("").kind, ErrorKind::Serialization);
454        assert_eq!(ChieError::cryptographic("").kind, ErrorKind::Cryptographic);
455        assert_eq!(ChieError::storage("").kind, ErrorKind::Storage);
456        assert_eq!(
457            ChieError::resource_exhausted("").kind,
458            ErrorKind::ResourceExhausted
459        );
460        assert_eq!(ChieError::not_found("").kind, ErrorKind::NotFound);
461        assert_eq!(ChieError::already_exists("").kind, ErrorKind::AlreadyExists);
462        assert_eq!(
463            ChieError::permission_denied("").kind,
464            ErrorKind::PermissionDenied
465        );
466        assert_eq!(ChieError::internal("").kind, ErrorKind::Internal);
467    }
468
469    #[test]
470    fn test_result_ext_context() {
471        let result: ChieResult<i32> = Err(ChieError::validation("Invalid value"));
472        let result_with_ctx = result.context("In function foo");
473
474        assert!(result_with_ctx.is_err());
475        let err = result_with_ctx.unwrap_err();
476        assert_eq!(err.context.len(), 1);
477        assert_eq!(err.context[0], "In function foo");
478    }
479
480    #[test]
481    fn test_result_ext_with_context() {
482        let result: ChieResult<i32> = Err(ChieError::validation("Invalid value"));
483        let result_with_ctx = result.with_context(|| format!("Value was {}", 42));
484
485        assert!(result_with_ctx.is_err());
486        let err = result_with_ctx.unwrap_err();
487        assert_eq!(err.context.len(), 1);
488        assert_eq!(err.context[0], "Value was 42");
489    }
490
491    #[test]
492    fn test_error_kind_display() {
493        assert_eq!(ErrorKind::Validation.to_string(), "Validation");
494        assert_eq!(ErrorKind::Network.to_string(), "Network");
495        assert_eq!(ErrorKind::NotFound.to_string(), "NotFound");
496    }
497
498    #[test]
499    fn test_result_ok_preserves_value() {
500        let result: ChieResult<i32> = Ok(42);
501        let result_with_ctx = result.context("Should not be called");
502
503        assert!(result_with_ctx.is_ok());
504        assert_eq!(result_with_ctx.unwrap(), 42);
505    }
506
507    // Telemetry tests
508    #[test]
509    fn test_telemetry_report() {
510        // This test just ensures the method exists and doesn't panic
511        let error = ChieError::validation("Test error");
512        error.report_telemetry(); // Should not panic even if handler not set
513    }
514
515    #[test]
516    fn test_new_with_telemetry() {
517        // Create error with telemetry (will call handler if set)
518        let error = ChieError::new_with_telemetry(ErrorKind::Network, "Network failure");
519        assert_eq!(error.kind, ErrorKind::Network);
520        assert_eq!(error.message, "Network failure");
521    }
522
523    #[test]
524    fn test_set_telemetry_handler() {
525        // Define a simple handler function
526        fn test_handler(error: &ChieError) {
527            // Just verify the error has expected structure
528            let _ = &error.kind;
529            let _ = &error.message;
530        }
531
532        // Should not panic
533        set_telemetry_handler(test_handler);
534    }
535
536    // Panic recovery tests
537    #[test]
538    fn test_catch_unwind_success() {
539        let result = PanicRecovery::catch_unwind(|| 42);
540        assert!(result.is_ok());
541        assert_eq!(result.unwrap(), 42);
542    }
543
544    #[test]
545    fn test_catch_unwind_panic() {
546        let result = PanicRecovery::catch_unwind(|| {
547            panic!("Test panic");
548        });
549        assert!(result.is_err());
550        let err = result.unwrap_err();
551        assert_eq!(err.kind, ErrorKind::Internal);
552        assert!(err.message.contains("Panic caught"));
553    }
554
555    #[test]
556    fn test_catch_unwind_with_context() {
557        let result = PanicRecovery::catch_unwind_with_context(
558            || panic!("Oops"),
559            "During database operation",
560        );
561        assert!(result.is_err());
562        let err = result.unwrap_err();
563        assert!(!err.context.is_empty());
564    }
565
566    #[test]
567    fn test_retry_on_panic_success_first_try() {
568        let result = PanicRecovery::retry_on_panic(3, || "success");
569        assert!(result.is_ok());
570        assert_eq!(result.unwrap(), "success");
571    }
572
573    #[test]
574    fn test_retry_on_panic_success_after_retries() {
575        use std::sync::atomic::{AtomicUsize, Ordering};
576        let attempt = AtomicUsize::new(0);
577        let result = PanicRecovery::retry_on_panic(3, || {
578            let current = attempt.fetch_add(1, Ordering::SeqCst) + 1;
579            if current < 3 {
580                panic!("Not yet");
581            }
582            "success"
583        });
584        assert!(result.is_ok());
585        assert_eq!(result.unwrap(), "success");
586        assert_eq!(attempt.load(Ordering::SeqCst), 3);
587    }
588
589    #[test]
590    fn test_retry_on_panic_all_fail() {
591        let result = PanicRecovery::retry_on_panic(2, || {
592            panic!("Always fails");
593        });
594        assert!(result.is_err());
595        let err = result.unwrap_err();
596        assert!(err.message.contains("attempt 2/2"));
597    }
598
599    #[test]
600    fn test_with_barrier_success() {
601        let result = PanicRecovery::with_barrier(|| 42, 0);
602        assert_eq!(result, 42);
603    }
604
605    #[test]
606    fn test_with_barrier_panic_fallback() {
607        let result = PanicRecovery::with_barrier(
608            || {
609                panic!("Panic!");
610            },
611            999,
612        );
613        assert_eq!(result, 999);
614    }
615}