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}