Skip to main content

churust_core/
pipeline.rs

1//! The request pipeline: [`Middleware`], the [`Phase`] ordering, the [`Next`]
2//! continuation, and the [`Endpoint`] terminal.
3//!
4//! Middleware runs as an onion: each layer may inspect/modify the [`Call`] on
5//! the way in, calls `next.run(call)` to proceed, and may post-process the
6//! [`Response`] on the way out — or short-circuit by returning a response
7//! without calling `next`. Layers execute in [`Phase`] order; the matched route
8//! handler (the [`Endpoint`]) sits at the center.
9
10use crate::call::Call;
11use crate::response::Response;
12use async_trait::async_trait;
13use std::collections::VecDeque;
14use std::future::Future;
15use std::pin::Pin;
16use std::sync::Arc;
17
18/// The ordered insertion points for middleware (Ktor-style).
19///
20/// Lower-ordered variants run further "outside" in the onion — first on the way
21/// in, last on the way out. The [`Ord`] derivation follows declaration order,
22/// and [`AppBuilder::build`](crate::AppBuilder::build) sorts installed
23/// middleware by phase (stably, so install order is preserved within a phase).
24///
25/// ```
26/// use churust_core::Phase;
27/// assert!(Phase::Setup < Phase::Monitoring);
28/// assert!(Phase::Monitoring < Phase::Plugins);
29/// assert!(Phase::Plugins < Phase::Call);
30/// assert!(Phase::Call < Phase::Fallback);
31/// ```
32#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
33pub enum Phase {
34    /// Earliest phase, for foundational setup that must wrap everything else.
35    Setup,
36    /// Observability concerns (logging, metrics, tracing).
37    Monitoring,
38    /// Default phase for installed plugins/middleware.
39    Plugins,
40    /// Closest to the route handler (e.g. per-call last-mile logic).
41    Call,
42    /// Latest phase, for catch-all fallbacks.
43    Fallback,
44}
45
46/// The terminal of the pipeline: a function that routes the call and runs the
47/// matched handler.
48///
49/// It returns a `'static` boxed future because the [`Call`] is owned (moved in).
50/// The framework constructs the endpoint internally; middleware reaches it by
51/// exhausting the [`Next`] chain.
52pub type Endpoint =
53    Arc<dyn Fn(Call) -> Pin<Box<dyn Future<Output = Response> + Send + 'static>> + Send + Sync>;
54
55/// A pipeline interceptor — one layer of the onion.
56///
57/// An implementor owns the [`Call`], may inspect or modify it, calls
58/// `next.run(call)` to proceed inward, and may post-process the resulting
59/// [`Response`]. Returning a response *without* calling `next` short-circuits
60/// the remaining layers and the handler. Install a middleware with
61/// [`AppBuilder::add_middleware`](crate::AppBuilder::add_middleware) or, for a
62/// specific [`Phase`], [`AppBuilder::add_middleware_in`](crate::AppBuilder::add_middleware_in).
63///
64/// ```
65/// use churust_core::{Call, Churust, Middleware, Next, Response, TestClient};
66/// use churust_core::pipeline::Phase;
67/// use async_trait::async_trait;
68/// use std::sync::Arc;
69/// use http::{header::HeaderName, HeaderValue};
70///
71/// struct Stamp;
72/// #[async_trait]
73/// impl Middleware for Stamp {
74///     async fn handle(&self, call: Call, next: Next) -> Response {
75///         let mut res = next.run(call).await; // proceed inward
76///         res.headers.insert(HeaderName::from_static("x-stamp"), HeaderValue::from_static("1"));
77///         res // post-process on the way out
78///     }
79/// }
80/// # tokio::runtime::Runtime::new().unwrap().block_on(async {
81/// let mut builder = Churust::server();
82/// builder.add_middleware_in(Phase::Plugins, Arc::new(Stamp));
83/// let app = builder.routing(|r| { r.get("/", |_c: Call| async { "ok" }); }).build();
84/// let res = TestClient::new(app).get("/").send().await;
85/// assert_eq!(res.header("x-stamp"), Some("1"));
86/// # });
87/// ```
88#[async_trait]
89pub trait Middleware: Send + Sync + 'static {
90    /// Handle the call: optionally inspect/modify it, call `next.run(call)` to
91    /// proceed, and return the (possibly post-processed) response.
92    async fn handle(&self, call: Call, next: Next) -> Response;
93}
94
95/// The remaining middleware chain plus the terminal [`Endpoint`].
96///
97/// It is owned (via `Arc` clones) and carries no lifetime parameters, so it
98/// threads cleanly through `async_trait` futures. A [`Middleware`] advances the
99/// pipeline by calling [`Next::run`].
100pub struct Next {
101    chain: VecDeque<Arc<dyn Middleware>>,
102    endpoint: Endpoint,
103}
104
105impl Next {
106    pub(crate) fn new(chain: VecDeque<Arc<dyn Middleware>>, endpoint: Endpoint) -> Self {
107        Self { chain, endpoint }
108    }
109
110    /// Run the next middleware in the chain, or the [`Endpoint`] if the chain is
111    /// exhausted, consuming `self`. Call this from a [`Middleware`] to proceed
112    /// inward; not calling it short-circuits the rest of the pipeline.
113    pub async fn run(mut self, call: Call) -> Response {
114        match self.chain.pop_front() {
115            Some(mw) => mw.handle(call, self).await,
116            None => (self.endpoint)(call).await,
117        }
118    }
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124    use crate::response::IntoResponse;
125
126    #[test]
127    fn phases_are_ordered() {
128        assert!(Phase::Setup < Phase::Monitoring);
129        assert!(Phase::Monitoring < Phase::Plugins);
130        assert!(Phase::Plugins < Phase::Call);
131        assert!(Phase::Call < Phase::Fallback);
132    }
133    use bytes::Bytes;
134    use http::header::HeaderName;
135    use http::{HeaderMap, HeaderValue, Method, StatusCode, Uri};
136
137    fn sample_call() -> Call {
138        Call::new(
139            Method::GET,
140            "/".parse::<Uri>().unwrap(),
141            HeaderMap::new(),
142            Bytes::new(),
143        )
144    }
145
146    fn endpoint() -> Endpoint {
147        Arc::new(|_call: Call| Box::pin(async { "inner".into_response() }) as _)
148    }
149
150    struct AddHeader;
151    #[async_trait]
152    impl Middleware for AddHeader {
153        async fn handle(&self, call: Call, next: Next) -> Response {
154            let mut res = next.run(call).await;
155            res.headers.insert(
156                HeaderName::from_static("x-mw"),
157                HeaderValue::from_static("1"),
158            );
159            res
160        }
161    }
162
163    struct ShortCircuit;
164    #[async_trait]
165    impl Middleware for ShortCircuit {
166        async fn handle(&self, _call: Call, _next: Next) -> Response {
167            Response::new(StatusCode::FORBIDDEN)
168        }
169    }
170
171    #[tokio::test]
172    async fn runs_endpoint_when_chain_empty() {
173        let next = Next::new(VecDeque::new(), endpoint());
174        let res = next.run(sample_call()).await;
175        assert_eq!(res.body, Bytes::from("inner"));
176    }
177
178    #[tokio::test]
179    async fn middleware_post_processes_response() {
180        let mut chain: VecDeque<Arc<dyn Middleware>> = VecDeque::new();
181        chain.push_back(Arc::new(AddHeader));
182        let res = Next::new(chain, endpoint()).run(sample_call()).await;
183        assert_eq!(res.headers.get("x-mw").unwrap(), "1");
184        assert_eq!(res.body, Bytes::from("inner"));
185    }
186
187    #[tokio::test]
188    async fn middleware_can_short_circuit() {
189        let mut chain: VecDeque<Arc<dyn Middleware>> = VecDeque::new();
190        chain.push_back(Arc::new(ShortCircuit));
191        chain.push_back(Arc::new(AddHeader)); // should never run
192        let res = Next::new(chain, endpoint()).run(sample_call()).await;
193        assert_eq!(res.status, StatusCode::FORBIDDEN);
194        assert!(res.headers.get("x-mw").is_none());
195    }
196}