use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Method {
GET,
POST,
PUT,
DELETE,
PATCH,
HEAD,
OPTIONS,
}
impl Method {
pub fn from_hyper(method: &hyper::Method) -> Option<Self> {
match *method {
hyper::Method::GET => Some(Method::GET),
hyper::Method::POST => Some(Method::POST),
hyper::Method::PUT => Some(Method::PUT),
hyper::Method::DELETE => Some(Method::DELETE),
hyper::Method::PATCH => Some(Method::PATCH),
hyper::Method::HEAD => Some(Method::HEAD),
hyper::Method::OPTIONS => Some(Method::OPTIONS),
_ => None,
}
}
}
pub type Params = HashMap<String, String>;
#[derive(Debug, Clone, PartialEq)]
enum Segment {
Static(String),
Param(String),
}
#[derive(Debug, Clone)]
pub struct Route {
segments: Vec<Segment>,
raw_path: String,
}
impl Route {
pub fn new(path: &str) -> Self {
let segments = Self::parse_path(path);
Self {
segments,
raw_path: path.to_string(),
}
}
fn parse_path(path: &str) -> Vec<Segment> {
path.split('/')
.filter(|s| !s.is_empty())
.map(|segment| {
if let Some(stripped) = segment.strip_prefix(':') {
Segment::Param(stripped.to_string())
} else {
Segment::Static(segment.to_string())
}
})
.collect()
}
pub fn matches(&self, path: &str) -> Option<Params> {
let path_segments: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
if path_segments.len() != self.segments.len() {
return None;
}
let mut params = HashMap::new();
for (route_seg, path_seg) in self.segments.iter().zip(path_segments.iter()) {
match route_seg {
Segment::Static(expected) => {
if expected != path_seg {
return None;
}
}
Segment::Param(name) => {
params.insert(name.clone(), path_seg.to_string());
}
}
}
Some(params)
}
pub fn path(&self) -> &str {
&self.raw_path
}
fn specificity(&self) -> usize {
self.segments
.iter()
.filter(|s| matches!(s, Segment::Static(_)))
.count()
}
fn static_key(&self) -> Option<String> {
let mut parts: Vec<&str> = Vec::with_capacity(self.segments.len());
for seg in &self.segments {
match seg {
Segment::Static(s) => parts.push(s),
Segment::Param(_) => return None,
}
}
Some(parts.join("/"))
}
}
fn normalize_path(path: &str) -> String {
path.split('/')
.filter(|s| !s.is_empty())
.collect::<Vec<_>>()
.join("/")
}
#[derive(Debug, Clone)]
pub struct RouterEntry {
pub method: Method,
pub route: Route,
pub handler_id: usize,
}
#[derive(Debug)]
pub struct Router {
routes: Vec<RouterEntry>,
static_index: HashMap<(Method, String), usize>,
dynamic: Vec<RouterEntry>,
}
impl Router {
pub fn new() -> Self {
Self {
routes: Vec::new(),
static_index: HashMap::new(),
dynamic: Vec::new(),
}
}
pub fn add_route(&mut self, method: Method, path: &str, handler_id: usize) {
let route = Route::new(path);
let entry = RouterEntry {
method,
route: route.clone(),
handler_id,
};
match route.static_key() {
Some(key) => {
self.static_index.entry((method, key)).or_insert(handler_id);
}
None => self.dynamic.push(entry.clone()),
}
self.routes.push(entry);
}
pub fn find_route(&self, method: Method, path: &str) -> Option<(usize, Params)> {
let key = normalize_path(path);
if let Some(&handler_id) = self.static_index.get(&(method, key)) {
return Some((handler_id, Params::new()));
}
let mut best: Option<(usize, Params, usize)> = None;
for entry in &self.dynamic {
if entry.method == method {
if let Some(params) = entry.route.matches(path) {
let spec = entry.route.specificity();
let better = match &best {
Some((_, _, best_spec)) => spec > *best_spec,
None => true,
};
if better {
best = Some((entry.handler_id, params, spec));
}
}
}
}
best.map(|(id, params, _)| (id, params))
}
pub fn routes(&self) -> &[RouterEntry] {
&self.routes
}
}
impl Default for Router {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_static_route() {
let route = Route::new("/users");
assert!(route.matches("/users").is_some());
assert!(route.matches("/posts").is_none());
assert!(route.matches("/users/123").is_none());
}
#[test]
fn test_single_param() {
let route = Route::new("/users/:id");
let params = route.matches("/users/123");
assert!(params.is_some());
let params = params.unwrap();
assert_eq!(params.get("id"), Some(&"123".to_string()));
}
#[test]
fn test_multiple_params() {
let route = Route::new("/users/:userId/posts/:postId");
let params = route.matches("/users/42/posts/100");
assert!(params.is_some());
let params = params.unwrap();
assert_eq!(params.get("userId"), Some(&"42".to_string()));
assert_eq!(params.get("postId"), Some(&"100".to_string()));
}
#[test]
fn test_no_match_different_length() {
let route = Route::new("/users/:id");
assert!(route.matches("/users").is_none());
assert!(route.matches("/users/123/posts").is_none());
}
#[test]
fn test_router_add_and_find() {
let mut router = Router::new();
router.add_route(Method::GET, "/users", 0);
router.add_route(Method::GET, "/users/:id", 1);
router.add_route(Method::POST, "/users", 2);
let result = router.find_route(Method::GET, "/users");
assert!(result.is_some());
let (handler_id, params) = result.unwrap();
assert_eq!(handler_id, 0);
assert!(params.is_empty());
let result = router.find_route(Method::GET, "/users/123");
assert!(result.is_some());
let (handler_id, params) = result.unwrap();
assert_eq!(handler_id, 1);
assert_eq!(params.get("id"), Some(&"123".to_string()));
let result = router.find_route(Method::PUT, "/users");
assert!(result.is_none());
}
#[test]
fn test_root_path() {
let route = Route::new("/");
assert!(route.matches("/").is_some());
assert!(route.matches("/users").is_none());
}
#[test]
fn test_mixed_static_and_params() {
let route = Route::new("/api/v1/users/:id/profile");
let params = route.matches("/api/v1/users/123/profile");
assert!(params.is_some());
let params = params.unwrap();
assert_eq!(params.get("id"), Some(&"123".to_string()));
assert_eq!(params.len(), 1);
}
#[test]
fn static_route_beats_param_regardless_of_order() {
let mut r = Router::new();
r.add_route(Method::GET, "/users/:id", 1);
r.add_route(Method::GET, "/users/me", 2);
let (id, _) = r.find_route(Method::GET, "/users/me").unwrap();
assert_eq!(id, 2, "static /users/me should win over /users/:id");
let (id, params) = r.find_route(Method::GET, "/users/42").unwrap();
assert_eq!(id, 1);
assert_eq!(params.get("id"), Some(&"42".to_string()));
}
#[test]
fn trailing_slash_is_ignored() {
let mut r = Router::new();
r.add_route(Method::GET, "/users", 1);
assert!(r.find_route(Method::GET, "/users/").is_some());
assert!(r.find_route(Method::GET, "/users").is_some());
}
#[test]
fn no_match_returns_none() {
let mut r = Router::new();
r.add_route(Method::GET, "/users/me", 1);
assert!(r.find_route(Method::GET, "/posts").is_none());
assert!(r.find_route(Method::POST, "/users/me").is_none());
}
#[test]
fn many_static_routes_resolve_correctly() {
let mut r = Router::new();
for i in 0..500 {
r.add_route(Method::GET, &format!("/route/{i}"), i);
}
let (id, params) = r.find_route(Method::GET, "/route/250").unwrap();
assert_eq!(id, 250);
assert!(params.is_empty());
assert!(r.find_route(Method::GET, "/route/999").is_none());
}
#[test]
fn duplicate_static_route_keeps_first_registration() {
let mut r = Router::new();
r.add_route(Method::GET, "/dup", 1);
r.add_route(Method::GET, "/dup", 2);
let (id, _) = r.find_route(Method::GET, "/dup").unwrap();
assert_eq!(id, 1);
}
#[test]
fn root_path_uses_static_index() {
let mut r = Router::new();
r.add_route(Method::GET, "/", 7);
let (id, _) = r.find_route(Method::GET, "/").unwrap();
assert_eq!(id, 7);
}
#[test]
fn method_is_part_of_the_static_key() {
let mut r = Router::new();
r.add_route(Method::GET, "/x", 1);
r.add_route(Method::POST, "/x", 2);
assert_eq!(r.find_route(Method::GET, "/x").unwrap().0, 1);
assert_eq!(r.find_route(Method::POST, "/x").unwrap().0, 2);
}
}