1use std::fmt;
7use std::sync::OnceLock;
8
9static TELEMETRY_HANDLER: OnceLock<fn(&ChieError)> = OnceLock::new();
14
15pub fn set_telemetry_handler(handler: fn(&ChieError)) {
35 let _ = TELEMETRY_HANDLER.set(handler);
36}
37
38pub type ChieResult<T> = Result<T, ChieError>;
43
44#[derive(Debug, Clone)]
49pub struct ChieError {
50 pub kind: ErrorKind,
52 pub message: String,
54 pub context: Vec<String>,
56}
57
58#[derive(Debug, Clone, PartialEq, Eq)]
60pub enum ErrorKind {
61 Validation,
63 Network,
65 Serialization,
67 Cryptographic,
69 Storage,
71 ResourceExhausted,
73 NotFound,
75 AlreadyExists,
77 PermissionDenied,
79 Internal,
81}
82
83impl ChieError {
84 #[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 #[must_use]
96 pub fn validation(message: impl Into<String>) -> Self {
97 Self::new(ErrorKind::Validation, message)
98 }
99
100 #[must_use]
102 pub fn network(message: impl Into<String>) -> Self {
103 Self::new(ErrorKind::Network, message)
104 }
105
106 #[must_use]
108 pub fn serialization(message: impl Into<String>) -> Self {
109 Self::new(ErrorKind::Serialization, message)
110 }
111
112 #[must_use]
114 pub fn cryptographic(message: impl Into<String>) -> Self {
115 Self::new(ErrorKind::Cryptographic, message)
116 }
117
118 #[must_use]
120 pub fn storage(message: impl Into<String>) -> Self {
121 Self::new(ErrorKind::Storage, message)
122 }
123
124 #[must_use]
126 pub fn resource_exhausted(message: impl Into<String>) -> Self {
127 Self::new(ErrorKind::ResourceExhausted, message)
128 }
129
130 #[must_use]
132 pub fn not_found(message: impl Into<String>) -> Self {
133 Self::new(ErrorKind::NotFound, message)
134 }
135
136 #[must_use]
138 pub fn already_exists(message: impl Into<String>) -> Self {
139 Self::new(ErrorKind::AlreadyExists, message)
140 }
141
142 #[must_use]
144 pub fn permission_denied(message: impl Into<String>) -> Self {
145 Self::new(ErrorKind::PermissionDenied, message)
146 }
147
148 #[must_use]
150 pub fn internal(message: impl Into<String>) -> Self {
151 Self::new(ErrorKind::Internal, message)
152 }
153
154 #[must_use]
166 pub fn context(mut self, ctx: impl Into<String>) -> Self {
167 self.context.push(ctx.into());
168 self
169 }
170
171 #[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 #[must_use]
182 pub fn is_permanent(&self) -> bool {
183 !self.is_transient()
184 }
185
186 #[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 pub fn report_telemetry(&self) {
202 if let Some(handler) = TELEMETRY_HANDLER.get() {
203 handler(self);
204 }
205 }
206
207 #[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
241pub trait ResultExt<T> {
243 fn context(self, ctx: impl Into<String>) -> ChieResult<T>;
249
250 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
273pub struct PanicRecovery;
275
276impl PanicRecovery {
277 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 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 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 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 #[test]
509 fn test_telemetry_report() {
510 let error = ChieError::validation("Test error");
512 error.report_telemetry(); }
514
515 #[test]
516 fn test_new_with_telemetry() {
517 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 fn test_handler(error: &ChieError) {
527 let _ = &error.kind;
529 let _ = &error.message;
530 }
531
532 set_telemetry_handler(test_handler);
534 }
535
536 #[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}