rust_web_server/router/mod.rs
1pub(crate) mod matcher;
2#[cfg(test)]
3mod tests;
4
5use std::collections::HashMap;
6use std::sync::Arc;
7
8use crate::request::Request;
9use crate::response::Response;
10use crate::server::ConnectionInfo;
11use matcher::Segment;
12
13/// Named path-segment values extracted from a matched route pattern.
14///
15/// Given the pattern `/users/:id/posts/:post_id` matched against
16/// `/users/42/posts/7`, `params.get("id")` returns `Some("42")` and
17/// `params.get("post_id")` returns `Some("7")`.
18///
19/// Wildcard segments (`*name`) capture everything after the prefix:
20/// `/files/*path` matched against `/files/a/b/c` gives `path = "a/b/c"`.
21#[derive(Clone)]
22pub struct PathParams {
23 params: HashMap<String, String>,
24}
25
26impl PathParams {
27 /// Build a `PathParams` from an existing map — used to adapt
28 /// [`matcher::try_match`]'s output for both `Router` and `AsyncAppWithState`.
29 pub(crate) fn from_map(params: HashMap<String, String>) -> Self {
30 PathParams { params }
31 }
32
33 /// Returns the value for the named parameter, or `None` if absent.
34 pub fn get(&self, name: &str) -> Option<&str> {
35 self.params.get(name).map(String::as_str)
36 }
37}
38
39type HandlerFn =
40 Arc<dyn Fn(&Request, &PathParams, &ConnectionInfo) -> Response + Send + Sync + 'static>;
41
42#[derive(Clone)]
43struct Route {
44 method: String,
45 segments: Vec<Segment>,
46 handler: HandlerFn,
47}
48
49/// A path-based HTTP router with named parameter extraction.
50///
51/// Register routes with [`Router::get`], [`Router::post`], etc. Each handler
52/// receives the parsed [`PathParams`] alongside the raw [`Request`] and
53/// [`ConnectionInfo`]. Call [`Router::handle`] from inside a [`Controller`]
54/// or an [`Application::execute`] implementation.
55///
56/// # Example
57///
58/// ```rust,no_run
59/// use rust_web_server::router::{Router, PathParams};
60/// use rust_web_server::request::Request;
61/// use rust_web_server::response::{Response, STATUS_CODE_REASON_PHRASE};
62/// use rust_web_server::range::Range;
63/// use rust_web_server::mime_type::MimeType;
64/// use rust_web_server::server::ConnectionInfo;
65/// use rust_web_server::core::New;
66///
67/// let router = Router::new()
68/// .get("/hello", |_req, _params, _conn| {
69/// let mut r = Response::new();
70/// r.status_code = *STATUS_CODE_REASON_PHRASE.n200_ok.status_code;
71/// r.reason_phrase = STATUS_CODE_REASON_PHRASE.n200_ok.reason_phrase.to_string();
72/// r.content_range_list = vec![Range::get_content_range(b"hello".to_vec(), MimeType::TEXT_PLAIN.to_string())];
73/// r
74/// })
75/// .get("/users/:id", |_req, params, _conn| {
76/// let id = params.get("id").unwrap_or("unknown");
77/// let mut r = Response::new();
78/// r.status_code = *STATUS_CODE_REASON_PHRASE.n200_ok.status_code;
79/// r.reason_phrase = STATUS_CODE_REASON_PHRASE.n200_ok.reason_phrase.to_string();
80/// r.content_range_list = vec![Range::get_content_range(
81/// format!("user {}", id).into_bytes(),
82/// MimeType::TEXT_PLAIN.to_string(),
83/// )];
84/// r
85/// });
86/// ```
87/// A registered route entry returned by [`Router::route_entries`].
88#[derive(Clone)]
89pub struct RouteInfo {
90 pub method: String,
91 pub pattern: String,
92}
93
94#[derive(Clone)]
95pub struct Router {
96 routes: Vec<Route>,
97 /// When set, `handle()` only matches if the request's SNI hostname (or
98 /// `Host` header for plain HTTP) equals this value.
99 host: Option<String>,
100}
101
102impl Router {
103 pub fn new() -> Self {
104 Router { routes: Vec::new(), host: None }
105 }
106
107 /// Restrict this router to requests whose SNI hostname (TLS) or `Host`
108 /// header (plain HTTP) matches `host`. Call before registering routes.
109 pub fn with_host(mut self, host: &str) -> Self {
110 self.host = Some(host.to_string());
111 self
112 }
113
114 /// Register a `GET` handler for `pattern`.
115 pub fn get<F>(self, pattern: &str, handler: F) -> Self
116 where F: Fn(&Request, &PathParams, &ConnectionInfo) -> Response + Send + Sync + 'static {
117 self.add("GET", pattern, handler)
118 }
119
120 /// Serve WebSocket connections at `pattern`: an upgrade request's
121 /// connection goes to `handler` with the route's parameters, anything
122 /// else gets `426 Upgrade Required` (see
123 /// [`WebSocket::upgrade`](crate::websocket::WebSocket::upgrade)).
124 ///
125 /// ```rust,no_run
126 /// use rust_web_server::router::Router;
127 /// use rust_web_server::websocket::Message;
128 ///
129 /// let router = Router::new().websocket("/rooms/:room", |mut ws, params| {
130 /// let room = params.get("room").unwrap_or("lobby").to_string();
131 /// while let Ok(Some(Message::Text(text))) = ws.recv() {
132 /// let _ = ws.send_text(&format!("{room}: {text}"));
133 /// }
134 /// });
135 /// ```
136 #[cfg(not(target_arch = "wasm32"))]
137 pub fn websocket<F>(self, pattern: &str, handler: F) -> Self
138 where
139 F: Fn(crate::websocket::WebSocketConnection, PathParams) + Send + Sync + 'static,
140 {
141 let handler = Arc::new(handler);
142 self.get(pattern, move |req, params, _| {
143 let (handler, params) = (Arc::clone(&handler), params.clone());
144 crate::websocket::WebSocket::upgrade(req, move |ws| handler(ws, params))
145 })
146 }
147
148 /// Register a `POST` handler for `pattern`.
149 pub fn post<F>(self, pattern: &str, handler: F) -> Self
150 where F: Fn(&Request, &PathParams, &ConnectionInfo) -> Response + Send + Sync + 'static {
151 self.add("POST", pattern, handler)
152 }
153
154 /// Register a `PUT` handler for `pattern`.
155 pub fn put<F>(self, pattern: &str, handler: F) -> Self
156 where F: Fn(&Request, &PathParams, &ConnectionInfo) -> Response + Send + Sync + 'static {
157 self.add("PUT", pattern, handler)
158 }
159
160 /// Register a `PATCH` handler for `pattern`.
161 pub fn patch<F>(self, pattern: &str, handler: F) -> Self
162 where F: Fn(&Request, &PathParams, &ConnectionInfo) -> Response + Send + Sync + 'static {
163 self.add("PATCH", pattern, handler)
164 }
165
166 /// Register a `DELETE` handler for `pattern`.
167 pub fn delete<F>(self, pattern: &str, handler: F) -> Self
168 where F: Fn(&Request, &PathParams, &ConnectionInfo) -> Response + Send + Sync + 'static {
169 self.add("DELETE", pattern, handler)
170 }
171
172 pub(crate) fn add<F>(mut self, method: &str, pattern: &str, handler: F) -> Self
173 where F: Fn(&Request, &PathParams, &ConnectionInfo) -> Response + Send + Sync + 'static {
174 self.routes.push(Route {
175 method: method.to_string(),
176 segments: matcher::parse_pattern(pattern),
177 handler: Arc::new(handler),
178 });
179 self
180 }
181
182 /// Return a snapshot of all registered routes as `(method, pattern)` pairs.
183 ///
184 /// Patterns are reconstructed from parsed segments, so the output exactly
185 /// matches what was passed to `.get()`, `.post()`, etc. at registration time.
186 pub fn route_entries(&self) -> Vec<RouteInfo> {
187 self.routes.iter().map(|r| RouteInfo {
188 method: r.method.clone(),
189 pattern: matcher::segments_to_pattern(&r.segments),
190 }).collect()
191 }
192
193 /// Try to match `request` against registered routes in registration order.
194 ///
195 /// Returns `Some(response)` on the first match, `None` if no route matches.
196 /// The query string is stripped before matching; only the path is used.
197 ///
198 /// When `.with_host()` is set, this returns `None` immediately unless the
199 /// request's SNI hostname (TLS) or `Host` header (plain HTTP) matches.
200 pub fn handle(&self, request: &Request, connection: &ConnectionInfo) -> Option<Response> {
201 let (route, params) = self.find(request, connection)?;
202 Some((route.handler)(request, ¶ms, connection))
203 }
204
205 /// `true` if [`handle`](Self::handle) would run a handler for `request`
206 /// (method, path and host all match), without running it.
207 pub fn matches(&self, request: &Request, connection: &ConnectionInfo) -> bool {
208 self.find(request, connection).is_some()
209 }
210
211 fn find(&self, request: &Request, connection: &ConnectionInfo) -> Option<(&Route, PathParams)> {
212 if let Some(required_host) = &self.host {
213 let actual = connection.sni_hostname.as_deref().or_else(|| {
214 request.headers.iter()
215 .find(|h| h.name.eq_ignore_ascii_case("host"))
216 .map(|h| h.value.as_ref())
217 });
218 if actual != Some(required_host.as_str()) {
219 return None;
220 }
221 }
222
223 let path = request.request_uri.split('?').next().unwrap_or(&request.request_uri);
224 let path_segs: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
225
226 for route in &self.routes {
227 if route.method != request.method {
228 continue;
229 }
230 if let Some(params) = matcher::try_match(&route.segments, &path_segs) {
231 return Some((route, PathParams::from_map(params)));
232 }
233 }
234 None
235 }
236}