Skip to main content

mini_serve/
router.rs

1use std::collections::HashMap;
2
3use hyper::Method;
4
5use crate::handler::Handler;
6
7/// Extracted path parameters from a matched route.
8///
9/// Contains a map of parameter names to their decoded string values
10/// (e.g., `id` → `"42"` from a route `/items/:id`). Populated by the router
11/// and stored in request extensions; extract via `path_params::<T>(req)`.
12#[derive(Clone, Debug, Default)]
13pub struct PathParams(pub HashMap<String, String>);
14
15/// Parsed query parameters from the request URL.
16///
17/// Contains a map of query parameter names to their values, with `+` decoded
18/// as space and percent-encoded sequences decoded. Populated by the app and
19/// stored in request extensions; extract via `req.extensions().get::<QueryParams>()`.
20#[derive(Clone, Debug, Default)]
21pub struct QueryParams(pub HashMap<String, String>);
22
23/// A trie-based HTTP router for matching requests to handlers.
24///
25/// Routes are matched by method and path, with support for dynamic path parameters
26/// (`:name`) and wildcards (`*`). Each request is matched in a single trie traversal.
27#[derive(Default)]
28pub struct Router<S> {
29	root: Node<S>,
30}
31
32struct Node<S> {
33	segment:    String,
34	param_name: String,
35	is_wildcard: bool,
36	handlers:   HashMap<Method, Handler<S>>,
37	children:   Vec<Node<S>>,
38}
39
40impl<S> Default for Node<S> {
41	fn default() -> Self {
42		Node {
43			segment:    String::new(),
44			param_name: String::new(),
45			is_wildcard: false,
46			handlers:   HashMap::new(),
47			children:   Vec::new(),
48		}
49	}
50}
51
52impl<S: Send + Sync + 'static> Router<S> {
53	/// Create a new empty router.
54	pub fn new() -> Self {
55		Router { root: Node::default() }
56	}
57
58	/// Register a handler for the given method and path.
59	///
60	/// Paths may contain static segments, dynamic parameters (`:name`), and a
61	/// wildcard (`*`) to match everything. For example: `/users/:id` or `/api/*`.
62	pub fn insert(&mut self, method: Method, path: &str, handler: Handler<S>) {
63		let segments = split_path(path);
64		let mut node = &mut self.root;
65
66		for seg in segments {
67			if seg == "*" {
68				if let Some(idx) = node.children.iter().position(|c| c.is_wildcard) {
69					node = &mut node.children[idx];
70				} else {
71					node.children.push(Node {
72						segment:    "*".to_string(),
73						param_name: String::new(),
74						is_wildcard: true,
75						handlers:   HashMap::new(),
76						children:   Vec::new(),
77					});
78					node = node.children.last_mut().unwrap();
79				}
80			} else if let Some(param_name) = seg.strip_prefix(':') {
81				if let Some(idx) = node.children.iter().position(|c| c.param_name == param_name) {
82					node = &mut node.children[idx];
83				} else {
84					node.children.push(Node {
85						segment:    seg.to_string(),
86						param_name: param_name.to_string(),
87						is_wildcard: false,
88						handlers:   HashMap::new(),
89						children:   Vec::new(),
90					});
91					node = node.children.last_mut().unwrap();
92				}
93			} else {
94				if let Some(idx) = node.children.iter().position(|c| c.segment == seg) {
95					node = &mut node.children[idx];
96				} else {
97					node.children.push(Node {
98						segment:    seg.to_string(),
99						param_name: String::new(),
100						is_wildcard: false,
101						handlers:   HashMap::new(),
102						children:   Vec::new(),
103					});
104					node = node.children.last_mut().unwrap();
105				}
106			}
107		}
108
109		node.handlers.insert(method, handler);
110	}
111
112	/// Find and return the handler for the given method and path, along with extracted parameters.
113	///
114	/// Returns `None` if no route matches. Path parameters are percent-decoded and
115	/// included in the returned `PathParams`.
116	pub fn match_route<'a>(
117		&'a self,
118		method: &Method,
119		path: &str,
120	) -> Option<(&'a Handler<S>, PathParams)> {
121		let segments = split_path(path);
122		let mut params = PathParams::default();
123		let node = Self::find_node(&self.root, &segments, 0, method, &mut params)?;
124		node.handlers.get(method).map(|h| (h, params))
125	}
126
127	/// List all HTTP methods that have handlers registered for the given path.
128	///
129	/// Returns an empty vector if the path has no registered handlers.
130	pub fn allowed_methods(&self, path: &str) -> Vec<Method> {
131		let segments = split_path(path);
132		let mut methods = std::collections::HashSet::new();
133		let mut params = PathParams::default();
134		Self::collect_allowed_methods(&self.root, &segments, 0, &mut params, &mut methods);
135		methods.into_iter().collect()
136	}
137
138	/// Check whether any handler is registered for the given path.
139	pub fn path_exists(&self, path: &str) -> bool {
140		!self.allowed_methods(path).is_empty()
141	}
142
143	/// Recursive backtracking search for a trie node that is both
144	/// path-complete *and* has a handler for `method`.
145	///
146	/// Tries children in precedence order: static → param → wildcard.
147	/// Backtracks to the next-choice child on either kind of dead end: no
148	/// path match, or a path-complete node with no handler for `method` (so
149	/// e.g. a registered `POST /users/new` doesn't shadow `GET /users/:id`
150	/// for a `GET /users/new` request — the static branch is path-complete
151	/// but lacks GET, so the search falls back to the param branch).
152	///
153	/// Optimization: borrows params through static segments without cloning,
154	/// only cloning before modifying for param branches. On backtrack,
155	/// truncates params to remove any additions made in failed attempts.
156	fn find_node<'a>(
157		node: &'a Node<S>,
158		segments: &[String],
159		idx: usize,
160		method: &Method,
161		params: &mut PathParams,
162	) -> Option<&'a Node<S>> {
163		if idx == segments.len() {
164			return if node.handlers.contains_key(method) { Some(node) } else { None };
165		}
166
167		let seg = &segments[idx];
168
169		for child in &node.children {
170			if !child.is_wildcard && child.param_name.is_empty() && child.segment == *seg {
171				// Static segment: pass params through without cloning.
172				if let Some(found) = Self::find_node(child, segments, idx + 1, method, params) {
173					return Some(found);
174				}
175			}
176		}
177
178		for child in &node.children {
179			if !child.is_wildcard && !child.param_name.is_empty() {
180				// Param segment: clone before modifying for backtrack safety.
181				let mut p = params.clone();
182				p.0.insert(child.param_name.clone(), seg.clone());
183				if let Some(found) = Self::find_node(child, segments, idx + 1, method, &mut p) {
184					*params = p;
185					return Some(found);
186				}
187				// Backtrack: remove the param we added in this failed attempt.
188				params.0.remove(&child.param_name);
189			}
190		}
191
192		for child in &node.children {
193			if child.is_wildcard && child.handlers.contains_key(method) {
194				params.0.insert("*".to_string(), segments[idx..].join("/"));
195				return Some(child);
196			}
197		}
198
199		None
200	}
201
202	/// Exhaustive counterpart to [`find_node`] for computing the `Allow`
203	/// header: unions handler methods across *every* path-complete node
204	/// reachable, not just the first one the static→param→wildcard
205	/// precedence would settle on. Necessary because, per the same
206	/// ambiguity `find_node` backtracks around, more than one branch can be
207	/// path-complete for a given request path.
208	fn collect_allowed_methods(
209		node: &Node<S>,
210		segments: &[String],
211		idx: usize,
212		params: &mut PathParams,
213		methods: &mut std::collections::HashSet<Method>,
214	) {
215		if idx == segments.len() {
216			methods.extend(node.handlers.keys().cloned());
217			return;
218		}
219
220		let seg = &segments[idx];
221
222		for child in &node.children {
223			if !child.is_wildcard && child.param_name.is_empty() && child.segment == *seg {
224				let mut p = params.clone();
225				Self::collect_allowed_methods(child, segments, idx + 1, &mut p, methods);
226			}
227		}
228
229		for child in &node.children {
230			if !child.is_wildcard && !child.param_name.is_empty() {
231				let mut p = params.clone();
232				p.0.insert(child.param_name.clone(), seg.clone());
233				Self::collect_allowed_methods(child, segments, idx + 1, &mut p, methods);
234			}
235		}
236
237		for child in &node.children {
238			if child.is_wildcard {
239				methods.extend(child.handlers.keys().cloned());
240			}
241		}
242	}
243}
244
245fn split_path(path: &str) -> Vec<String> {
246	path.trim_start_matches('/')
247		.split('/')
248		.filter(|s| !s.is_empty())
249		.map(|s| percent_encoding::percent_decode_str(s).decode_utf8_lossy().into_owned())
250		.collect()
251}
252
253#[cfg(test)]
254mod tests {
255	use super::*;
256	use crate::error::ServeError;
257	use hyper::Response;
258	use hyper::body::Bytes;
259
260	fn dummy_handler() -> Handler<()> {
261		crate::handler::handler(|_, _| async {
262			Ok::<_, ServeError>(Response::new(crate::handler::body(Bytes::from("ok"))))
263		})
264	}
265
266	#[test]
267	fn insert_and_match_static() {
268		let mut router = Router::new();
269		router.insert(Method::GET, "/hello", dummy_handler());
270		let (_, _) = router.match_route(&Method::GET, "/hello").unwrap();
271	}
272
273	#[test]
274	fn match_with_path_param() {
275		let mut router = Router::new();
276		router.insert(Method::GET, "/users/:id", dummy_handler());
277		let (_, params) = router.match_route(&Method::GET, "/users/42").unwrap();
278		assert_eq!(params.0.get("id").unwrap(), "42");
279	}
280
281	#[test]
282	fn match_with_wildcard() {
283		let mut router = Router::new();
284		router.insert(Method::GET, "/files/*", dummy_handler());
285		let (_, params) = router.match_route(&Method::GET, "/files/a/b/c").unwrap();
286		assert_eq!(params.0.get("*").unwrap(), "a/b/c");
287	}
288
289	#[test]
290	fn no_match_for_unregistered_route() {
291		let mut router = Router::new();
292		router.insert(Method::GET, "/hello", dummy_handler());
293		assert!(router.match_route(&Method::GET, "/world").is_none());
294	}
295
296	#[test]
297	fn method_mismatch_returns_none() {
298		let mut router = Router::new();
299		router.insert(Method::GET, "/hello", dummy_handler());
300		assert!(router.match_route(&Method::POST, "/hello").is_none());
301	}
302
303	#[test]
304	fn root_path_matches() {
305		let mut router = Router::new();
306		router.insert(Method::GET, "/", dummy_handler());
307		let (_, _) = router.match_route(&Method::GET, "/").unwrap();
308	}
309
310	#[test]
311	fn method_mismatch_on_static_branch_backtracks_to_param_sibling() {
312		let mut router = Router::new();
313		router.insert(Method::GET, "/users/:id", dummy_handler());
314		router.insert(Method::POST, "/users/new", dummy_handler());
315
316		// The static "new" branch matches the path but has no GET handler;
317		// the search must fall back to the ":id" param branch rather than
318		// reporting no match.
319		let (_, params) = router.match_route(&Method::GET, "/users/new").unwrap();
320		assert_eq!(params.0.get("id").unwrap(), "new");
321	}
322
323	#[test]
324	fn static_branch_with_matching_method_still_wins_over_param_sibling() {
325		let mut router = Router::new();
326		router.insert(Method::GET, "/users/:id", dummy_handler());
327		router.insert(Method::GET, "/users/new", dummy_handler());
328
329		let (_, params) = router.match_route(&Method::GET, "/users/new").unwrap();
330		assert!(params.0.is_empty(), "static branch should win, not fall back to :id");
331	}
332
333	#[test]
334	fn allowed_methods_unions_across_ambiguous_branches() {
335		let mut router = Router::new();
336		router.insert(Method::GET, "/users/:id", dummy_handler());
337		router.insert(Method::POST, "/users/new", dummy_handler());
338
339		let mut allowed = router.allowed_methods("/users/new");
340		allowed.sort_by_key(|m| m.to_string());
341		assert_eq!(allowed, vec![Method::GET, Method::POST]);
342	}
343
344	#[test]
345	fn static_path_traversal_borrows_params_without_cloning() {
346		let mut router = Router::new();
347		// Deep static path: /api/v1/users/profile
348		router.insert(Method::GET, "/api/v1/users/profile", dummy_handler());
349		let (_, params) = router.match_route(&Method::GET, "/api/v1/users/profile").unwrap();
350
351		// For a pure static path with no params, params map should be empty.
352		// More importantly, the optimization ensures we don't clone params
353		// while traversing static segments.
354		assert!(params.0.is_empty());
355	}
356
357	#[test]
358	fn param_backtracking_truncates_params_on_failure() {
359		let mut router = Router::new();
360		// Route with param after static segments
361		router.insert(Method::GET, "/users/:id", dummy_handler());
362		// Also register a route that forces backtracking
363		router.insert(Method::POST, "/users/new", dummy_handler());
364
365		// GET /users/new should match the param route (static branch has no GET)
366		let (_, params) = router.match_route(&Method::GET, "/users/new").unwrap();
367		assert_eq!(params.0.get("id").unwrap(), "new");
368
369		// Verify no extra params were left behind from the static branch attempt
370		assert_eq!(params.0.len(), 1);
371	}
372}