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    /// `GET from` → redirect to `to` with the given HTTP status (e.g. `302`, `301`, `303`).
248    ///
249    /// ```ignore
250    /// app.redirect("/health", "/healthz", 302);
251    /// app.redirect("/old", "/new", 301);
252    /// ```
253    pub fn redirect(&mut self, from: &str, to: impl Into<String>, status: u16) -> &mut Self {
254        let location = to.into();
255        self.get(from, move || {
256            let location = location.clone();
257            async move { crate::Redirect::with(status, location) }
258        })
259    }
260
261    /// Escape hatch: handle a path with a raw Hyper request/response (no Sova middleware).
262    pub fn raw<H>(&mut self, path: &str, handler: H) -> &mut Self
263    where
264        H: IntoRawHandler,
265    {
266        self.raw_routes.push(RawDef {
267            path: normalize_path(path),
268            handler: handler.into_raw_handler(),
269        });
270        self
271    }
272
273    /// Mount a child router under `prefix`.
274    ///
275    /// Bakes the child's middleware stack into its routes. Does **not** prepend
276    /// this router's middleware — the eventual root stack is applied once in
277    /// `compile_router` as an outer wrap (so App-level middleware is not doubled).
278    pub fn mount(&mut self, prefix: &str, other: Router) -> &mut Self {
279        let prefix = normalize_prefix(prefix);
280        let child_mw = other.middleware;
281
282        for mut route in other.routes {
283            route.path = join_paths(&prefix, &route.path);
284            let mut mw = child_mw.clone();
285            mw.extend(route.middleware);
286            route.middleware = mw;
287            let mut meta = other.defaults.clone();
288            meta.extend(route.meta);
289            route.meta = meta;
290            self.routes.push(route);
291        }
292        self.last_was_route = false;
293
294        for mut raw in other.raw_routes {
295            raw.path = join_paths(&prefix, &raw.path);
296            self.raw_routes.push(raw);
297        }
298
299        self.state.extend(other.state);
300
301        if !other.catchers.is_empty() {
302            self.scoped_catchers.push((prefix.clone(), other.catchers));
303        }
304        for (child_prefix, map) in other.scoped_catchers {
305            self.scoped_catchers
306                .push((join_paths(&prefix, &child_prefix), map));
307        }
308        if self.error_handler.is_none() {
309            self.error_handler = other.error_handler;
310        }
311
312        self
313    }
314
315    /// Sugar over [`Self::mount`]: build a child router in a closure.
316    pub fn group<F>(&mut self, prefix: &str, f: F) -> &mut Self
317    where
318        F: FnOnce(&mut Router),
319    {
320        let mut child = Router::new();
321        f(&mut child);
322        self.mount(prefix, child)
323    }
324
325    /// Register a catcher for HTTP `status` in this router's mount scope.
326    ///
327    /// At dispatch, the catcher with the **longest** matching prefix wins.
328    /// `not_found` is sugar for `catch(404, …)`.
329    pub fn catch<H, T>(&mut self, status: u16, handler: H) -> &mut Self
330    where
331        H: IntoHandler<T>,
332    {
333        self.last_was_route = false;
334        self.catchers.insert(status, handler.into_handler());
335        self
336    }
337
338    /// Sugar for [`Self::catch`]`(404, handler)`.
339    pub fn not_found<H, T>(&mut self, handler: H) -> &mut Self
340    where
341        H: IntoHandler<T>,
342    {
343        self.catch(404, handler)
344    }
345
346    /// Called when a leaf handler returns `Err`. Request is already consumed.
347    pub fn error_handler<F, Fut>(&mut self, f: F) -> &mut Self
348    where
349        F: Fn(crate::error::Error) -> Fut + Send + Sync + 'static,
350        Fut: std::future::Future<Output = Response> + Send + 'static,
351    {
352        self.error_handler = Some(Arc::new(move |err| Box::pin(f(err))));
353        self
354    }
355
356    /// Full introspection: HTTP routes and raw paths.
357    pub fn route_entries(&self) -> Vec<RouteEntry> {
358        let mut out = Vec::new();
359        for r in &self.routes {
360            let mut meta = self.defaults.clone();
361            meta.extend(r.meta.clone());
362            out.push(RouteEntry::Http {
363                method: r.method.clone(),
364                path: r.path.clone(),
365                meta,
366            });
367        }
368        for r in &self.raw_routes {
369            out.push(RouteEntry::Raw {
370                path: r.path.clone(),
371            });
372        }
373        out
374    }
375
376    /// Run [`RouteValue::check`] for router defaults and every route.
377    pub(crate) fn check_route_values(
378        &self,
379        state: &crate::state::StateMap,
380        installed_plugins: &std::collections::HashSet<&'static str>,
381    ) -> Result<(), String> {
382        use crate::route_value::BuildCtx;
383        let ctx = BuildCtx {
384            state,
385            installed_plugins,
386            route_path: "<defaults>",
387            route_method: None,
388        };
389        self.defaults.check_all(&ctx)?;
390        for r in &self.routes {
391            let mut meta = self.defaults.clone();
392            meta.extend(r.meta.clone());
393            let ctx = BuildCtx {
394                state,
395                installed_plugins,
396                route_path: &r.path,
397                route_method: Some(&r.method),
398            };
399            meta.check_all(&ctx)?;
400        }
401        Ok(())
402    }
403
404    /// Human-readable route map (method, path, middleware names).
405    pub fn explain(&self) -> String {
406        use std::fmt::Write;
407        let mut out = String::new();
408        let _ = writeln!(out, "root_middleware: [{}]", format_mw_names(&self.middleware));
409        let _ = writeln!(out, "error_handler: {}", self.error_handler.is_some());
410        let mut catch_lines = Vec::new();
411        if !self.catchers.is_empty() {
412            let mut codes: Vec<_> = self.catchers.keys().copied().collect();
413            codes.sort_unstable();
414            catch_lines.push(format!("/ → {:?}", codes));
415        }
416        for (prefix, map) in &self.scoped_catchers {
417            let mut codes: Vec<_> = map.keys().copied().collect();
418            codes.sort_unstable();
419            let p = if prefix.is_empty() { "/" } else { prefix.as_str() };
420            catch_lines.push(format!("{p} → {:?}", codes));
421        }
422        let _ = writeln!(out, "catchers: [{}]", catch_lines.join("; "));
423        if !self.defaults.is_empty() {
424            let _ = writeln!(out, "defaults: [{}]", self.defaults.labels().join(" "));
425        }
426        for r in &self.routes {
427            let labels = r.meta.labels();
428            if labels.is_empty() {
429                let _ = writeln!(
430                    out,
431                    "{} {} mw=[{}]",
432                    r.method,
433                    r.path,
434                    format_mw_names(&r.middleware)
435                );
436            } else {
437                let _ = writeln!(
438                    out,
439                    "{} {} mw=[{}] {}",
440                    r.method,
441                    r.path,
442                    format_mw_names(&r.middleware),
443                    labels.join(" ")
444                );
445            }
446        }
447        for r in &self.raw_routes {
448            let _ = writeln!(out, "RAW {} mw=[]", r.path);
449        }
450        out
451    }
452
453    fn add<H, T>(&mut self, method: Method, path: &str, handler: H) -> &mut Self
454    where
455        H: IntoHandler<T>,
456    {
457        self.routes.push(RouteDef {
458            method,
459            path: normalize_path(path),
460            // Module stack is baked at `mount`, not here — keeps root mw outer-only.
461            middleware: Vec::new(),
462            handler: handler.into_handler(),
463            meta: MetaMap::new(),
464        });
465        self.last_was_route = true;
466        self
467    }
468}
469
470impl Default for Router {
471    fn default() -> Self {
472        Self::new()
473    }
474}
475
476fn format_mw_names(entries: &[MwEntry]) -> String {
477    entries
478        .iter()
479        .map(|e| e.name.as_str())
480        .collect::<Vec<_>>()
481        .join(", ")
482}