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}