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