Skip to main content

sova_core/
middleware.rs

1use crate::handler::{BoxFuture, Handler};
2use crate::request::Request;
3use crate::response::Response;
4use std::future::Future;
5use std::sync::Arc;
6
7/// Call the rest of the middleware / handler chain.
8pub type Next = Box<dyn FnOnce(Request) -> BoxFuture<Response> + Send>;
9
10/// Type-erased middleware: `(Request, Next) -> Response`.
11pub type Middleware = Arc<dyn Fn(Request, Next) -> BoxFuture<Response> + Send + Sync>;
12
13/// Named middleware entry used for [`crate::Router::explain`].
14#[derive(Clone)]
15pub struct MwEntry {
16    pub name: String,
17    pub mw: Middleware,
18}
19
20/// Wrap middleware with an explicit explain label (e.g. `named("auth", …)`).
21pub fn named(name: impl Into<String>, mw: impl IntoMiddleware) -> MwEntry {
22    MwEntry {
23        name: name.into(),
24        mw: mw.into_middleware(),
25    }
26}
27
28pub(crate) fn abbreviate_type_name(full: &str) -> String {
29    let trimmed = full
30        .trim_end_matches(">::{{closure}}")
31        .trim_end_matches("::{{closure}}")
32        .trim_end_matches("{{closure}}");
33    let base = trimmed.rsplit("::").next().unwrap_or(trimmed);
34    if base.is_empty() || base == "{{closure}}" {
35        "closure".into()
36    } else {
37        base.to_string()
38    }
39}
40
41pub trait IntoMiddleware {
42    fn into_middleware(self) -> Middleware;
43}
44
45/// Convert into a named [`MwEntry`] (explicit name via [`named`], else type name).
46pub trait IntoMwEntry {
47    fn into_mw_entry(self) -> MwEntry;
48}
49
50impl<F, Fut> IntoMiddleware for F
51where
52    F: Fn(Request, Next) -> Fut + Send + Sync + 'static,
53    Fut: Future<Output = Response> + Send + 'static,
54{
55    fn into_middleware(self) -> Middleware {
56        Arc::new(move |req, next| Box::pin(self(req, next)))
57    }
58}
59
60impl IntoMiddleware for Middleware {
61    fn into_middleware(self) -> Middleware {
62        self
63    }
64}
65
66impl IntoMwEntry for MwEntry {
67    fn into_mw_entry(self) -> MwEntry {
68        self
69    }
70}
71
72impl IntoMwEntry for Middleware {
73    fn into_mw_entry(self) -> MwEntry {
74        MwEntry {
75            name: "mw".into(),
76            mw: self,
77        }
78    }
79}
80
81impl<F, Fut> IntoMwEntry for F
82where
83    F: Fn(Request, Next) -> Fut + Send + Sync + 'static,
84    Fut: Future<Output = Response> + Send + 'static,
85{
86    fn into_mw_entry(self) -> MwEntry {
87        MwEntry {
88            name: abbreviate_type_name(std::any::type_name::<F>()),
89            mw: self.into_middleware(),
90        }
91    }
92}
93
94/// Middleware with owned state — hides the `Arc::clone` dance.
95///
96/// Prefer this over capturing many fields and cloning them into each future.
97/// If a closure needs more than one `.clone()`, the state should be one `Arc`
98/// (this helper) or process-lifetime `&'static` via [`with_leaked`].
99pub fn with_state<S, F, Fut>(state: S, f: F) -> Middleware
100where
101    S: Send + Sync + 'static,
102    F: Fn(Arc<S>, Request, Next) -> Fut + Send + Sync + 'static,
103    Fut: Future<Output = Response> + Send + 'static,
104{
105    let state = Arc::new(state);
106    Arc::new(move |req, next| Box::pin(f(Arc::clone(&state), req, next)))
107}
108
109/// Immutable plugin config that lives for the process.
110///
111/// # ponytail
112/// One-shot `Box::leak` at install — no Arc atomics on the hot path.
113pub fn with_leaked<S, F, Fut>(state: S, f: F) -> Middleware
114where
115    S: Send + Sync + 'static,
116    F: Fn(&'static S, Request, Next) -> Fut + Send + Sync + 'static,
117    Fut: Future<Output = Response> + Send + 'static,
118{
119    let state: &'static S = Box::leak(Box::new(state));
120    Arc::new(move |req, next| Box::pin(f(state, req, next)))
121}
122
123/// Build Express-style onion once; returns a reusable [`Handler`].
124pub fn build_chain(middleware: &[Middleware], handler: Handler) -> Handler {
125    let mut next = handler;
126
127    for mw in middleware.iter().rev() {
128        let mw = Arc::clone(mw);
129        let inner = next;
130        next = Arc::new(move |req| {
131            let inner = Arc::clone(&inner);
132            let mw = Arc::clone(&mw);
133            mw(
134                req,
135                Box::new(move |r| {
136                    let inner = Arc::clone(&inner);
137                    inner(r)
138                }),
139            )
140        });
141    }
142
143    next
144}
145
146pub(crate) fn chain_from_entries(entries: &[MwEntry], handler: Handler) -> Handler {
147    let mws: Vec<Middleware> = entries.iter().map(|e| Arc::clone(&e.mw)).collect();
148    build_chain(&mws, handler)
149}
150
151/// Request logger (`method`, `path`, `status`, `latency_ms`, optional `request_id`).
152pub fn logger() -> MwEntry {
153    named("logger", |req: Request, next: Next| async move {
154        let method = req.method.as_str().to_string();
155        let path = req.path.clone();
156        let request_id = req
157            .get::<crate::request_id::RequestId>()
158            .map(|r| r.0.clone())
159            .unwrap_or_default();
160        let start = std::time::Instant::now();
161        let res = next(req).await;
162        let status = res.status_code().as_u16();
163        let latency_ms = start.elapsed().as_millis() as u64;
164        if request_id.is_empty() {
165            tracing::info!(
166                method = %method,
167                path = %path,
168                status,
169                latency_ms,
170                "request"
171            );
172        } else {
173            tracing::info!(
174                request_id = %request_id,
175                method = %method,
176                path = %path,
177                status,
178                latency_ms,
179                "request"
180            );
181        }
182        res
183    })
184}
185
186#[cfg(test)]
187mod tests {
188    use super::*;
189    use http::Method;
190
191    fn ok_handler() -> Handler {
192        Arc::new(|_req: Request| Box::pin(async { Response::text("ok") }))
193    }
194
195    #[tokio::test]
196    async fn onion_order() {
197        let leaf = ok_handler();
198        let outer = |req: Request, next: Next| async move {
199            let mut res = next(req).await;
200            res = res.header("x-outer", "1");
201            res
202        };
203        let inner = |req: Request, next: Next| async move {
204            let mut res = next(req).await;
205            res = res.header("x-inner", "1");
206            res
207        };
208        let chain = build_chain(
209            &[outer.into_middleware(), inner.into_middleware()],
210            leaf,
211        );
212        let res = chain(Request::new(Method::GET, "/")).await;
213        assert_eq!(res.body_bytes(), Some(b"ok".as_slice()));
214        assert_eq!(res.headers.get("x-outer").map(|v| v.to_str().unwrap()), Some("1"));
215        assert_eq!(res.headers.get("x-inner").map(|v| v.to_str().unwrap()), Some("1"));
216    }
217
218    #[tokio::test]
219    async fn with_state_runs() {
220        let leaf = ok_handler();
221        let mw = with_state(7u32, |n, req, next| async move {
222            assert_eq!(*n, 7);
223            next(req).await
224        });
225        let chain = build_chain(&[mw], leaf);
226        assert_eq!(
227            chain(Request::new(Method::GET, "/")).await.body_bytes(),
228            Some(b"ok".as_slice())
229        );
230    }
231
232    #[test]
233    fn abbreviate_closure() {
234        let n = abbreviate_type_name("foo::bar::{{closure}}");
235        assert_eq!(n, "bar");
236    }
237}