Skip to main content

a3s_boot/pipeline/
interceptor.rs

1use super::ExecutionContext;
2use crate::{BootError, BootResponse, BoxFuture, Result};
3use std::future::Future;
4use std::sync::atomic::{AtomicBool, Ordering};
5use std::sync::Arc;
6
7/// Reusable access to the next handler in an interceptor chain.
8///
9/// This is the Rust equivalent of Nest's `CallHandler`. Calling [`handle`](Self::handle)
10/// runs the remaining interceptors, pipes, validation, and handler. The handle is
11/// reusable so an interceptor can deliberately retry the downstream pipeline
12/// sequentially. Concurrent calls are rejected because they would share one
13/// request-scoped provider context.
14pub struct CallHandler<'a, T = BootResponse> {
15    call: Arc<dyn Fn() -> BoxFuture<'a, Result<T>> + Send + Sync + 'a>,
16    running: Arc<AtomicBool>,
17}
18
19impl<'a, T> Clone for CallHandler<'a, T> {
20    fn clone(&self) -> Self {
21        Self {
22            call: Arc::clone(&self.call),
23            running: Arc::clone(&self.running),
24        }
25    }
26}
27
28impl<'a, T> CallHandler<'a, T>
29where
30    T: Send + 'a,
31{
32    /// Build a call handler from a reusable async function.
33    ///
34    /// Frameworks normally provide the handler to an interceptor. This
35    /// constructor is public so interceptors and combinators can be tested in
36    /// isolation.
37    pub fn from_fn<F, Fut>(call: F) -> Self
38    where
39        F: Fn() -> Fut + Send + Sync + 'a,
40        Fut: Future<Output = Result<T>> + Send + 'a,
41    {
42        Self {
43            call: Arc::new(move || Box::pin(call())),
44            running: Arc::new(AtomicBool::new(false)),
45        }
46    }
47
48    /// Run the remaining interceptor chain and underlying handler once.
49    ///
50    /// A completed or cancelled call releases the handler for a later retry.
51    /// Starting overlapping calls returns [`BootError::Internal`].
52    pub fn handle(&self) -> BoxFuture<'a, Result<T>> {
53        let call = Arc::clone(&self.call);
54        let running = Arc::clone(&self.running);
55        Box::pin(async move {
56            if running
57                .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
58                .is_err()
59            {
60                return Err(BootError::Internal(
61                    "call handler is already running".to_string(),
62                ));
63            }
64
65            let _reset = CallHandlerReset {
66                running: Arc::clone(&running),
67            };
68            call().await
69        })
70    }
71}
72
73struct CallHandlerReset {
74    running: Arc<AtomicBool>,
75}
76
77impl Drop for CallHandlerReset {
78    fn drop(&mut self) {
79        self.running.store(false, Ordering::Release);
80    }
81}
82
83/// Runs around the handler for cross-cutting behavior.
84pub trait Interceptor: Send + Sync + 'static {
85    /// Run around the remaining HTTP pipeline.
86    ///
87    /// Override this method to catch or replace downstream errors, retry the
88    /// handler, apply a timeout, or return a response without calling `next`.
89    /// The default implementation preserves the legacy `before`,
90    /// `short_circuit`, and `after` hook behavior.
91    fn intercept<'a>(
92        &'a self,
93        context: ExecutionContext,
94        next: CallHandler<'a>,
95    ) -> BoxFuture<'a, Result<BootResponse>> {
96        Box::pin(async move {
97            self.before(context.clone()).await?;
98
99            if let Some(response) = self.short_circuit(context.clone()).await? {
100                return Ok(response);
101            }
102
103            let response = next.handle().await?;
104            self.after(context, response).await
105        })
106    }
107
108    fn before(&self, _context: ExecutionContext) -> BoxFuture<'static, Result<()>> {
109        Box::pin(async { Ok(()) })
110    }
111
112    fn short_circuit(
113        &self,
114        _context: ExecutionContext,
115    ) -> BoxFuture<'static, Result<Option<BootResponse>>> {
116        Box::pin(async { Ok(None) })
117    }
118
119    fn after(
120        &self,
121        _context: ExecutionContext,
122        response: BootResponse,
123    ) -> BoxFuture<'static, Result<BootResponse>> {
124        Box::pin(async move { Ok(response) })
125    }
126}
127
128impl<F, Fut> Interceptor for F
129where
130    F: Fn(ExecutionContext) -> Fut + Send + Sync + 'static,
131    Fut: std::future::Future<Output = Result<()>> + Send + 'static,
132{
133    fn before(&self, context: ExecutionContext) -> BoxFuture<'static, Result<()>> {
134        Box::pin(self(context))
135    }
136}
137
138/// Protocol-neutral observer that can run around HTTP, WebSocket, or transport handlers.
139pub trait ExecutionInterceptor: Send + Sync + 'static {
140    fn before(&self, _context: ExecutionContext) -> BoxFuture<'static, Result<()>> {
141        Box::pin(async { Ok(()) })
142    }
143
144    fn after(&self, _context: ExecutionContext) -> BoxFuture<'static, Result<()>> {
145        Box::pin(async { Ok(()) })
146    }
147}
148
149impl<F, Fut> ExecutionInterceptor for F
150where
151    F: Fn(ExecutionContext) -> Fut + Send + Sync + 'static,
152    Fut: std::future::Future<Output = Result<()>> + Send + 'static,
153{
154    fn before(&self, context: ExecutionContext) -> BoxFuture<'static, Result<()>> {
155        Box::pin(self(context))
156    }
157}
158
159impl ExecutionInterceptor for Arc<dyn ExecutionInterceptor> {
160    fn before(&self, context: ExecutionContext) -> BoxFuture<'static, Result<()>> {
161        self.as_ref().before(context)
162    }
163
164    fn after(&self, context: ExecutionContext) -> BoxFuture<'static, Result<()>> {
165        self.as_ref().after(context)
166    }
167}
168
169pub(crate) struct ExecutionInterceptorAdapter<I> {
170    inner: I,
171}
172
173impl<I> ExecutionInterceptorAdapter<I> {
174    pub(crate) fn new(inner: I) -> Self {
175        Self { inner }
176    }
177}
178
179impl<I> Interceptor for ExecutionInterceptorAdapter<I>
180where
181    I: ExecutionInterceptor,
182{
183    fn before(&self, context: ExecutionContext) -> BoxFuture<'static, Result<()>> {
184        self.inner.before(context)
185    }
186
187    fn after(
188        &self,
189        context: ExecutionContext,
190        response: BootResponse,
191    ) -> BoxFuture<'static, Result<BootResponse>> {
192        let future = self.inner.after(context);
193        Box::pin(async move {
194            future.await?;
195            Ok(response)
196        })
197    }
198}