use crate::handler::{boxed, BoxHandler, IntoHandler};
use http::Method;
use std::collections::HashMap;
pub enum Match {
Found {
handler: BoxHandler,
params: HashMap<String, String>,
},
MethodNotAllowed {
allow: Vec<Method>,
},
NotFound,
}
#[derive(Default)]
struct Node {
statics: HashMap<String, Node>,
param: Option<(String, Box<Node>)>, wildcard: Option<(String, BoxHandlers)>, handlers: BoxHandlers,
}
#[derive(Default)]
struct BoxHandlers(HashMap<Method, BoxHandler>);
impl std::fmt::Debug for Node {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Node").finish_non_exhaustive()
}
}
#[derive(Debug, Default)]
pub struct Router {
root: Node,
}
impl Router {
pub fn new() -> Self {
Self::default()
}
pub fn add(&mut self, method: Method, pattern: &str, handler: BoxHandler) {
let mut node = &mut self.root;
let segments: Vec<&str> = split_segments(pattern);
for (i, seg) in segments.iter().enumerate() {
if let Some(name) = seg.strip_prefix('{').and_then(|s| s.strip_suffix("...}")) {
assert!(
i == segments.len() - 1,
"wildcard `{{{name}...}}` must be last segment"
);
let entry = node
.wildcard
.get_or_insert_with(|| (name.to_string(), BoxHandlers::default()));
entry.1 .0.insert(method, handler);
return;
} else if let Some(name) = seg.strip_prefix('{').and_then(|s| s.strip_suffix('}')) {
let entry = node
.param
.get_or_insert_with(|| (name.to_string(), Box::new(Node::default())));
node = entry.1.as_mut();
} else {
node = node.statics.entry(seg.to_string()).or_default();
}
}
node.handlers.0.insert(method, handler);
}
pub fn route(&self, method: &Method, path: &str) -> Match {
let segments = split_segments(path);
let mut params = HashMap::new();
match Self::walk(&self.root, &segments, 0, &mut params) {
Some(node) => match node.handlers.0.get(method) {
Some(h) => Match::Found {
handler: h.clone(),
params,
},
None if node.handlers.0.is_empty() => Match::NotFound,
None => Match::MethodNotAllowed {
allow: node.handlers.0.keys().cloned().collect(),
},
},
None => {
if let Some(m) = Self::walk_wildcard(&self.root, &segments, 0, method, &mut params)
{
m
} else {
Match::NotFound
}
}
}
}
fn walk<'a>(
node: &'a Node,
segs: &[&str],
i: usize,
params: &mut HashMap<String, String>,
) -> Option<&'a Node> {
if i == segs.len() {
return Some(node);
}
let seg = segs[i];
if let Some(child) = node.statics.get(seg) {
if let Some(n) = Self::walk(child, segs, i + 1, params) {
return Some(n);
}
}
if let Some((name, child)) = &node.param {
params.insert(name.clone(), seg.to_string());
if let Some(n) = Self::walk(child, segs, i + 1, params) {
return Some(n);
}
params.remove(name);
}
None
}
fn walk_wildcard(
node: &Node,
segs: &[&str],
i: usize,
method: &Method,
params: &mut HashMap<String, String>,
) -> Option<Match> {
if let Some((name, handlers)) = &node.wildcard {
let rest = segs[i..].join("/");
params.insert(name.clone(), rest);
return Some(match handlers.0.get(method) {
Some(h) => Match::Found {
handler: h.clone(),
params: std::mem::take(params),
},
None => Match::MethodNotAllowed {
allow: handlers.0.keys().cloned().collect(),
},
});
}
if i < segs.len() {
if let Some(child) = node.statics.get(segs[i]) {
if let Some(m) = Self::walk_wildcard(child, segs, i + 1, method, params) {
return Some(m);
}
}
if let Some((pname, child)) = &node.param {
params.insert(pname.clone(), segs[i].to_string());
if let Some(m) = Self::walk_wildcard(child, segs, i + 1, method, params) {
return Some(m);
}
params.remove(pname);
}
}
None
}
}
fn split_segments(path: &str) -> Vec<&str> {
path.split('/').filter(|s| !s.is_empty()).collect()
}
pub struct RouteBuilder<'r> {
router: &'r mut Router,
prefix: String,
}
impl<'r> RouteBuilder<'r> {
pub(crate) fn new(router: &'r mut Router) -> Self {
Self {
router,
prefix: String::new(),
}
}
fn full(&self, path: &str) -> String {
let mut p = self.prefix.clone();
if !path.starts_with('/') {
p.push('/');
}
p.push_str(path);
p
}
pub fn method<Marker, H>(&mut self, method: Method, path: &str, handler: H) -> &mut Self
where
H: IntoHandler<Marker>,
{
let full = self.full(path);
self.router
.add(method, &full, boxed(handler.into_handler()));
self
}
pub fn get<Marker, H>(&mut self, path: &str, handler: H) -> &mut Self
where
H: IntoHandler<Marker>,
{
self.method(Method::GET, path, handler)
}
pub fn post<Marker, H>(&mut self, path: &str, handler: H) -> &mut Self
where
H: IntoHandler<Marker>,
{
self.method(Method::POST, path, handler)
}
pub fn put<Marker, H>(&mut self, path: &str, handler: H) -> &mut Self
where
H: IntoHandler<Marker>,
{
self.method(Method::PUT, path, handler)
}
pub fn delete<Marker, H>(&mut self, path: &str, handler: H) -> &mut Self
where
H: IntoHandler<Marker>,
{
self.method(Method::DELETE, path, handler)
}
pub fn route(&mut self, path: &str, f: impl FnOnce(&mut RouteBuilder)) -> &mut Self {
let prefix = self.full(path);
let mut child = RouteBuilder {
router: self.router,
prefix,
};
f(&mut child);
self
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::call::Call;
use bytes::Bytes;
use http::{HeaderMap, StatusCode, Uri};
fn build() -> Router {
let mut r = Router::new();
{
let mut b = RouteBuilder::new(&mut r);
b.get("/", |_c: Call| async { "root" });
b.route("/users", |b| {
b.get("/{id}", |c: Call| async move {
format!("user {}", c.param_raw("id").unwrap())
});
b.post("/", |_c: Call| async { (StatusCode::CREATED, "created") });
});
b.get("/files/{path...}", |c: Call| async move {
format!("file {}", c.param_raw("path").unwrap())
});
}
r
}
fn run(r: &Router, m: Method, path: &str) -> Match {
r.route(&m, path)
}
#[tokio::test]
async fn matches_static_and_param() {
let r = build();
match run(&r, Method::GET, "/users/7") {
Match::Found { handler, params } => {
assert_eq!(params.get("id").unwrap(), "7");
let mut c = Call::new(
Method::GET,
"/users/7".parse::<Uri>().unwrap(),
HeaderMap::new(),
Bytes::new(),
);
c.set_params(params);
let res = handler.handle(c).await;
assert_eq!(res.body, Bytes::from("user 7"));
}
_ => panic!("expected Found"),
}
}
#[test]
fn unknown_path_is_not_found() {
let r = build();
assert!(matches!(run(&r, Method::GET, "/nope"), Match::NotFound));
}
#[test]
fn known_path_wrong_method_is_405() {
let r = build();
match run(&r, Method::DELETE, "/users/7") {
Match::MethodNotAllowed { allow } => assert!(allow.contains(&Method::GET)),
_ => panic!("expected 405"),
}
}
#[test]
fn wildcard_captures_rest() {
let r = build();
match run(&r, Method::GET, "/files/a/b/c.txt") {
Match::Found { params, .. } => assert_eq!(params.get("path").unwrap(), "a/b/c.txt"),
_ => panic!("expected wildcard Found"),
}
}
#[tokio::test]
async fn route_builder_accepts_boxed_handler() {
let pre: BoxHandler = boxed((|_c: Call| async { "pre-boxed" }).into_handler());
let mut r = Router::new();
{
let mut b = RouteBuilder::new(&mut r);
b.get("/pre", pre);
}
match run(&r, Method::GET, "/pre") {
Match::Found { handler, .. } => {
let c = Call::new(
Method::GET,
"/pre".parse::<Uri>().unwrap(),
HeaderMap::new(),
Bytes::new(),
);
let res = handler.handle(c).await;
assert_eq!(res.body, Bytes::from("pre-boxed"));
}
_ => panic!("expected Found"),
}
}
}