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}