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}