use crate::handler::{boxed, BoxHandler, IntoHandler};
use http::Method;
use std::collections::HashMap;
#[non_exhaustive]
pub enum Match {
Found {
handler: BoxHandler,
params: HashMap<String, String>,
},
MethodNotAllowed {
allow: Vec<Method>,
},
NotFound,
BadPath,
}
#[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 decoded = match decode_segments(path) {
Some(d) => d,
None => return Match::BadPath,
};
let segments: Vec<&str> = decoded.iter().map(String::as_str).collect();
let mut params = HashMap::new();
let exact = Self::walk(&self.root, &segments, 0, &mut params);
if let Some(node) = exact {
if let Some(h) = node.handlers.0.get(method) {
return Match::Found {
handler: h.clone(),
params,
};
}
}
let exact_allow: Vec<Method> = exact
.map(|n| n.handlers.0.keys().cloned().collect())
.unwrap_or_default();
params.clear();
match Self::walk_wildcard(&self.root, &segments, 0, method, &mut params) {
Some(found @ Match::Found { .. }) => found,
Some(Match::MethodNotAllowed { allow: wild_allow }) => {
let mut allow = exact_allow;
for m in wild_allow {
if !allow.contains(&m) {
allow.push(m);
}
}
Match::MethodNotAllowed { allow }
}
_ if !exact_allow.is_empty() => Match::MethodNotAllowed { allow: exact_allow },
_ => Match::NotFound,
}
}
pub fn methods_for(&self, path: &str) -> Vec<Method> {
let decoded = match decode_segments(path) {
Some(d) => d,
None => return Vec::new(),
};
let segments: Vec<&str> = decoded.iter().map(String::as_str).collect();
let mut params = HashMap::new();
let mut out: Vec<Method> = Self::walk(&self.root, &segments, 0, &mut params)
.map(|n| n.handlers.0.keys().cloned().collect())
.unwrap_or_default();
params.clear();
if let Some(Match::MethodNotAllowed { allow }) =
Self::walk_wildcard(&self.root, &segments, 0, &Method::TRACE, &mut params)
{
for m in allow {
if !out.contains(&m) {
out.push(m);
}
}
}
out
}
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()
}
fn decode_segments(path: &str) -> Option<Vec<String>> {
split_segments(path)
.into_iter()
.map(crate::path::decode_path_segment)
.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"),
}
}
fn build_shadowed() -> Router {
let mut r = Router::new();
{
let mut b = RouteBuilder::new(&mut r);
b.get("/files/{path...}", |c: Call| async move {
format!("wild:{}", c.param_raw("path").unwrap_or(""))
});
b.get("/files/special/x", |_c: Call| async { "static" });
b.post("/files/only-post", |_c: Call| async { "posted" });
}
r
}
#[test]
fn path_params_are_percent_decoded() {
let mut r = Router::new();
{
let mut b = RouteBuilder::new(&mut r);
b.get("/u/{name}", |_c: Call| async { "" });
}
match r.route(&Method::GET, "/u/John%20Doe") {
Match::Found { params, .. } => assert_eq!(params.get("name").unwrap(), "John Doe"),
_ => panic!("expected a match"),
}
}
#[test]
fn encoded_slash_does_not_create_a_segment() {
let mut r = Router::new();
{
let mut b = RouteBuilder::new(&mut r);
b.get("/u/{name}", |_c: Call| async { "" });
}
match r.route(&Method::GET, "/u/a%2Fb") {
Match::Found { params, .. } => assert_eq!(params.get("name").unwrap(), "a/b"),
_ => panic!("%2F must not manufacture a separator"),
}
}
#[test]
fn a_route_with_a_literal_space_is_reachable_encoded() {
let mut r = Router::new();
{
let mut b = RouteBuilder::new(&mut r);
b.get("/a b", |_c: Call| async { "" });
}
assert!(matches!(
r.route(&Method::GET, "/a%20b"),
Match::Found { .. }
));
}
#[test]
fn malformed_encoding_is_a_bad_path() {
let mut r = Router::new();
{
let mut b = RouteBuilder::new(&mut r);
b.get("/u/{name}", |_c: Call| async { "" });
}
assert!(matches!(r.route(&Method::GET, "/u/%zz"), Match::BadPath));
assert!(matches!(r.route(&Method::GET, "/u/%FF"), Match::BadPath));
}
#[test]
fn wildcard_captures_decoded_segments() {
let mut r = Router::new();
{
let mut b = RouteBuilder::new(&mut r);
b.get("/f/{rest...}", |_c: Call| async { "" });
}
match r.route(&Method::GET, "/f/a%20b/c") {
Match::Found { params, .. } => assert_eq!(params.get("rest").unwrap(), "a b/c"),
_ => panic!("expected the wildcard to match"),
}
}
#[test]
fn wildcard_is_reachable_through_a_static_sibling() {
let r = build_shadowed();
match run(&r, Method::GET, "/files/special") {
Match::Found { params, .. } => {
assert_eq!(params.get("path").unwrap(), "special");
}
_ => panic!("wildcard should serve /files/special"),
}
}
#[test]
fn exact_match_still_wins_over_wildcard() {
let r = build_shadowed();
match run(&r, Method::GET, "/files/special/x") {
Match::Found { params, .. } => {
assert!(
!params.contains_key("path"),
"the static route captured a wildcard param"
);
}
_ => panic!("expected the static route"),
}
}
#[test]
fn allow_header_unions_exact_and_wildcard_methods() {
let r = build_shadowed();
match run(&r, Method::DELETE, "/files/only-post") {
Match::MethodNotAllowed { allow } => {
assert!(allow.contains(&Method::POST), "missing the exact method");
assert!(allow.contains(&Method::GET), "missing the wildcard method");
}
_ => panic!("expected 405"),
}
}
#[test]
fn abandoned_branch_params_do_not_leak_into_the_wildcard() {
let mut r = Router::new();
{
let mut b = RouteBuilder::new(&mut r);
b.post("/u/{id}/edit", |_c: Call| async { "edit" });
b.get("/u/{rest...}", |_c: Call| async { "wild" });
}
match r.route(&Method::GET, "/u/7/edit") {
Match::Found { params, .. } => {
assert_eq!(params.get("rest").unwrap(), "7/edit");
assert!(
!params.contains_key("id"),
"stale `id` leaked from the abandoned walk"
);
}
_ => panic!("the wildcard should have matched"),
}
}
#[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"),
}
}
}