error_rail/async_ext/
context_future.rs

1//! Future wrappers for lazy context evaluation.
2//!
3//! This module provides `ContextFuture`, which wraps a `Future<Output = Result<T, E>>`
4//! and attaches error context only when the future resolves to an error.
5
6use core::future::Future;
7use core::pin::Pin;
8use core::task::{Context, Poll};
9
10use futures_core::future::FusedFuture;
11
12use pin_project_lite::pin_project;
13
14use crate::traits::IntoErrorContext;
15use crate::types::ComposableError;
16
17pin_project! {
18    /// A Future wrapper that attaches error context lazily.
19    ///
20    /// The context is only evaluated when the inner future resolves to an error,
21    /// maintaining zero-cost on the success path.
22    ///
23    /// # Cancel Safety
24    ///
25    /// `ContextFuture` is cancel-safe if the inner future is cancel-safe.
26    /// The `context_fn` is only called when `poll` returns `Poll::Ready(Err(_))`.
27    ///
28    /// # Examples
29    ///
30    /// ```rust
31    /// use error_rail::prelude_async::*;
32    ///
33    /// async fn example() -> BoxedResult<i32, &'static str> {
34    ///     async { Err("failed") }
35    ///         .ctx("operation context")
36    ///         .await
37    ///         .map_err(Box::new)
38    /// }
39    /// ```
40    #[must_use = "futures do nothing unless polled"]
41    pub struct ContextFuture<Fut, F> {
42        #[pin]
43        future: Fut,
44        context_fn: Option<F>,
45    }
46}
47
48impl<Fut, F> ContextFuture<Fut, F> {
49    /// Creates a new `ContextFuture` with the given future and context generator.
50    #[inline]
51    pub fn new(future: Fut, context_fn: F) -> Self {
52        Self { future, context_fn: Some(context_fn) }
53    }
54}
55
56impl<Fut, F, C, T, E> Future for ContextFuture<Fut, F>
57where
58    Fut: Future<Output = Result<T, E>>,
59    F: FnOnce() -> C,
60    C: IntoErrorContext,
61{
62    type Output = Result<T, ComposableError<E>>;
63
64    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
65        let this = self.project();
66
67        this.future.poll(cx).map(|res| {
68            res.map_err(|err| {
69                let context_fn = this
70                    .context_fn
71                    .take()
72                    .expect("ContextFuture polled after completion; this is a bug");
73                ComposableError::new(err).with_context(context_fn())
74            })
75        })
76    }
77}
78
79impl<Fut, F, C, T, E> FusedFuture for ContextFuture<Fut, F>
80where
81    Fut: FusedFuture<Output = Result<T, E>>,
82    F: FnOnce() -> C,
83    C: IntoErrorContext,
84{
85    fn is_terminated(&self) -> bool {
86        // Also check context_fn since it's taken on error completion
87        self.context_fn.is_none() || self.future.is_terminated()
88    }
89}