rest/
router.rs

1//! Router module entry: routing descriptions (Route) and runtime router.
2
3use crate::http::HandlerFunc;
4use crate::middleware::{IntoHandler, Middleware};
5use http::Method;
6
7pub mod params;
8#[allow(clippy::module_inception)]
9pub mod router;
10
11pub use params::PathParams;
12pub use router::Router;
13
14/// Single route definition.
15#[derive(Clone)]
16pub struct Route {
17    pub method: Method,
18    pub path: String,
19    pub handler: HandlerFunc,
20}
21
22impl Route {
23    /// Build a route and convert async function into internal HandlerFunc.
24    pub fn new(method: Method, path: impl Into<String>, handler: impl IntoHandler) -> Self {
25        Self {
26            method,
27            path: normalize_path(path.into()),
28            handler: handler.into_handler(),
29        }
30    }
31
32    /// Apply middlewares to this route (internal use).
33    pub(crate) fn with_middlewares(self, middlewares: &[Middleware]) -> Self {
34        let handler = crate::middleware::apply_middlewares(self.handler.clone(), middlewares);
35        Self { handler, ..self }
36    }
37}
38
39/// Join prefix with routes.
40pub fn with_prefix(prefix: &str, routes: Vec<Route>) -> Vec<Route> {
41    let normalized_prefix = normalize_path(prefix.to_string());
42    routes
43        .into_iter()
44        .map(|mut route| {
45            route.path = join_path(&normalized_prefix, &route.path);
46            route
47        })
48        .collect()
49}
50
51/// Alias of `with_prefix` for root prefix.
52pub fn with_root(root: &str, routes: Vec<Route>) -> Vec<Route> {
53    with_prefix(root, routes)
54}
55
56/// Apply middlewares to a list of routes (keeps original order).
57pub fn with_handlers(middlewares: Vec<Middleware>, routes: Vec<Route>) -> Vec<Route> {
58    crate::middleware::with_middlewares(middlewares, routes)
59}
60
61fn join_path(prefix: &str, path: &str) -> String {
62    let mut result = String::new();
63    if !prefix.starts_with('/') {
64        result.push('/');
65    }
66    result.push_str(prefix.trim_end_matches('/'));
67    if !result.ends_with('/') {
68        result.push('/');
69    }
70    result.push_str(path.trim_start_matches('/'));
71    normalize_path(result)
72}
73
74/// Normalize path by collapsing slashes and ensuring leading slash.
75fn normalize_path(path: String) -> String {
76    if path.is_empty() {
77        return "/".to_string();
78    }
79    let mut cleaned = path.replace("//", "/");
80    if !cleaned.starts_with('/') {
81        cleaned.insert(0, '/');
82    }
83    if cleaned.len() > 1 && cleaned.ends_with('/') {
84        cleaned.pop();
85    }
86    cleaned
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92    use hyper::Body;
93    use tokio::runtime::Runtime;
94
95    fn runtime() -> Runtime {
96        Runtime::new().unwrap()
97    }
98
99    #[test]
100    fn normalize_path_should_strip_extra_slash() {
101        assert_eq!(normalize_path("//api//v1/".to_string()), "/api/v1");
102        assert_eq!(normalize_path("api/v1".to_string()), "/api/v1");
103        assert_eq!(normalize_path("/".to_string()), "/");
104    }
105
106    #[test]
107    fn with_root_should_alias_prefix() {
108        let routes = vec![Route::new(http::Method::GET, "/list", handler())];
109        let prefixed = with_root("/api", routes);
110        assert_eq!(prefixed[0].path, "/api/list");
111    }
112
113    #[test]
114    fn with_prefix_should_join_correctly() {
115        let routes = vec![Route::new(http::Method::GET, "/list", handler())];
116        let prefixed = with_prefix("/api/v1", routes);
117        assert_eq!(prefixed[0].path, "/api/v1/list");
118    }
119
120    #[test]
121    fn with_middlewares_should_wrap() {
122        let route = Route::new(http::Method::POST, "/test", handler());
123        let mw = crate::middleware::middleware(|req, next| async move {
124            let mut resp = next.call(req).await;
125            resp.headers_mut().insert("X-MW", "1".parse().unwrap());
126            resp
127        });
128        let wrapped = route.with_middlewares(&[mw]);
129        let resp = runtime().block_on(
130            wrapped.handler.call(
131                http::Request::builder()
132                    .method(http::Method::POST)
133                    .uri("/test")
134                    .body(Body::empty())
135                    .unwrap(),
136            ),
137        );
138        assert_eq!(resp.headers().get("X-MW").unwrap().to_str().unwrap(), "1");
139    }
140
141    fn handler() -> impl IntoHandler {
142        |_: http::Request<Body>| async {
143            http::Response::builder()
144                .status(http::StatusCode::OK)
145                .body(Body::empty())
146                .unwrap()
147        }
148    }
149}