rws_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 #[doc(hidden)]
30 pub fn from_map(params: HashMap<String, String>) -> Self {
31 PathParams { params }
32 }
33
34 /// Returns the value for the named parameter, or `None` if absent.
35 pub fn get(&self, name: &str) -> Option<&str> {
36 self.params.get(name).map(String::as_str)
37 }
38}
39
40/// The host a request asked for: its `Host` header (HTTP/2 and HTTP/3 turn
41/// `:authority` into one), else the TLS SNI name, else `""`. Host comes first
42/// because browsers reuse one connection for every name its certificate covers
43/// (RFC 9113 §9.1.1), so SNI names the connection's first request, not this one.
44#[doc(hidden)]
45pub fn request_host<'a>(request: &'a Request, connection: &'a ConnectionInfo) -> &'a str {
46 request.headers.iter()
47 .find(|h| h.name.eq_ignore_ascii_case("host"))
48 .map(|h| h.value.as_ref())
49 .filter(|v: &&str| !v.is_empty())
50 .or(connection.sni_hostname.as_deref())
51 .unwrap_or("")
52}
53
54/// A route's `host` against the request's ([`request_host`]): ignoring ASCII
55/// case, and ignoring a port the route doesn't name (#50).
56#[doc(hidden)]
57pub fn host_matches(expected: &str, actual: &str) -> bool {
58 let actual = if expected.contains(':') {
59 actual
60 } else if let Some(rest) = actual.strip_prefix('[') {
61 // [IPv6]:port
62 rest.split(']').next().unwrap_or(rest)
63 } else {
64 actual.rsplit_once(':').map_or(actual, |(host, port)| if port.bytes().all(|b| b.is_ascii_digit()) { host } else { actual })
65 };
66 let expected = expected.trim_start_matches('[').trim_end_matches(']');
67 actual.eq_ignore_ascii_case(expected)
68}
69
70type HandlerFn =
71 Arc<dyn Fn(&Request, &PathParams, &ConnectionInfo) -> Response + Send + Sync + 'static>;
72
73#[derive(Clone)]
74struct Route {
75 method: String,
76 segments: Vec<Segment>,
77 handler: HandlerFn,
78}
79
80/// A path-based HTTP router with named parameter extraction.
81///
82/// Register routes with [`Router::get`], [`Router::post`], etc. Each handler
83/// receives the parsed [`PathParams`] alongside the raw [`Request`] and
84/// [`ConnectionInfo`]. Call [`Router::handle`] from inside a [`Controller`]
85/// or an [`Application::execute`] implementation.
86///
87/// # Example
88///
89/// ```rust,no_run
90/// # extern crate rws_server as rust_web_server;
91/// use rust_web_server::router::{Router, PathParams};
92/// use rust_web_server::request::Request;
93/// use rust_web_server::response::{Response, STATUS_CODE_REASON_PHRASE};
94/// use rust_web_server::range::Range;
95/// use rust_web_server::mime_type::MimeType;
96/// use rust_web_server::server::ConnectionInfo;
97/// use rust_web_server::core::New;
98///
99/// let router = Router::new()
100/// .get("/hello", |_req, _params, _conn| {
101/// let mut r = Response::new();
102/// r.status_code = *STATUS_CODE_REASON_PHRASE.n200_ok.status_code;
103/// r.reason_phrase = STATUS_CODE_REASON_PHRASE.n200_ok.reason_phrase.to_string();
104/// r.content_range_list = vec![Range::get_content_range(b"hello".to_vec(), MimeType::TEXT_PLAIN.to_string())];
105/// r
106/// })
107/// .get("/users/:id", |_req, params, _conn| {
108/// let id = params.get("id").unwrap_or("unknown");
109/// let mut r = Response::new();
110/// r.status_code = *STATUS_CODE_REASON_PHRASE.n200_ok.status_code;
111/// r.reason_phrase = STATUS_CODE_REASON_PHRASE.n200_ok.reason_phrase.to_string();
112/// r.content_range_list = vec![Range::get_content_range(
113/// format!("user {}", id).into_bytes(),
114/// MimeType::TEXT_PLAIN.to_string(),
115/// )];
116/// r
117/// });
118/// ```
119/// A registered route entry returned by [`Router::route_entries`].
120#[derive(Clone)]
121pub struct RouteInfo {
122 pub method: String,
123 pub pattern: String,
124}
125
126#[derive(Clone)]
127pub struct Router {
128 routes: Vec<Route>,
129 /// When set, `handle()` only matches if the request's SNI hostname (or
130 /// `Host` header for plain HTTP) equals this value.
131 host: Option<String>,
132}
133
134impl Router {
135 pub fn new() -> Self {
136 Router { routes: Vec::new(), host: None }
137 }
138
139 /// Restrict this router to requests for `host`: their `Host` header
140 /// (`:authority` over HTTP/2 and HTTP/3), else the TLS SNI name, ignoring
141 /// case and a port `host` doesn't name. Call before registering routes.
142 pub fn with_host(mut self, host: &str) -> Self {
143 self.host = Some(host.to_string());
144 self
145 }
146
147 /// Register a `GET` handler for `pattern`.
148 pub fn get<F>(self, pattern: &str, handler: F) -> Self
149 where F: Fn(&Request, &PathParams, &ConnectionInfo) -> Response + Send + Sync + 'static {
150 self.add("GET", pattern, handler)
151 }
152
153 /// Serve WebSocket connections at `pattern`: an upgrade request's
154 /// connection goes to `handler` with the route's parameters, anything
155 /// else gets `426 Upgrade Required` (see
156 /// [`WebSocket::upgrade`](crate::websocket::WebSocket::upgrade)).
157 ///
158 /// ```rust,no_run
159 /// # extern crate rws_server as rust_web_server;
160 /// use rust_web_server::router::Router;
161 /// use rust_web_server::websocket::Message;
162 ///
163 /// let router = Router::new().websocket("/rooms/:room", |mut ws, params| {
164 /// let room = params.get("room").unwrap_or("lobby").to_string();
165 /// while let Ok(Some(Message::Text(text))) = ws.recv() {
166 /// let _ = ws.send_text(&format!("{room}: {text}"));
167 /// }
168 /// });
169 /// ```
170 #[cfg(not(target_arch = "wasm32"))]
171 pub fn websocket<F>(self, pattern: &str, handler: F) -> Self
172 where
173 F: Fn(crate::websocket::WebSocketConnection, PathParams) + Send + Sync + 'static,
174 {
175 let handler = Arc::new(handler);
176 self.get(pattern, move |req, params, _| {
177 let (handler, params) = (Arc::clone(&handler), params.clone());
178 crate::websocket::WebSocket::upgrade(req, move |ws| handler(ws, params))
179 })
180 }
181
182 /// Register a `POST` handler for `pattern`.
183 pub fn post<F>(self, pattern: &str, handler: F) -> Self
184 where F: Fn(&Request, &PathParams, &ConnectionInfo) -> Response + Send + Sync + 'static {
185 self.add("POST", pattern, handler)
186 }
187
188 /// Register a `PUT` handler for `pattern`.
189 pub fn put<F>(self, pattern: &str, handler: F) -> Self
190 where F: Fn(&Request, &PathParams, &ConnectionInfo) -> Response + Send + Sync + 'static {
191 self.add("PUT", pattern, handler)
192 }
193
194 /// Register a `PATCH` handler for `pattern`.
195 pub fn patch<F>(self, pattern: &str, handler: F) -> Self
196 where F: Fn(&Request, &PathParams, &ConnectionInfo) -> Response + Send + Sync + 'static {
197 self.add("PATCH", pattern, handler)
198 }
199
200 /// Register a `DELETE` handler for `pattern`.
201 pub fn delete<F>(self, pattern: &str, handler: F) -> Self
202 where F: Fn(&Request, &PathParams, &ConnectionInfo) -> Response + Send + Sync + 'static {
203 self.add("DELETE", pattern, handler)
204 }
205
206 #[doc(hidden)]
207 pub fn add<F>(mut self, method: &str, pattern: &str, handler: F) -> Self
208 where F: Fn(&Request, &PathParams, &ConnectionInfo) -> Response + Send + Sync + 'static {
209 self.routes.push(Route {
210 method: method.to_string(),
211 segments: matcher::parse_pattern(pattern),
212 handler: Arc::new(handler),
213 });
214 self
215 }
216
217 /// Return a snapshot of all registered routes as `(method, pattern)` pairs.
218 ///
219 /// Patterns are reconstructed from parsed segments, so the output exactly
220 /// matches what was passed to `.get()`, `.post()`, etc. at registration time.
221 pub fn route_entries(&self) -> Vec<RouteInfo> {
222 self.routes.iter().map(|r| RouteInfo {
223 method: r.method.clone(),
224 pattern: matcher::segments_to_pattern(&r.segments),
225 }).collect()
226 }
227
228 /// Try to match `request` against registered routes in registration order.
229 ///
230 /// Returns `Some(response)` on the first match, `None` if no route matches.
231 /// The query string is stripped before matching; only the path is used.
232 ///
233 /// When `.with_host()` is set, this returns `None` immediately unless the
234 /// request's host (`Host` header, else SNI) matches.
235 pub fn handle(&self, request: &Request, connection: &ConnectionInfo) -> Option<Response> {
236 let (route, params) = self.find(request, connection)?;
237 Some((route.handler)(request, ¶ms, connection))
238 }
239
240 /// `true` if [`handle`](Self::handle) would run a handler for `request`
241 /// (method, path and host all match), without running it.
242 pub fn matches(&self, request: &Request, connection: &ConnectionInfo) -> bool {
243 self.find(request, connection).is_some()
244 }
245
246 fn find(&self, request: &Request, connection: &ConnectionInfo) -> Option<(&Route, PathParams)> {
247 if let Some(required_host) = &self.host {
248 if !host_matches(required_host, request_host(request, connection)) {
249 return None;
250 }
251 }
252
253 let path = request.request_uri.split('?').next().unwrap_or(&request.request_uri);
254 let path_segs: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
255
256 for route in &self.routes {
257 if route.method != request.method {
258 continue;
259 }
260 if let Some(params) = matcher::try_match(&route.segments, &path_segs) {
261 return Some((route, PathParams::from_map(params)));
262 }
263 }
264 None
265 }
266}