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;
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 evaluated when an error occurs (lazy evaluation).
120    ///
121    /// # Arguments
122    ///
123    /// * `context` - Any type implementing `IntoErrorContext`
124    ///
125    /// # Examples
126    ///
127    /// ```rust
128    /// use error_rail::async_ext::AsyncErrorPipeline;
129    ///
130    /// let pipeline = AsyncErrorPipeline::new(async { Err::<(), _>("error") })
131    ///     .with_context("operation context");
132    /// ```
133    #[inline]
134    pub fn with_context<C>(
135        self,
136        context: C,
137    ) -> AsyncErrorPipeline<impl Future<Output = Result<T, ComposableError<E>>>>
138    where
139        C: IntoErrorContext,
140    {
141        AsyncErrorPipeline { future: self.future.ctx(context) }
142    }
143
144    /// Adds a lazily-evaluated context using a closure.
145    ///
146    /// The closure is only called when an error occurs, avoiding
147    /// any computation on the success path.
148    ///
149    /// # Arguments
150    ///
151    /// * `f` - A closure that produces the error context
152    ///
153    /// # Examples
154    ///
155    /// ```rust,no_run
156    /// use error_rail::async_ext::AsyncErrorPipeline;
157    ///
158    /// #[derive(Debug)]
159    /// struct User;
160    ///
161    /// #[derive(Debug)]
162    /// struct ApiError;
163    ///
164    /// async fn fetch_user(_id: u64) -> Result<User, ApiError> {
165    ///     Err(ApiError)
166    /// }
167    ///
168    /// let id = 42u64;
169    /// let _pipeline = AsyncErrorPipeline::new(fetch_user(id))
170    ///     .with_context_fn(|| format!("user_id: {}", id));
171    /// ```
172    #[inline]
173    pub fn with_context_fn<F, C>(
174        self,
175        f: F,
176    ) -> AsyncErrorPipeline<impl Future<Output = Result<T, ComposableError<E>>>>
177    where
178        F: FnOnce() -> C,
179        C: IntoErrorContext,
180    {
181        AsyncErrorPipeline { future: self.future.with_ctx(f) }
182    }
183}
184
185impl<Fut, T, E> AsyncErrorPipeline<Fut>
186where
187    Fut: Future<Output = Result<T, ComposableError<E>>>,
188{
189    /// Completes the pipeline and returns a boxed error result.
190    ///
191    /// This is the recommended way to finish a pipeline when returning
192    /// from a function, as it provides minimal stack footprint.
193    ///
194    /// # Examples
195    ///
196    /// ```rust
197    /// use error_rail::prelude_async::*;
198    ///
199    /// async fn example() -> BoxedResult<i32, &'static str> {
200    ///     AsyncErrorPipeline::new(async { Err("error") })
201    ///         .with_context("operation failed")
202    ///         .finish_boxed()
203    ///         .await
204    /// }
205    /// ```
206    #[inline]
207    pub async fn finish_boxed(self) -> Result<T, Box<ComposableError<E>>> {
208        self.future.await.map_err(Box::new)
209    }
210
211    /// Maps the error type using a transformation function.
212    ///
213    /// # Arguments
214    ///
215    /// * `f` - A function that transforms `ComposableError<E>` to `ComposableError<E2>`
216    ///
217    /// # Examples
218    ///
219    /// ```rust
220    /// use error_rail::async_ext::AsyncErrorPipeline;
221    ///
222    /// let pipeline = AsyncErrorPipeline::new(async { Err::<(), _>("error") })
223    ///     .with_context("context")
224    ///     .map_err(|e| e.map_core(|_| "new error"));
225    /// ```
226    #[inline]
227    pub fn map_err<F, E2>(
228        self,
229        f: F,
230    ) -> AsyncErrorPipeline<impl Future<Output = Result<T, ComposableError<E2>>>>
231    where
232        F: FnOnce(ComposableError<E>) -> ComposableError<E2>,
233    {
234        AsyncErrorPipeline { future: async move { self.future.await.map_err(f) } }
235    }
236}