error_rail/async_ext/
future_ext.rs

1//! Extension trait for `Future<Output = Result<T, E>>`.
2//!
3//! Provides `.ctx()` and `.with_ctx()` methods for futures, mirroring
4//! the sync `ResultExt` trait.
5
6use core::future::Future;
7
8use crate::traits::IntoErrorContext;
9
10use super::context_future::ContextFuture;
11
12/// Extension trait for attaching error context to async Result-returning futures.
13///
14/// This trait mirrors the sync [`ResultExt`](crate::traits::ResultExt) trait,
15/// providing the same `.ctx()` and `.with_ctx()` ergonomics for async code.
16///
17/// # Design Principles
18///
19/// - **Lazy evaluation**: Context is only evaluated when an error occurs
20/// - **Zero-cost success path**: No allocation or context computation on success
21/// - **Familiar syntax**: Same method names as sync counterparts
22///
23/// # Examples
24///
25/// ## Basic Usage
26///
27/// ```rust,no_run
28/// use error_rail::prelude_async::*;
29///
30/// #[derive(Debug)]
31/// struct User;
32///
33/// #[derive(Debug)]
34/// struct ApiError;
35///
36/// async fn fetch_from_db(_id: u64) -> Result<User, ApiError> {
37///     Err(ApiError)
38/// }
39///
40/// async fn fetch_user(id: u64) -> BoxedResult<User, ApiError> {
41///     fetch_from_db(id)
42///         .ctx("fetching user")
43///         .await
44///         .map_err(Box::new)
45/// }
46/// ```
47///
48/// ## With Lazy Context
49///
50/// ```rust,no_run
51/// use error_rail::prelude_async::*;
52///
53/// #[derive(Debug)]
54/// struct Order;
55///
56/// #[derive(Debug)]
57/// struct OrderError;
58///
59/// async fn validate_order(_order_id: u64) -> Result<Order, OrderError> {
60///     Err(OrderError)
61/// }
62///
63/// async fn process_order(order_id: u64) -> BoxedResult<Order, OrderError> {
64///     validate_order(order_id)
65///         .with_ctx(|| format!("validating order {}", order_id))
66///         .await
67///         .map_err(Box::new)
68/// }
69/// ```
70pub trait FutureResultExt<T, E>: Future<Output = Result<T, E>> + Sized {
71    /// Attaches a static context to the future's error.
72    ///
73    /// The context is converted to an error context only when the future
74    /// resolves to an error, maintaining lazy evaluation.
75    ///
76    /// # Arguments
77    ///
78    /// * `context` - Any type implementing `IntoErrorContext`
79    ///
80    /// # Examples
81    ///
82    /// ```rust
83    /// use error_rail::prelude_async::*;
84    ///
85    /// async fn example() {
86    ///     let result = async { Err::<(), _>("failed") }
87    ///         .ctx("operation failed")
88    ///         .await;
89    ///     assert!(result.is_err());
90    /// }
91    /// ```
92    fn ctx<C>(self, context: C) -> ContextFuture<Self, impl FnOnce() -> C>
93    where
94        C: IntoErrorContext,
95    {
96        self.with_ctx(move || context)
97    }
98
99    /// Attaches a lazily-evaluated context to the future's error.
100    ///
101    /// The closure is only called when the future resolves to an error,
102    /// avoiding any computation on the success path.
103    ///
104    /// # Arguments
105    ///
106    /// * `f` - A closure that produces the error context
107    ///
108    /// # Examples
109    ///
110    /// ```rust,no_run
111    /// use error_rail::prelude_async::*;
112    ///
113    /// #[derive(Debug)]
114    /// struct User;
115    ///
116    /// #[derive(Debug)]
117    /// struct ApiError;
118    ///
119    /// async fn fetch_from_db(_id: u64) -> Result<User, ApiError> {
120    ///     Err(ApiError)
121    /// }
122    ///
123    /// async fn fetch_user(id: u64) -> BoxedResult<User, ApiError> {
124    ///     fetch_from_db(id)
125    ///         .with_ctx(|| format!("fetching user {}", id))
126    ///         .await
127    ///         .map_err(Box::new)
128    /// }
129    /// ```
130    fn with_ctx<F, C>(self, f: F) -> ContextFuture<Self, F>
131    where
132        F: FnOnce() -> C,
133        C: IntoErrorContext;
134}
135
136impl<Fut, T, E> FutureResultExt<T, E> for Fut
137where
138    Fut: Future<Output = Result<T, E>>,
139{
140    #[inline]
141    fn with_ctx<F, C>(self, f: F) -> ContextFuture<Self, F>
142    where
143        F: FnOnce() -> C,
144        C: IntoErrorContext,
145    {
146        ContextFuture::new(self, f)
147    }
148}