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