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}