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}