1use 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#[derive(Clone)]
16pub struct Route {
17 pub method: Method,
18 pub path: String,
19 pub handler: HandlerFunc,
20}
21
22impl Route {
23 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 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
39pub 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
51pub fn with_root(root: &str, routes: Vec<Route>) -> Vec<Route> {
53 with_prefix(root, routes)
54}
55
56pub 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
74fn 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}