error_rail/async_ext/
pipeline.rs

1//! Async error pipeline for chainable error handling.
2//!
3//! Provides `AsyncErrorPipeline`, the async counterpart to [`ErrorPipeline`](crate::ErrorPipeline).
4
5use core::future::Future;
6
7use crate::traits::IntoErrorContext;
8use crate::types::alloc_type::Box;
9use crate::types::{ComposableError, MarkedError};
10
11use super::future_ext::FutureResultExt;
12
13/// Async error pipeline for chainable error handling.
14///
15/// This is the async counterpart to [`ErrorPipeline`](crate::ErrorPipeline),
16/// providing fluent, chainable error context accumulation for async operations.
17///
18/// # Examples
19///
20/// ## Basic Usage
21///
22/// ```rust,no_run
23/// use error_rail::prelude_async::*;
24///
25/// #[derive(Debug)]
26/// struct Data;
27///
28/// #[derive(Debug)]
29/// struct ApiError;
30///
31/// async fn fetch_data(_id: u64) -> Result<Data, ApiError> {
32///     Err(ApiError)
33/// }
34///
35/// async fn example(id: u64) -> BoxedResult<Data, ApiError> {
36///     AsyncErrorPipeline::new(fetch_data(id))
37///         .with_context("fetching data")
38///         .finish_boxed()
39///         .await
40/// }
41/// ```
42///
43/// ## With Multiple Contexts
44///
45/// ```rust,no_run
46/// use error_rail::prelude_async::*;
47///
48/// #[derive(Debug)]
49/// struct Order;
50///
51/// #[derive(Debug)]
52/// struct OrderError;
53///
54/// async fn load_order(_order_id: u64) -> Result<Order, OrderError> {
55///     Err(OrderError)
56/// }
57///
58/// async fn process_order(order_id: u64) -> BoxedResult<Order, OrderError> {
59///     AsyncErrorPipeline::new(load_order(order_id))
60///         .with_context(group!(
61///             message("loading order"),
62///             metadata("order_id", format!("{}", order_id))
63///         ))
64///         .finish_boxed()
65///         .await
66/// }
67/// ```
68pub struct AsyncErrorPipeline<Fut> {
69    future: Fut,
70}
71
72impl<Fut> AsyncErrorPipeline<Fut> {
73    /// Creates a new async error pipeline from a future.
74    ///
75    /// # Arguments
76    ///
77    /// * `future` - A future that returns a `Result<T, E>`
78    ///
79    /// # Examples
80    ///
81    /// ```rust
82    /// use error_rail::async_ext::AsyncErrorPipeline;
83    ///
84    /// let pipeline = AsyncErrorPipeline::new(async { Ok::<_, &str>(42) });
85    /// ```
86    #[inline]
87    pub fn new(future: Fut) -> Self {
88        Self { future }
89    }
90
91    /// Completes the pipeline and returns the inner future.
92    ///
93    /// This method consumes the pipeline and returns a future that
94    /// produces the original `Result<T, E>`.
95    ///
96    /// # Examples
97    ///
98    /// ```rust
99    /// use error_rail::async_ext::AsyncErrorPipeline;
100    ///
101    /// async fn example() -> Result<i32, &'static str> {
102    ///     AsyncErrorPipeline::new(async { Ok(42) })
103    ///         .finish()
104    ///         .await
105    /// }
106    /// ```
107    #[inline]
108    pub fn finish(self) -> Fut {
109        self.future
110    }
111}
112
113impl<Fut, T, E> AsyncErrorPipeline<Fut>
114where
115    Fut: Future<Output = Result<T, E>>,
116{
117    /// Adds a context that will be attached to any error.
118    ///
119    /// The context is only attached when an error occurs.
120    ///
121    /// Note: if you pass an already-formatted `String` (e.g. `format!(...)`),
122    /// that formatting still happens eagerly before calling `.with_context(...)`.
123    ///
124    /// # Arguments
125    ///
126    /// * `context` - Any type implementing `IntoErrorContext`
127    ///
128    /// # Examples
129    ///
130    /// ```rust
131    /// use error_rail::async_ext::AsyncErrorPipeline;
132    ///
133    /// let pipeline = AsyncErrorPipeline::new(async { Err::<(), _>("error") })
134    ///     .with_context("operation context");
135    /// ```
136    #[inline]
137    pub fn with_context<C>(
138        self,
139        context: C,
140    ) -> AsyncErrorPipeline<impl Future<Output = Result<T, ComposableError<E>>>>
141    where
142        C: IntoErrorContext,
143    {
144        AsyncErrorPipeline { future: self.future.ctx(context) }
145    }
146
147    /// Marks the error as transient or permanent based on a closure.
148    ///
149    /// This allows for flexible retry control without implementing the [`crate::traits::TransientError`]
150    /// trait for the error type.
151    ///
152    /// # Arguments
153    ///
154    /// * `classifier` - A closure that returns `true` if the error should be treated as transient
155    ///
156    /// # Examples
157    ///
158    /// ```rust
159    /// use error_rail::prelude_async::*;
160    /// use error_rail::types::MarkedError;
161    ///
162    /// async fn example() -> Result<(), MarkedError<String, impl Fn(&String) -> bool>> {
163    ///     AsyncErrorPipeline::new(async { Err("error".to_string()) })
164    ///         .mark_transient_if(|e: &String| e.contains("error"))
165    ///         .finish()
166    ///         .await
167    /// }
168    /// ```
169    #[inline]
170    pub fn mark_transient_if<F>(
171        self,
172        classifier: F,
173    ) -> AsyncErrorPipeline<impl Future<Output = Result<T, MarkedError<E, F>>>>
174    where
175        F: Fn(&E) -> bool + Send + 'static,
176        E: Send + 'static,
177        T: Send + 'static,
178    {
179        AsyncErrorPipeline {
180            future: async move {
181                self.future
182                    .await
183                    .map_err(|e| MarkedError { inner: e, classifier })
184            },
185        }
186    }
187
188    /// Adds a lazily-evaluated context using a closure.
189    ///
190    /// The closure is only called when an error occurs, avoiding
191    /// any computation on the success path.
192    ///
193    /// # Arguments
194    ///
195    /// * `f` - A closure that produces the error context
196    ///
197    /// # Examples
198    ///
199    /// ```rust,no_run
200    /// use error_rail::async_ext::AsyncErrorPipeline;
201    ///
202    /// #[derive(Debug)]
203    /// struct User;
204    ///
205    /// #[derive(Debug)]
206    /// struct ApiError;
207    ///
208    /// async fn fetch_user(_id: u64) -> Result<User, ApiError> {
209    ///     Err(ApiError)
210    /// }
211    ///
212    /// let id = 42u64;
213    /// let _pipeline = AsyncErrorPipeline::new(fetch_user(id))
214    ///     .with_context_fn(|| format!("user_id: {}", id));
215    /// ```
216    #[inline]
217    pub fn with_context_fn<F, C>(
218        self,
219        f: F,
220    ) -> AsyncErrorPipeline<impl Future<Output = Result<T, ComposableError<E>>>>
221    where
222        F: FnOnce() -> C,
223        C: IntoErrorContext,
224    {
225        AsyncErrorPipeline { future: self.future.with_ctx(f) }
226    }
227
228    /// Transforms the success value using a mapping function.
229    #[inline]
230    pub fn map<U, F>(self, f: F) -> AsyncErrorPipeline<impl Future<Output = Result<U, E>>>
231    where
232        F: FnOnce(T) -> U + Send + 'static,
233        T: Send + 'static,
234        U: Send + 'static,
235        E: Send + 'static,
236    {
237        AsyncErrorPipeline { future: async move { self.future.await.map(f) } }
238    }
239
240    /// Transforms the pipeline using a fallible function.
241    ///
242    /// This is an alias for `and_then`.
243    #[inline]
244    pub fn step<U, F>(self, f: F) -> AsyncErrorPipeline<impl Future<Output = Result<U, E>>>
245    where
246        F: FnOnce(T) -> Result<U, E> + Send + 'static,
247        T: Send + 'static,
248        U: Send + 'static,
249        E: Send + 'static,
250    {
251        AsyncErrorPipeline {
252            future: async move {
253                let res = self.future.await;
254                res.and_then(f)
255            },
256        }
257    }
258
259    /// Attempts to recover from an error using a fallback value.
260    #[inline]
261    pub fn fallback(self, value: T) -> AsyncErrorPipeline<impl Future<Output = Result<T, E>>>
262    where
263        T: Send + 'static,
264        E: Send + 'static,
265    {
266        AsyncErrorPipeline { future: async move { self.future.await.or(Ok(value)) } }
267    }
268
269    /// Attempts to recover from an error using a safe recovery function.
270    #[inline]
271    pub fn recover_safe<F>(self, f: F) -> AsyncErrorPipeline<impl Future<Output = Result<T, E>>>
272    where
273        F: FnOnce(E) -> T + Send + 'static,
274        T: Send + 'static,
275        E: Send + 'static,
276    {
277        AsyncErrorPipeline {
278            future: async move {
279                match self.future.await {
280                    Ok(v) => Ok(v),
281                    Err(e) => Ok(f(e)),
282                }
283            },
284        }
285    }
286
287    /// Adds a tag indicating this error was retried.
288    #[inline]
289    pub fn with_retry_context(
290        self,
291        attempt: u32,
292    ) -> AsyncErrorPipeline<impl Future<Output = Result<T, ComposableError<E>>>> {
293        use crate::types::utils::u32_to_cow;
294        let attempt_str = u32_to_cow(attempt);
295
296        AsyncErrorPipeline {
297            future: self
298                .future
299                .with_ctx(move || crate::ErrorContext::metadata("retry_attempt", attempt_str)),
300        }
301    }
302}
303
304impl<Fut, T, E> AsyncErrorPipeline<Fut>
305where
306    Fut: Future<Output = Result<T, ComposableError<E>>>,
307{
308    /// Completes the pipeline and returns a boxed error result.
309    ///
310    /// This is the recommended way to finish a pipeline when returning
311    /// from a function, as it provides minimal stack footprint.
312    ///
313    /// # Examples
314    ///
315    /// ```rust
316    /// use error_rail::prelude_async::*;
317    ///
318    /// async fn example() -> BoxedResult<i32, &'static str> {
319    ///     AsyncErrorPipeline::new(async { Err("error") })
320    ///         .with_context("operation failed")
321    ///         .finish_boxed()
322    ///         .await
323    /// }
324    /// ```
325    #[inline]
326    pub async fn finish_boxed(self) -> Result<T, Box<ComposableError<E>>> {
327        self.future.await.map_err(Box::new)
328    }
329
330    /// Maps the error type using a transformation function.
331    ///
332    /// # Arguments
333    ///
334    /// * `f` - A function that transforms `ComposableError<E>` to `ComposableError<E2>`
335    ///
336    /// # Examples
337    ///
338    /// ```rust
339    /// use error_rail::async_ext::AsyncErrorPipeline;
340    ///
341    /// let pipeline = AsyncErrorPipeline::new(async { Err::<(), _>("error") })
342    ///     .with_context("context")
343    ///     .map_err(|e| e.map_core(|_| "new error"));
344    /// ```
345    #[inline]
346    pub fn map_err<F, E2>(
347        self,
348        f: F,
349    ) -> AsyncErrorPipeline<impl Future<Output = Result<T, ComposableError<E2>>>>
350    where
351        F: FnOnce(ComposableError<E>) -> ComposableError<E2>,
352    {
353        AsyncErrorPipeline { future: async move { self.future.await.map_err(f) } }
354    }
355}