Skip to main content

icechunk_types/
error.rs

1//! Generic error wrapper with span tracing.
2//!
3//! [`ICError<E>`] wraps an error kind `E` with a [`SpanTrace`] captured at the
4//! point of construction. Every error type in icechunk is an alias for
5//! `ICError<SomeErrorKind>` (e.g. `type SessionError = ICError<SessionErrorKind>`).
6//!
7//! # Converting errors
8//!
9//! Prefer **`inject`** over `capture` when the error is already an [`ICError`],
10//! because `inject` preserves the original span trace while `capture` replaces
11//! it with a new one captured at the current call site.
12//!
13//! | Method | Input | Span trace | Use when |
14//! |---|---|---|---|
15//! | [`ICError::capture`] | bare error kind | new | constructing a fresh error |
16//! | [`.capture()`](ICResultExt::capture) | `Result<T, E>` where `E: Into<Kind>` | new | converting a foreign (non-ICError) result |
17//! | [`.capture_box()`](ICResultExt::capture_box) | `Result<T, E>` where `E: Error` | new | same, when Kind has `From<Box<dyn Error>>` |
18//! | [`.inject()`](ICResultCtxExt::inject) | `Result<T, ICError<E>>` | **preserved** | propagating between `ICError` kinds |
19//! | [`ICError::inject`] | `ICError<E>` | **preserved** | same, outside of Result |
20//!
21//! # Examples
22//!
23//! **Constructing a fresh error** — use the type alias, not `ICError` directly:
24//! ```ignore
25//! Err(SessionError::capture(SessionErrorKind::ReadOnlySession))
26//! ```
27//!
28//! **Converting a foreign result** (e.g. serde, I/O):
29//! ```ignore
30//! serde_json::to_vec(&data).capture()?;          // E: Into<Kind>
31//! builder.build().capture_box()?;                 // E: Error, Kind: From<Box<dyn Error>>
32//! rmp_serde::to_vec(&data).map_err(Box::new).capture()?;  // Kind: From<Box<ConcreteError>>
33//! ```
34//!
35//! **Propagating between `ICError` kinds** — preserves span trace:
36//! ```ignore
37//! session_fn().inject()?;   // SessionError → RepositoryError
38//! ```
39
40use std::fmt::Display;
41
42use tracing_error::SpanTrace;
43
44#[derive(Debug)]
45pub struct ICError<E> {
46    pub kind: E,
47    pub context: SpanTrace,
48}
49
50impl<E> ICError<E> {
51    /// Wrap `kind` in an [`ICError`] with a [`SpanTrace`] captured at the call site.
52    pub fn capture(kind: E) -> Self {
53        Self { kind, context: SpanTrace::capture() }
54    }
55
56    pub fn kind(&self) -> &E {
57        &self.kind
58    }
59
60    pub fn span(&self) -> &SpanTrace {
61        &self.context
62    }
63
64    /// Convert the error kind, preserving the original span trace.
65    ///
66    /// Prefer this over [`capture`](Self::capture) when you already have an
67    /// `ICError` and want to change the kind without losing trace context.
68    pub fn inject<F>(self) -> ICError<F>
69    where
70        E: Into<F>,
71    {
72        ICError { kind: self.kind.into(), context: self.context }
73    }
74}
75
76impl<E: Display> Display for ICError<E> {
77    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78        self.kind.fmt(f)?;
79        write!(f, "\n\ncontext:\n{}\n", self.context)?;
80        Ok(())
81    }
82}
83
84impl<E: std::error::Error + 'static> std::error::Error for ICError<E> {
85    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
86        Some(&self.kind)
87    }
88
89    fn cause(&self) -> Option<&dyn std::error::Error> {
90        self.source()
91    }
92}
93
94// We intentionally omit `From<E> for ICError<E>` to force callers to choose
95// between `capture` (new span trace) and `inject` (preserved span trace).
96
97/// Extension trait for converting `Result<T, E>` into `Result<T, ICError<Kind>>`,
98/// capturing a new span trace.
99pub trait ICResultExt<T, E> {
100    /// Convert the error via `E: Into<Kind>`, capturing a new span trace.
101    fn capture<Kind>(self) -> Result<T, ICError<Kind>>
102    where
103        E: Into<Kind>;
104
105    /// Box the error into `Box<dyn Error>`, then convert via `Into<Kind>`,
106    /// capturing a new span trace.
107    ///
108    /// Use this when the error kind has `#[from] Box<dyn Error + Send + Sync>`.
109    /// When the kind instead has `#[from] Box<ConcreteError>`, use
110    /// `.map_err(Box::new).capture()`.
111    fn capture_box<Kind>(self) -> Result<T, ICError<Kind>>
112    where
113        E: std::error::Error + Send + Sync + 'static,
114        Box<dyn std::error::Error + Send + Sync + 'static>: Into<Kind>;
115}
116
117impl<T, E> ICResultExt<T, E> for Result<T, E> {
118    fn capture<Kind>(self) -> Result<T, ICError<Kind>>
119    where
120        E: Into<Kind>,
121    {
122        self.map_err(|e| ICError::capture(e.into()))
123    }
124
125    fn capture_box<Kind>(self) -> Result<T, ICError<Kind>>
126    where
127        E: std::error::Error + Send + Sync + 'static,
128        Box<dyn std::error::Error + Send + Sync + 'static>: Into<Kind>,
129    {
130        self.map_err(|e| {
131            let e: Box<dyn std::error::Error + Send + Sync + 'static> = Box::new(e);
132            ICError::capture(e.into())
133        })
134    }
135}
136
137/// Extension trait for converting `Result<T, ICError<E>>` into
138/// `Result<T, ICError<Kind>>`, **preserving** the original span trace.
139///
140/// Prefer this over [`ICResultExt`] when the error is already an [`ICError`].
141pub trait ICResultCtxExt<T, E> {
142    fn inject<Kind>(self) -> Result<T, ICError<Kind>>
143    where
144        E: Into<Kind>;
145}
146
147impl<T, E> ICResultCtxExt<T, E> for Result<T, ICError<E>> {
148    fn inject<Kind>(self) -> Result<T, ICError<Kind>>
149    where
150        E: Into<Kind>,
151    {
152        self.map_err(|e| e.inject())
153    }
154}