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// ============================================================================
10// IntoErrorContext
11// ============================================================================
12
13/// Converts a type into an [`ErrorContext`] for error annotation.
14#[diagnostic::on_unimplemented(
15    message = "`{Self}` cannot be used as error context",
16    label = "this type does not implement `IntoErrorContext`",
17    note = "implement `IntoErrorContext` manually or use `impl_error_context!({Self})` macro",
18    note = "see: https://docs.rs/error-rail/latest/error_rail/macro.impl_error_context.html"
19)]
20pub trait IntoErrorContext {
21    /// Converts `self` into an [`ErrorContext`].
22    fn into_error_context(self) -> ErrorContext;
23}
24
25impl IntoErrorContext for String {
26    #[inline]
27    fn into_error_context(self) -> ErrorContext {
28        ErrorContext::new(self)
29    }
30}
31
32impl IntoErrorContext for &'static str {
33    #[inline]
34    fn into_error_context(self) -> ErrorContext {
35        ErrorContext::new(self)
36    }
37}
38
39impl IntoErrorContext for Cow<'static, str> {
40    #[inline]
41    fn into_error_context(self) -> ErrorContext {
42        ErrorContext::new(self)
43    }
44}
45
46impl IntoErrorContext for ErrorContext {
47    #[inline]
48    fn into_error_context(self) -> ErrorContext {
49        self
50    }
51}
52
53// ============================================================================
54// TransientError
55// ============================================================================
56
57/// Classification of errors as transient or permanent.
58pub trait TransientError {
59    /// Returns `true` if this error is transient and may succeed on retry.
60    fn is_transient(&self) -> bool;
61
62    /// Returns `true` if this error is permanent and should not be retried.
63    #[inline]
64    fn is_permanent(&self) -> bool {
65        !self.is_transient()
66    }
67
68    /// Optional hint for how long to wait before retrying.
69    #[inline]
70    fn retry_after_hint(&self) -> Option<Duration> {
71        None
72    }
73
74    /// Returns the maximum number of retry attempts for this error.
75    #[inline]
76    fn max_retries_hint(&self) -> Option<u32> {
77        None
78    }
79}
80
81#[cfg(feature = "std")]
82impl TransientError for std::io::Error {
83    fn is_transient(&self) -> bool {
84        use std::io::ErrorKind;
85        matches!(
86            self.kind(),
87            ErrorKind::ConnectionRefused
88                | ErrorKind::ConnectionReset
89                | ErrorKind::ConnectionAborted
90                | ErrorKind::TimedOut
91                | ErrorKind::Interrupted
92                | ErrorKind::WouldBlock
93        )
94    }
95}
96
97/// Extension methods for working with transient errors.
98pub trait TransientErrorExt<T, E: TransientError> {
99    /// Converts a transient error to `Some(Err(e))` for retry, or `None` to stop.
100    fn retry_if_transient(self) -> Option<Result<T, E>>;
101}
102
103impl<T, E: TransientError> TransientErrorExt<T, E> for Result<T, E> {
104    fn retry_if_transient(self) -> Option<Result<T, E>> {
105        match &self {
106            Ok(_) => None,
107            Err(e) if e.is_transient() => Some(self),
108            Err(_) => None,
109        }
110    }
111}
112
113// ============================================================================
114// WithError
115// ============================================================================
116
117/// Abstraction over types that carry an error variant which can be remapped.
118pub trait WithError<E> {
119    type Success;
120    type ErrorOutput<G>;
121
122    /// Maps the error value using `f`, producing a new container with error type `G`.
123    fn fmap_error<F, G>(self, f: F) -> Self::ErrorOutput<G>
124    where
125        F: Fn(E) -> G;
126
127    /// Converts the container into a `Result`, taking only the first error if invalid.
128    fn to_result_first(self) -> Result<Self::Success, E>;
129
130    /// Converts the container into a `Result`, preserving all errors if invalid.
131    fn to_result_all(self) -> Result<Self::Success, ErrorVec<E>>;
132}
133
134impl<T, E> WithError<E> for Result<T, E> {
135    type Success = T;
136    type ErrorOutput<G> = Result<T, G>;
137
138    fn fmap_error<F, G>(self, f: F) -> Self::ErrorOutput<G>
139    where
140        F: FnOnce(E) -> G,
141    {
142        match self {
143            Ok(t) => Ok(t),
144            Err(e) => Err(f(e)),
145        }
146    }
147
148    fn to_result_first(self) -> Result<Self::Success, E> {
149        self
150    }
151
152    fn to_result_all(self) -> Result<Self::Success, ErrorVec<E>> {
153        match self {
154            Ok(t) => Ok(t),
155            Err(e) => {
156                let mut error_vec = ErrorVec::new();
157                error_vec.push(e);
158                Err(error_vec)
159            }
160        }
161    }
162}
163
164// ============================================================================
165// ErrorOps
166// ============================================================================
167
168/// Operations for error recovery and bidirectional mapping.
169pub trait ErrorOps<E>: WithError<E> {
170    /// Attempts to recover from an error using the provided recovery function.
171    fn recover<F>(self, recovery: F) -> Self
172    where
173        F: FnOnce(E) -> Self,
174        Self: Sized;
175
176    /// Maps both success and error cases simultaneously.
177    fn bimap_result<B, F, SuccessF, ErrorF>(
178        self,
179        success_f: SuccessF,
180        error_f: ErrorF,
181    ) -> Result<B, F>
182    where
183        SuccessF: FnOnce(Self::Success) -> B,
184        ErrorF: FnOnce(E) -> F,
185        Self: Sized;
186}
187
188impl<T, E> ErrorOps<E> for Result<T, E> {
189    #[inline]
190    fn recover<F>(self, recovery: F) -> Self
191    where
192        F: FnOnce(E) -> Self,
193    {
194        match self {
195            Ok(value) => Ok(value),
196            Err(error) => recovery(error),
197        }
198    }
199
200    #[inline]
201    fn bimap_result<B, F, SuccessF, ErrorF>(
202        self,
203        success_f: SuccessF,
204        error_f: ErrorF,
205    ) -> Result<B, F>
206    where
207        SuccessF: FnOnce(T) -> B,
208        ErrorF: FnOnce(E) -> F,
209    {
210        match self {
211            Ok(value) => Ok(success_f(value)),
212            Err(error) => Err(error_f(error)),
213        }
214    }
215}
216
217// ============================================================================
218// ResultExt
219// ============================================================================
220
221/// Extension trait for ergonomic context addition to `Result` types.
222pub trait ResultExt<T, E> {
223    /// Adds a static context message to the error.
224    fn ctx<C: IntoErrorContext>(self, msg: C) -> Result<T, Box<ComposableError<E>>>;
225
226    /// Adds a lazily-evaluated context message to the error.
227    fn ctx_with<F>(self, f: F) -> Result<T, Box<ComposableError<E>>>
228    where
229        F: FnOnce() -> String;
230}
231
232impl<T, E> ResultExt<T, E> for Result<T, E> {
233    #[inline]
234    fn ctx<C: IntoErrorContext>(self, msg: C) -> Result<T, Box<ComposableError<E>>> {
235        self.map_err(|e| Box::new(ComposableError::new(e).with_context(msg)))
236    }
237
238    #[inline]
239    fn ctx_with<F>(self, f: F) -> Result<T, Box<ComposableError<E>>>
240    where
241        F: FnOnce() -> String,
242    {
243        self.map_err(|e| Box::new(ComposableError::new(e).with_context(LazyContext::new(f))))
244    }
245}
246
247/// Extension trait for adding context to already-boxed `ComposableError` results.
248pub trait BoxedResultExt<T, E> {
249    /// Adds additional context to an already-boxed `ComposableError`.
250    fn ctx_boxed<C: IntoErrorContext>(self, msg: C) -> Self;
251
252    /// Adds lazily-evaluated context to an already-boxed `ComposableError`.
253    fn ctx_boxed_with<F>(self, f: F) -> Self
254    where
255        F: FnOnce() -> String;
256}
257
258impl<T, E> BoxedResultExt<T, E> for Result<T, Box<ComposableError<E>>> {
259    #[inline]
260    fn ctx_boxed<C: IntoErrorContext>(self, msg: C) -> Self {
261        self.map_err(|e| {
262            let inner = *e;
263            Box::new(inner.with_context(msg))
264        })
265    }
266
267    #[inline]
268    fn ctx_boxed_with<F>(self, f: F) -> Self
269    where
270        F: FnOnce() -> String,
271    {
272        self.map_err(|e| {
273            let inner = *e;
274            Box::new(inner.with_context(LazyContext::new(f)))
275        })
276    }
277}
278
279// ============================================================================
280// ErrorCategory
281// ============================================================================
282
283/// Trait for types that can lift values and handle errors in a functorial way.
284pub trait ErrorCategory<E> {
285    type ErrorFunctor<T>: WithError<E>;
286
287    fn lift<T>(value: T) -> Self::ErrorFunctor<T>;
288    fn handle_error<T>(error: E) -> Self::ErrorFunctor<T>;
289}
290
291impl<E> ErrorCategory<E> for Result<(), E> {
292    type ErrorFunctor<T> = Result<T, E>;
293
294    #[inline]
295    fn lift<T>(value: T) -> Result<T, E> {
296        Ok(value)
297    }
298
299    #[inline]
300    fn handle_error<T>(error: E) -> Result<T, E> {
301        Err(error)
302    }
303}