error_rail/traits/mod.rs
1//! Core traits for error handling and composition.
2//!
3//! This module consolidates the key traits used throughout `error-rail`.
4
5use crate::types::alloc_type::{Box, Cow, String};
6use crate::types::{ComposableError, ErrorContext, ErrorVec, LazyContext};
7use core::time::Duration;
8
9/// Converts a type into an [`ErrorContext`] for error annotation.
10#[diagnostic::on_unimplemented(
11 message = "`{Self}` cannot be used as error context",
12 label = "this type does not implement `IntoErrorContext`",
13 note = "implement `IntoErrorContext` manually or use `impl_error_context!({Self})` macro",
14 note = "see: https://docs.rs/error-rail/latest/error_rail/macro.impl_error_context.html"
15)]
16pub trait IntoErrorContext {
17 /// Converts `self` into an [`ErrorContext`].
18 fn into_error_context(self) -> ErrorContext;
19}
20
21impl IntoErrorContext for String {
22 #[inline(always)]
23 fn into_error_context(self) -> ErrorContext {
24 ErrorContext::new(self)
25 }
26}
27
28impl IntoErrorContext for &'static str {
29 #[inline(always)]
30 fn into_error_context(self) -> ErrorContext {
31 ErrorContext::new(self)
32 }
33}
34
35impl IntoErrorContext for Cow<'static, str> {
36 #[inline(always)]
37 fn into_error_context(self) -> ErrorContext {
38 ErrorContext::new(self)
39 }
40}
41
42impl IntoErrorContext for ErrorContext {
43 #[inline(always)]
44 fn into_error_context(self) -> ErrorContext {
45 self
46 }
47}
48
49/// Classification of errors as transient or permanent.
50pub trait TransientError {
51 /// Returns `true` if this error is transient and may succeed on retry.
52 fn is_transient(&self) -> bool;
53
54 /// Returns `true` if this error is permanent and should not be retried.
55 #[inline(always)]
56 fn is_permanent(&self) -> bool {
57 !self.is_transient()
58 }
59
60 /// Optional hint for how long to wait before retrying.
61 #[inline(always)]
62 fn retry_after_hint(&self) -> Option<Duration> {
63 None
64 }
65
66 /// Returns the maximum number of retry attempts for this error.
67 #[inline(always)]
68 fn max_retries_hint(&self) -> Option<u32> {
69 None
70 }
71}
72
73#[cfg(feature = "std")]
74impl TransientError for std::io::Error {
75 #[inline]
76 fn is_transient(&self) -> bool {
77 use std::io::ErrorKind;
78 matches!(
79 self.kind(),
80 ErrorKind::ConnectionRefused
81 | ErrorKind::ConnectionReset
82 | ErrorKind::ConnectionAborted
83 | ErrorKind::TimedOut
84 | ErrorKind::Interrupted
85 | ErrorKind::WouldBlock
86 )
87 }
88}
89
90/// Extension methods for working with transient errors.
91pub trait TransientErrorExt<T, E: TransientError> {
92 /// Converts a transient error to `Some(Err(e))` for retry, or `None` to stop.
93 fn retry_if_transient(self) -> Option<Result<T, E>>;
94}
95
96impl<T, E: TransientError> TransientErrorExt<T, E> for Result<T, E> {
97 #[inline]
98 fn retry_if_transient(self) -> Option<Result<T, E>> {
99 match &self {
100 Ok(_) => None,
101 Err(e) if e.is_transient() => Some(self),
102 Err(_) => None,
103 }
104 }
105}
106
107/// Abstraction over types that carry an error variant which can be remapped.
108pub trait WithError<E> {
109 type Success;
110 type ErrorOutput<G>;
111
112 /// Maps the error value using `f`, producing a new container with error type `G`.
113 fn fmap_error<F, G>(self, f: F) -> Self::ErrorOutput<G>
114 where
115 F: Fn(E) -> G;
116
117 /// Converts the container into a `Result`, taking only the first error if invalid.
118 fn to_result_first(self) -> Result<Self::Success, E>;
119
120 /// Converts the container into a `Result`, preserving all errors if invalid.
121 fn to_result_all(self) -> Result<Self::Success, ErrorVec<E>>;
122}
123
124impl<T, E> WithError<E> for Result<T, E> {
125 type Success = T;
126 type ErrorOutput<G> = Result<T, G>;
127
128 #[inline]
129 fn fmap_error<F, G>(self, f: F) -> Self::ErrorOutput<G>
130 where
131 F: FnOnce(E) -> G,
132 {
133 self.map_err(f)
134 }
135
136 #[inline(always)]
137 fn to_result_first(self) -> Result<Self::Success, E> {
138 self
139 }
140
141 #[inline]
142 fn to_result_all(self) -> Result<Self::Success, ErrorVec<E>> {
143 self.map_err(|e| {
144 let mut error_vec = ErrorVec::with_capacity(1);
145 error_vec.push(e);
146 error_vec
147 })
148 }
149}
150
151/// Operations for error recovery and bidirectional mapping.
152pub trait ErrorOps<E>: WithError<E> {
153 /// Attempts to recover from an error using the provided recovery function.
154 fn recover<F>(self, recovery: F) -> Self
155 where
156 F: FnOnce(E) -> Self,
157 Self: Sized;
158
159 /// Maps both success and error cases simultaneously.
160 fn bimap_result<B, F, SuccessF, ErrorF>(
161 self,
162 success_f: SuccessF,
163 error_f: ErrorF,
164 ) -> Result<B, F>
165 where
166 SuccessF: FnOnce(Self::Success) -> B,
167 ErrorF: FnOnce(E) -> F,
168 Self: Sized;
169}
170
171impl<T, E> ErrorOps<E> for Result<T, E> {
172 #[inline]
173 fn recover<F>(self, recovery: F) -> Self
174 where
175 F: FnOnce(E) -> Self,
176 {
177 self.or_else(recovery)
178 }
179
180 #[inline]
181 fn bimap_result<B, F, SuccessF, ErrorF>(
182 self,
183 success_f: SuccessF,
184 error_f: ErrorF,
185 ) -> Result<B, F>
186 where
187 SuccessF: FnOnce(T) -> B,
188 ErrorF: FnOnce(E) -> F,
189 {
190 match self {
191 Ok(value) => Ok(success_f(value)),
192 Err(error) => Err(error_f(error)),
193 }
194 }
195}
196
197/// Extension trait for ergonomic context addition to `Result` types.
198///
199/// This trait provides methods for enriching errors with contextual information,
200/// wrapping them in a [`ComposableError`] for structured error handling.
201///
202/// # Examples
203///
204/// ```
205/// use error_rail::{ResultExt, ErrorContext};
206///
207/// fn read_config() -> Result<String, Box<error_rail::ComposableError<std::io::Error>>> {
208/// std::fs::read_to_string("config.toml")
209/// .ctx("failed to read configuration file")
210/// }
211///
212/// fn parse_config() -> Result<i32, Box<error_rail::ComposableError<&'static str>>> {
213/// Err("invalid format")
214/// .ctx_with(|| format!("parsing failed at line {}", 42))
215/// }
216/// ```
217pub trait ResultExt<T, E> {
218 /// Adds a static context message to the error.
219 ///
220 /// Wraps the error in a [`ComposableError`] with the provided context.
221 /// Use this when the context message is cheap to construct or already available.
222 ///
223 /// # Arguments
224 ///
225 /// * `msg` - Any type implementing [`IntoErrorContext`], such as `&str`, `String`, or [`ErrorContext`]
226 ///
227 /// # Examples
228 ///
229 /// ```
230 /// use error_rail::ResultExt;
231 ///
232 /// let result: Result<i32, &str> = Err("connection failed");
233 /// let enriched = result.ctx("database operation failed");
234 /// assert!(enriched.is_err());
235 /// ```
236 fn ctx<C: IntoErrorContext>(self, msg: C) -> Result<T, Box<ComposableError<E>>>;
237
238 /// Adds a lazily-evaluated context message to the error.
239 ///
240 /// The context is only computed if the result is an error, making this
241 /// suitable for expensive context generation (e.g., formatting, I/O).
242 ///
243 /// # Arguments
244 ///
245 /// * `f` - A closure that produces the context string when called
246 ///
247 /// # Examples
248 ///
249 /// ```
250 /// use error_rail::ResultExt;
251 ///
252 /// let result: Result<i32, &str> = Err("timeout");
253 /// let enriched = result.ctx_with(|| format!("request failed after {} retries", 3));
254 /// assert!(enriched.is_err());
255 /// ```
256 fn ctx_with<F>(self, f: F) -> Result<T, Box<ComposableError<E>>>
257 where
258 F: FnOnce() -> String;
259}
260
261impl<T, E> ResultExt<T, E> for Result<T, E> {
262 #[inline]
263 fn ctx<C: IntoErrorContext>(self, msg: C) -> Result<T, Box<ComposableError<E>>> {
264 self.map_err(|e| Box::new(ComposableError::new(e).with_context(msg)))
265 }
266
267 #[inline]
268 fn ctx_with<F>(self, f: F) -> Result<T, Box<ComposableError<E>>>
269 where
270 F: FnOnce() -> String,
271 {
272 self.map_err(|e| Box::new(ComposableError::new(e).with_context(LazyContext::new(f))))
273 }
274}
275
276/// Extension trait for adding context to already-boxed `ComposableError` results.
277///
278/// This trait provides methods for enriching errors that are already wrapped in
279/// `Box<ComposableError<E>>`, allowing additional context to be added without
280/// re-boxing the error.
281///
282/// # Examples
283///
284/// ```
285/// use error_rail::{BoxedResultExt, ResultExt, ErrorContext};
286///
287/// fn inner_operation() -> Result<i32, Box<error_rail::ComposableError<&'static str>>> {
288/// Err("inner error").ctx("inner context")
289/// }
290///
291/// fn outer_operation() -> Result<i32, Box<error_rail::ComposableError<&'static str>>> {
292/// inner_operation().ctx_boxed("outer context")
293/// }
294/// ```
295pub trait BoxedResultExt<T, E> {
296 /// Adds additional context to an already-boxed `ComposableError`.
297 ///
298 /// This method is useful when you have a `Result<T, Box<ComposableError<E>>>`
299 /// and want to add more context without changing the error type.
300 ///
301 /// # Arguments
302 ///
303 /// * `msg` - The context message to add, which must implement `IntoErrorContext`
304 ///
305 /// # Examples
306 ///
307 /// ```
308 /// use error_rail::{BoxedResultExt, ResultExt};
309 ///
310 /// let result: Result<i32, _> = Err("error").ctx("first context");
311 /// let enriched = result.ctx_boxed("second context");
312 /// ```
313 fn ctx_boxed<C: IntoErrorContext>(self, msg: C) -> Self;
314
315 /// Adds lazily-evaluated context to an already-boxed `ComposableError`.
316 ///
317 /// The context message is only computed if the result is an error,
318 /// which can improve performance when context generation is expensive.
319 ///
320 /// # Arguments
321 ///
322 /// * `f` - A closure that produces the context message
323 ///
324 /// # Examples
325 ///
326 /// ```
327 /// use error_rail::{BoxedResultExt, ResultExt};
328 ///
329 /// let result: Result<i32, _> = Err("error").ctx("first context");
330 /// let enriched = result.ctx_boxed_with(|| format!("context at {}", "runtime"));
331 /// ```
332 fn ctx_boxed_with<F>(self, f: F) -> Self
333 where
334 F: FnOnce() -> String;
335}
336
337impl<T, E> BoxedResultExt<T, E> for Result<T, Box<ComposableError<E>>> {
338 #[inline]
339 fn ctx_boxed<C: IntoErrorContext>(self, msg: C) -> Self {
340 self.map_err(|mut e| {
341 e.with_context_inplace(msg);
342 e
343 })
344 }
345
346 #[inline]
347 fn ctx_boxed_with<F>(self, f: F) -> Self
348 where
349 F: FnOnce() -> String,
350 {
351 self.map_err(|mut e| {
352 e.with_context_inplace(LazyContext::new(f));
353 e
354 })
355 }
356}
357
358/// Trait for types that can lift values and handle errors in a functorial way.
359///
360/// This trait provides a category-theoretic abstraction for error handling,
361/// allowing types to be lifted into an error-handling context and errors
362/// to be properly propagated.
363///
364/// # Type Parameters
365///
366/// * `E` - The error type that this category handles
367///
368/// # Associated Types
369///
370/// * `ErrorFunctor<T>` - The functor type that wraps values of type `T` with error handling
371///
372/// # Examples
373///
374/// ```
375/// use error_rail::traits::ErrorCategory;
376///
377/// fn example<E>() {
378/// let success: Result<i32, E> = <Result<(), E> as ErrorCategory<E>>::lift(42);
379/// assert!(success.is_ok());
380/// }
381/// ```
382pub trait ErrorCategory<E> {
383 /// The functor type that provides error handling for values of type `T`.
384 type ErrorFunctor<T>: WithError<E>;
385
386 /// Lifts a pure value into the error-handling context.
387 ///
388 /// # Arguments
389 ///
390 /// * `value` - The value to lift into the functor
391 ///
392 /// # Returns
393 ///
394 /// The value wrapped in a successful error-handling context.
395 fn lift<T>(value: T) -> Self::ErrorFunctor<T>;
396
397 /// Creates an error-handling context representing a failure.
398 ///
399 /// # Arguments
400 ///
401 /// * `error` - The error to wrap
402 ///
403 /// # Returns
404 ///
405 /// An error-handling context containing the error.
406 fn handle_error<T>(error: E) -> Self::ErrorFunctor<T>;
407}
408
409impl<E> ErrorCategory<E> for Result<(), E> {
410 type ErrorFunctor<T> = Result<T, E>;
411
412 #[inline(always)]
413 fn lift<T>(value: T) -> Result<T, E> {
414 Ok(value)
415 }
416
417 #[inline(always)]
418 fn handle_error<T>(error: E) -> Result<T, E> {
419 Err(error)
420 }
421}