Skip to main content

sova_core/router/
mod.rs

1mod compile;
2mod path;
3
4pub use path::{join_paths, normalize_path, to_brace_path};
5pub(crate) use compile::{compile_router, CompiledRouter};
6pub(crate) use path::to_matchit_path;
7
8use path::normalize_prefix;
9
10use crate::handler::{ErrorHandlerFn, FallibleHandler, IntoHandler};
11use crate::middleware::{IntoMwEntry, MwEntry};
12use crate::raw::{IntoRawHandler, RawHandler};
13use crate::response::Response;
14use crate::route_value::{MetaMap, RouteValue};
15use crate::state::TypeMap;
16use http::Method;
17use std::collections::HashMap;
18use std::sync::Arc;
19
20struct RouteDef {
21    method: Method,
22    /// Express-style path (`/users/:id`, `/assets/*path`).
23    path: String,
24    middleware: Vec<MwEntry>,
25    handler: FallibleHandler,
26    meta: MetaMap,
27}
28
29struct RawDef {
30    path: String,
31    handler: RawHandler,
32}
33
34/// Registered route for introspection (OpenAPI, metrics, debug).
35#[derive(Clone)]
36pub enum RouteEntry {
37    Http {
38        method: Method,
39        path: String,
40        /// Typed metadata bag (one value per `TypeId`; last insert wins).
41        meta: MetaMap,
42    },
43    Raw {
44        path: String,
45    },
46}
47
48/// Snapshot of all routes, inserted into app state at compile time.
49#[derive(Clone)]
50pub struct RouteTable(pub Vec<RouteEntry>);
51
52/// Catchers registered on one router, later scoped by mount prefix.
53pub(crate) type CatcherMap = HashMap<u16, FallibleHandler>;
54
55/// Express-style router. Return from modules and `app.mount("/blog", routes())`.
56pub struct Router {
57    routes: Vec<RouteDef>,
58    raw_routes: Vec<RawDef>,
59    middleware: Vec<MwEntry>,
60    pub(crate) state: TypeMap,
61    /// Router/app-level [`RouteValue`] defaults (overridden per route).
62    pub(crate) defaults: MetaMap,
63    /// After `get`/`post`/…, [`Self::with`] writes to the last route.
64    last_was_route: bool,
65    /// Status → handler for this router's mount prefix (empty = app root).
66    catchers: CatcherMap,
67    /// Catchers collected from mounted children: `(prefix, status → handler)`.
68    scoped_catchers: Vec<(String, CatcherMap)>,
69    error_handler: Option<ErrorHandlerFn>,
70}
71
72impl Router {
73    pub fn new() -> Self {
74        Self {
75            routes: Vec::new(),
76            raw_routes: Vec::new(),
77            middleware: Vec::new(),
78            state: TypeMap::new(),
79            defaults: MetaMap::new(),
80            last_was_route: false,
81            catchers: HashMap::new(),
82            scoped_catchers: Vec::new(),
83            error_handler: None,
84        }
85    }
86
87    /// Deep-enough clone for test dispatch without consuming the live router.
88    pub(crate) fn clone_for_compile(&self) -> Router {
89        Router {
90            routes: self
91                .routes
92                .iter()
93                .map(|r| RouteDef {
94                    method: r.method.clone(),
95                    path: r.path.clone(),
96                    middleware: r.middleware.clone(),
97                    handler: Arc::clone(&r.handler),
98                    meta: r.meta.clone(),
99                })
100                .collect(),
101            raw_routes: self
102                .raw_routes
103                .iter()
104                .map(|r| RawDef {
105                    path: r.path.clone(),
106                    handler: Arc::clone(&r.handler),
107                })
108                .collect(),
109            middleware: self.middleware.clone(),
110            state: self.state.clone_map(),
111            defaults: self.defaults.clone(),
112            last_was_route: self.last_was_route,
113            catchers: self
114                .catchers
115                .iter()
116                .map(|(s, h)| (*s, Arc::clone(h)))
117                .collect(),
118            scoped_catchers: self
119                .scoped_catchers
120                .iter()
121                .map(|(p, m)| {
122                    (
123                        p.clone(),
124                        m.iter().map(|(s, h)| (*s, Arc::clone(h))).collect(),
125                    )
126                })
127                .collect(),
128            error_handler: self.error_handler.as_ref().map(Arc::clone),
129        }
130    }
131
132    pub fn use_middleware<M>(&mut self, mw: M) -> &mut Self
133    where
134        M: IntoMwEntry,
135    {
136        self.last_was_route = false;
137        self.middleware.push(mw.into_mw_entry());
138        self
139    }
140
141    /// Attach a [`RouteValue`] to the last HTTP route, or to router defaults.
142    ///
143    /// After `get`/`post`/…, writes to that route. Otherwise writes to router/app
144    /// defaults (inherited by routes: route > router > app).
145    pub fn with<T: RouteValue>(&mut self, value: T) -> &mut Self {
146        if self.last_was_route {
147            if let Some(r) = self.routes.last_mut() {
148                r.meta.insert(value);
149            }
150        } else {
151            self.defaults.insert(value);
152        }
153        self
154    }
155
156    /// Update a [`RouteValue`] on the last route (insert `T::default()` if missing).
157    pub fn with_update<T, F>(&mut self, f: F) -> &mut Self
158    where
159        T: RouteValue + Clone + Default,
160        F: FnOnce(&mut T),
161    {
162        if let Some(r) = self.routes.last_mut() {
163            let mut v = r.meta.get::<T>().map(|a| (*a).clone()).unwrap_or_default();
164            f(&mut v);
165            r.meta.insert(v);
166            self.last_was_route = true;
167        }
168        self
169    }
170
171    /// Push middleware onto the last registered HTTP route only.
172    pub fn route_middleware<M>(&mut self, mw: M) -> &mut Self
173    where
174        M: IntoMwEntry,
175    {
176        if let Some(r) = self.routes.last_mut() {
177            let entry = mw.into_mw_entry();
178            if !r.middleware.iter().any(|e| e.name == entry.name) {
179                r.middleware.push(entry);
180            }
181        }
182        self
183    }
184
185    /// Alias for [`Self::with`] (writes to the last route when present).
186    pub fn route_meta<T: RouteValue>(&mut self, value: T) -> &mut Self {
187        if let Some(r) = self.routes.last_mut() {
188            r.meta.insert(value);
189            self.last_was_route = true;
190        } else {
191            self.defaults.insert(value);
192        }
193        self
194    }
195
196    pub fn state<T>(&mut self, value: T) -> &mut Self
197    where
198        T: Send + Sync + 'static,
199    {
200        self.state.insert(value);
201        self
202    }
203
204    /// Shared app state inserted via [`Self::state`], if present.
205    pub fn try_state<T>(&self) -> Option<std::sync::Arc<T>>
206    where
207        T: Send + Sync + 'static,
208    {
209        self.state.get::<T>()
210    }
211
212    pub fn get<H, T>(&mut self, path: &str, handler: H) -> &mut Self
213    where
214        H: IntoHandler<T>,
215    {
216        self.add(Method::GET, path, handler)
217    }
218
219    pub fn post<H, T>(&mut self, path: &str, handler: H) -> &mut Self
220    where
221        H: IntoHandler<T>,
222    {
223        self.add(Method::POST, path, handler)
224    }
225
226    pub fn put<H, T>(&mut self, path: &str, handler: H) -> &mut Self
227    where
228        H: IntoHandler<T>,
229    {
230        self.add(Method::PUT, path, handler)
231    }
232
233    pub fn patch<H, T>(&mut self, path: &str, handler: H) -> &mut Self
234    where
235        H: IntoHandler<T>,
236    {
237        self.add(Method::PATCH, path, handler)
238    }
239
240    pub fn delete<H, T>(&mut self, path: &str, handler: H) -> &mut Self
241    where
242        H: IntoHandler<T>,
243    {
244        self.add(Method::DELETE, path, handler)
245    }
246
247    /// Explicit `HEAD` handler (takes precedence over GET→strip-body).
248    pub fn head<H, T>(&mut self, path: &str, handler: H) -> &mut Self
249    where
250        H: IntoHandler<T>,
251    {
252        self.add(Method::HEAD, path, handler)
253    }
254
255    /// Explicit `OPTIONS` handler (takes precedence over auto `204 + Allow`).
256    pub fn options<H, T>(&mut self, path: &str, handler: H) -> &mut Self
257    where
258        H: IntoHandler<T>,
259    {
260        self.add(Method::OPTIONS, path, handler)
261    }
262
263    /// `GET from` → redirect to `to` with the given HTTP status (e.g. `302`, `301`, `303`).
264    ///
265    /// ```ignore
266    /// app.redirect("/health", "/healthz", 302);
267    /// app.redirect("/old", "/new", 301);
268    /// ```
269    pub fn redirect(&mut self, from: &str, to: impl Into<String>, status: u16) -> &mut Self {
270        let location = to.into();
271        self.get(from, move || {
272            let location = location.clone();
273            async move { crate::Redirect::with(status, location) }
274        })
275    }
276
277    /// Escape hatch: handle a path with a raw Hyper request/response (no Sova middleware).
278    pub fn raw<H>(&mut self, path: &str, handler: H) -> &mut Self
279    where
280        H: IntoRawHandler,
281    {
282        self.raw_routes.push(RawDef {
283            path: normalize_path(path),
284            handler: handler.into_raw_handler(),
285        });
286        self
287    }
288
289    /// Mount a child router under `prefix`.
290    ///
291    /// Bakes the child's middleware stack into its routes. Does **not** prepend
292    /// this router's middleware — the eventual root stack is applied once in
293    /// `compile_router` as an outer wrap (so App-level middleware is not doubled).
294    pub fn mount(&mut self, prefix: &str, other: Router) -> &mut Self {
295        let prefix = normalize_prefix(prefix);
296        let child_mw = other.middleware;
297
298        for mut route in other.routes {
299            route.path = join_paths(&prefix, &route.path);
300            let mut mw = child_mw.clone();
301            mw.extend(route.middleware);
302            route.middleware = mw;
303            let mut meta = other.defaults.clone();
304            meta.extend(route.meta);
305            route.meta = meta;
306            self.routes.push(route);
307        }
308        self.last_was_route = false;
309
310        for mut raw in other.raw_routes {
311            raw.path = join_paths(&prefix, &raw.path);
312            self.raw_routes.push(raw);
313        }
314
315        self.state.extend(other.state);
316
317        if !other.catchers.is_empty() {
318            self.scoped_catchers.push((prefix.clone(), other.catchers));
319        }
320        for (child_prefix, map) in other.scoped_catchers {
321            self.scoped_catchers
322                .push((join_paths(&prefix, &child_prefix), map));
323        }
324        if self.error_handler.is_none() {
325            self.error_handler = other.error_handler;
326        }
327
328        self
329    }
330
331    /// Sugar over [`Self::mount`]: build a child router in a closure.
332    pub fn group<F>(&mut self, prefix: &str, f: F) -> &mut Self
333    where
334        F: FnOnce(&mut Router),
335    {
336        let mut child = Router::new();
337        f(&mut child);
338        self.mount(prefix, child)
339    }
340
341    /// Register a catcher for HTTP `status` in this router's mount scope.
342    ///
343    /// At dispatch, the catcher with the **longest** matching prefix wins.
344    /// `not_found` is sugar for `catch(404, …)`.
345    pub fn catch<H, T>(&mut self, status: u16, handler: H) -> &mut Self
346    where
347        H: IntoHandler<T>,
348    {
349        self.last_was_route = false;
350        self.catchers.insert(status, handler.into_handler());
351        self
352    }
353
354    /// Sugar for [`Self::catch`]`(404, handler)`.
355    pub fn not_found<H, T>(&mut self, handler: H) -> &mut Self
356    where
357        H: IntoHandler<T>,
358    {
359        self.catch(404, handler)
360    }
361
362    /// Called when a leaf handler returns `Err`. Request is already consumed.
363    pub fn error_handler<F, Fut>(&mut self, f: F) -> &mut Self
364    where
365        F: Fn(crate::error::Error) -> Fut + Send + Sync + 'static,
366        Fut: std::future::Future<Output = Response> + Send + 'static,
367    {
368        self.error_handler = Some(Arc::new(move |err| Box::pin(f(err))));
369        self
370    }
371
372    /// Full introspection: HTTP routes and raw paths.
373    pub fn route_entries(&self) -> Vec<RouteEntry> {
374        let mut out = Vec::new();
375        for r in &self.routes {
376            let mut meta = self.defaults.clone();
377            meta.extend(r.meta.clone());
378            out.push(RouteEntry::Http {
379                method: r.method.clone(),
380                path: r.path.clone(),
381                meta,
382            });
383        }
384        for r in &self.raw_routes {
385            out.push(RouteEntry::Raw {
386                path: r.path.clone(),
387            });
388        }
389        out
390    }
391
392    /// Run [`RouteValue::check`] for router defaults and every route.
393    pub(crate) fn check_route_values(
394        &self,
395        state: &crate::state::StateMap,
396        installed_plugins: &std::collections::HashSet<&'static str>,
397    ) -> Result<(), String> {
398        use crate::route_value::BuildCtx;
399        let ctx = BuildCtx {
400            state,
401            installed_plugins,
402            route_path: "<defaults>",
403            route_method: None,
404        };
405        self.defaults.check_all(&ctx)?;
406        for r in &self.routes {
407            let mut meta = self.defaults.clone();
408            meta.extend(r.meta.clone());
409            let ctx = BuildCtx {
410                state,
411                installed_plugins,
412                route_path: &r.path,
413                route_method: Some(&r.method),
414            };
415            meta.check_all(&ctx)?;
416        }
417        Ok(())
418    }
419
420    /// Human-readable route map (method, path, middleware names).
421    pub fn explain(&self) -> String {
422        use std::fmt::Write;
423        let mut out = String::new();
424        let _ = writeln!(out, "root_middleware: [{}]", format_mw_names(&self.middleware));
425        let _ = writeln!(out, "error_handler: {}", self.error_handler.is_some());
426        let mut catch_lines = Vec::new();
427        if !self.catchers.is_empty() {
428            let mut codes: Vec<_> = self.catchers.keys().copied().collect();
429            codes.sort_unstable();
430            catch_lines.push(format!("/ → {:?}", codes));
431        }
432        for (prefix, map) in &self.scoped_catchers {
433            let mut codes: Vec<_> = map.keys().copied().collect();
434            codes.sort_unstable();
435            let p = if prefix.is_empty() { "/" } else { prefix.as_str() };
436            catch_lines.push(format!("{p} → {:?}", codes));
437        }
438        let _ = writeln!(out, "catchers: [{}]", catch_lines.join("; "));
439        if !self.defaults.is_empty() {
440            let _ = writeln!(out, "defaults: [{}]", self.defaults.labels().join(" "));
441        }
442        for r in &self.routes {
443            let labels = r.meta.labels();
444            if labels.is_empty() {
445                let _ = writeln!(
446                    out,
447                    "{} {} mw=[{}]",
448                    r.method,
449                    r.path,
450                    format_mw_names(&r.middleware)
451                );
452            } else {
453                let _ = writeln!(
454                    out,
455                    "{} {} mw=[{}] {}",
456                    r.method,
457                    r.path,
458                    format_mw_names(&r.middleware),
459                    labels.join(" ")
460                );
461            }
462        }
463        for r in &self.raw_routes {
464            let _ = writeln!(out, "RAW {} mw=[]", r.path);
465        }
466        out
467    }
468
469    fn add<H, T>(&mut self, method: Method, path: &str, handler: H) -> &mut Self
470    where
471        H: IntoHandler<T>,
472    {
473        self.routes.push(RouteDef {
474            method,
475            path: normalize_path(path),
476            // Module stack is baked at `mount`, not here — keeps root mw outer-only.
477            middleware: Vec::new(),
478            handler: handler.into_handler(),
479            meta: MetaMap::new(),
480        });
481        self.last_was_route = true;
482        self
483    }
484}
485
486impl Default for Router {
487    fn default() -> Self {
488        Self::new()
489    }
490}
491
492fn format_mw_names(entries: &[MwEntry]) -> String {
493    entries
494        .iter()
495        .map(|e| e.name.as_str())
496        .collect::<Vec<_>>()
497        .join(", ")
498}