use std::cmp::{min, Eq, Ordering};
use std::collections::HashMap;
use std::hash::Hash;
use std::mem;
use std::ops::Index;
use std::str;
pub struct Router<K: Eq + Hash, V> {
pub map: HashMap<K, Node<V>>,
}
impl<K: Eq + Hash, V> Default for Router<K, V> {
fn default() -> Self {
Router {
map: HashMap::new(),
}
}
}
impl<K: Eq + Hash, V> Router<K, V> {
pub fn with_capacity(capacity: usize) -> Self {
Router {
map: HashMap::with_capacity(capacity),
}
}
pub fn add(&mut self, key: K, value: V, path: &str) {
if !path.starts_with('/') {
panic!("path must begin with '/' in path '{}'", path);
}
self
.map
.entry(key)
.or_insert_with(Node::default)
.add_route(path, value);
}
pub fn lookup(&mut self, key: &K, path: &str) -> Result<RouteLookup<V>, bool> {
self
.map
.get_mut(key)
.map(|n| n.get_value(path))
.unwrap_or(Err(false))
}
}
pub struct RouteLookup<'a, V> {
pub value: &'a V,
pub params: Params,
pub parent_values: Vec<&'a V>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Param {
pub key: String,
pub value: String,
}
impl Param {
pub fn new(key: &str, value: &str) -> Param {
Param {
key: key.to_string(),
value: value.to_string(),
}
}
}
#[derive(Debug, PartialEq)]
pub struct Params(pub Vec<Param>);
impl Default for Params {
fn default() -> Self {
Params(Vec::new())
}
}
impl Params {
pub fn by_name(&self, name: &str) -> Option<&str> {
match self.0.iter().find(|param| param.key == name) {
Some(param) => Some(¶m.value),
None => None,
}
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn push(&mut self, p: Param) {
self.0.push(p);
}
}
impl Index<usize> for Params {
type Output = str;
fn index(&self, i: usize) -> &Self::Output {
&(self.0)[i].value
}
}
#[derive(PartialEq, PartialOrd, Debug)]
pub enum NodeType {
Static,
Root,
Param,
CatchAll,
}
pub struct Node<V> {
path: Vec<u8>,
wild_child: bool,
node_type: NodeType,
indices: Vec<u8>,
children: Vec<Box<Node<V>>>,
value: Option<V>,
priority: u32,
}
impl<V> Default for Node<V> {
fn default() -> Self {
Node {
path: Vec::new(),
wild_child: false,
node_type: NodeType::Static,
indices: Vec::new(),
children: Vec::new(),
value: None,
priority: 0,
}
}
}
impl<V> Node<V> {
fn increment_child_prio(&mut self, pos: usize) -> usize {
self.children[pos].priority += 1;
let prio = self.children[pos].priority;
let mut new_pos = pos;
while new_pos > 0 && self.children[new_pos - 1].priority < prio {
self.children.swap(new_pos - 1, new_pos);
new_pos -= 1;
}
if new_pos != pos {
self.indices = [
&self.indices[..new_pos],
&self.indices[pos..pos + 1],
&self.indices[new_pos..pos],
&self.indices[pos + 1..],
]
.concat();
}
new_pos
}
pub fn add_route(&mut self, path: &str, value: V) {
let full_path = <&str>::clone(&path);
self.priority += 1;
if self.path.is_empty() && self.children.is_empty() {
self.insert_child(path.as_ref(), full_path, value);
self.node_type = NodeType::Root;
return;
}
self.add_route_helper(path.as_ref(), full_path, value);
}
fn add_route_helper(&mut self, mut path: &[u8], full_path: &str, value: V) {
let mut i = 0;
let max = min(path.len(), self.path.len());
while i < max && path[i] == self.path[i] {
i += 1;
}
if i < self.path.len() {
let mut child = Node {
path: self.path[i..].to_vec(),
wild_child: self.wild_child,
indices: self.indices.clone(),
value: self.value.take(),
priority: self.priority - 1,
..Node::default()
};
mem::swap(&mut self.children, &mut child.children);
self.children = vec![Box::new(child)];
self.indices = vec![self.path[i]];
self.path = path[..i].to_vec();
self.wild_child = false;
self.value = None;
}
match path.len().cmp(&i) {
Ordering::Greater => {
path = &path[i..];
if self.wild_child {
return self.children[0].wild_child_conflict(path, full_path, value);
}
let idxc = path[0];
if self.node_type == NodeType::Param && idxc == b'/' && self.children.len() == 1 {
self.children[0].priority += 1;
return self.children[0].add_route_helper(path, full_path, value);
}
for mut i in 0..self.indices.len() {
if idxc == self.indices[i] {
i = self.increment_child_prio(i);
return self.children[i].add_route_helper(path, full_path, value);
}
}
if idxc != b':' && idxc != b'*' {
self.indices.push(idxc);
self.children.push(Box::new(Node::default()));
let i = self.increment_child_prio(self.indices.len() - 1);
return self.children[i].insert_child(path, full_path, value);
}
self.insert_child(path, full_path, value)
}
_ => {
if self.value.is_some() {
panic!("a value is already registered for path '{}'", full_path);
}
self.value = Some(value);
}
}
}
fn wild_child_conflict(&mut self, path: &[u8], full_path: &str, value: V) {
self.priority += 1;
if path.len() >= self.path.len()
&& self.path == &path[..self.path.len()]
&& self.node_type != NodeType::CatchAll
&& (self.path.len() >= path.len() || path[self.path.len()] == b'/')
{
self.add_route_helper(path, full_path, value);
} else {
let path_seg = if self.node_type == NodeType::CatchAll {
str::from_utf8(path).unwrap()
} else {
str::from_utf8(path).unwrap().splitn(2, '/').next().unwrap()
};
let prefix = format!(
"{}{}",
&full_path[..full_path.find(path_seg).unwrap()],
str::from_utf8(&self.path).unwrap(),
);
panic!(
"'{}' in new path '{}' conflicts with existing wildcard '{}' in existing prefix '{}'",
path_seg,
full_path,
str::from_utf8(&self.path).unwrap(),
prefix
);
}
}
fn insert_child(&mut self, mut path: &[u8], full_path: &str, value: V) {
let (wildcard, wildcard_index, valid) = find_wildcard(path);
let wildcard = match wildcard_index {
Some(_) => wildcard.unwrap(),
None => {
self.value = Some(value);
self.path = path.to_vec();
return;
}
};
let mut wildcard_index = wildcard_index.unwrap();
if !valid {
panic!(
"only one wildcard per path segment is allowed, has: '{}' in path '{}'",
str::from_utf8(wildcard).unwrap(),
full_path
);
};
if wildcard.len() < 2 {
panic!(
"wildcards must be named with a non-empty name in path '{}'",
full_path
);
}
if !self.children.is_empty() {
panic!(
"wildcard segment '{}' conflicts with existing children in path '{}'",
str::from_utf8(wildcard).unwrap(),
full_path
)
}
if wildcard[0] == b':' {
if wildcard_index > 0 {
self.path = path[..wildcard_index].to_vec();
path = &path[wildcard_index..];
}
let child = Node {
node_type: NodeType::Param,
path: wildcard.to_vec(),
..Node::default()
};
self.wild_child = true;
self.children = vec![Box::new(child)];
self.children[0].priority += 1;
if wildcard.len() < path.len() {
path = &path[wildcard.len()..];
let child = Node {
priority: 1,
..Node::default()
};
self.children[0].children = vec![Box::new(child)];
return self.children[0].children[0].insert_child(path, full_path, value);
}
self.children[0].value = Some(value);
return;
}
if wildcard_index + wildcard.len() != path.len() {
panic!(
"catch-all routes are only allowed at the end of the path in path '{}'",
full_path
);
}
if !self.path.is_empty() && self.path[self.path.len() - 1] == b'/' {
panic!(
"catch-all conflicts with existing value for the path segment root in path '{}'",
full_path
);
}
wildcard_index -= 1;
if path[wildcard_index] != b'/' {
panic!("no / before catch-all in path '{}'", full_path);
}
let child = Node {
wild_child: true,
node_type: NodeType::CatchAll,
..Node::default()
};
self.path = path[..wildcard_index].to_vec();
self.children = vec![Box::new(child)];
self.indices = vec![b'/'];
self.children[0].priority += 1;
let child = Node {
path: path[wildcard_index..].to_vec(),
node_type: NodeType::CatchAll,
value: Some(value),
priority: 1,
..Node::default()
};
self.children[0].children = vec![Box::new(child)];
}
pub fn get_value(&self, path: &str) -> Result<RouteLookup<V>, bool> {
self.get_value_helper(path.as_ref(), Params::default(), Vec::new())
}
fn get_value_helper<'a>(
&'a self,
mut path: &[u8],
params: Params,
mut parent_values: Vec<&'a V>,
) -> Result<RouteLookup<V>, bool> {
let prefix = self.path.clone();
if path.len() > prefix.len() {
if prefix == &path[..prefix.len()] {
path = &path[prefix.len()..];
if !self.wild_child {
let idxc = path[0];
for i in 0..self.indices.len() {
if idxc == self.indices[i] {
parent_values = self.collect_parents(parent_values);
return self.children[i].get_value_helper(path, params, parent_values);
}
}
let tsr = path == [b'/'] && self.value.is_some();
return Err(tsr);
}
parent_values = self.collect_parents(parent_values);
return self.children[0].handle_wild_child(path, params, parent_values);
}
} else if path == prefix {
if let Some(value) = self.value.as_ref() {
return Ok(RouteLookup {
value,
params,
parent_values,
});
}
if path == [b'/'] && self.wild_child && self.node_type != NodeType::Root {
return Err(true);
}
for i in 0..self.indices.len() {
if self.indices[i] == b'/' {
let tsr = (prefix.len() == 1 && self.children[i].value.is_some())
|| (self.children[i].node_type == NodeType::CatchAll
&& self.children[i].children[0].value.is_some());
return Err(tsr);
}
}
return Err(false);
}
let tsr = (path == [b'/'])
|| (prefix.len() == path.len() + 1
&& prefix[path.len()] == b'/'
&& path == &prefix[..prefix.len() - 1]
&& self.value.is_some());
Err(tsr)
}
fn handle_wild_child<'a>(
&'a self,
mut path: &[u8],
mut params: Params,
mut parent_values: Vec<&'a V>,
) -> Result<RouteLookup<V>, bool> {
match self.node_type {
NodeType::Param => {
let mut end = 0;
while end < path.len() && path[end] != b'/' {
end += 1;
}
params.push(Param {
key: String::from_utf8(self.path[1..].to_vec()).unwrap(),
value: String::from_utf8(path[..end].to_vec()).unwrap(),
});
if end < path.len() {
if !self.children.is_empty() {
path = &path[end..];
parent_values = self.collect_parents(parent_values);
return self.children[0].get_value_helper(path, params, parent_values);
}
let tsr = path.len() == end + 1;
return Err(tsr);
}
if let Some(value) = self.value.as_ref() {
return Ok(RouteLookup {
value,
params,
parent_values,
});
} else if self.children.len() == 1 {
let tsr = self.children[0].path == [b'/'] && self.children[0].value.is_some();
return Err(tsr);
}
Err(false)
}
NodeType::CatchAll => {
params.push(Param {
key: String::from_utf8(self.path[2..].to_vec()).unwrap(),
value: String::from_utf8(path.to_vec()).unwrap(),
});
match self.value.as_ref() {
Some(value) => Ok(RouteLookup {
value,
params,
parent_values,
}),
None => Err(false),
}
}
_ => panic!("invalid node type"),
}
}
fn collect_parents<'a>(&'a self, mut values: Vec<&'a V>) -> Vec<&'a V> {
if let Some(value) = self.value.as_ref() {
values.push(value)
};
values
}
pub fn find_case_insensitive_path(&self, path: &str, fix_trailing_slash: bool) -> Option<String> {
let mut insensitive_path = Vec::with_capacity(path.len() + 1);
let found = self.find_case_insensitive_path_helper(
path.as_bytes(),
&mut insensitive_path,
[0; 4],
fix_trailing_slash,
);
match found {
true => Some(String::from_utf8(insensitive_path).unwrap()),
false => None,
}
}
fn find_case_insensitive_path_helper(
&self,
mut path: &[u8],
insensitive_path: &mut Vec<u8>,
mut buf: [u8; 4],
fix_trailing_slash: bool,
) -> bool {
let lower_path: &[u8] = &path.to_ascii_lowercase();
if lower_path.len() >= self.path.len()
&& (self.path.is_empty()
|| lower_path[1..self.path.len()].eq_ignore_ascii_case(&self.path[1..]))
{
insensitive_path.append(&mut self.path.clone());
path = &path[self.path.len()..];
if !path.is_empty() {
let cached_lower_path = <&[u8]>::clone(&lower_path);
if !self.wild_child {
buf = shift_n_bytes(buf, self.path.len());
if buf[0] != 0 {
for i in 0..self.indices.len() {
if self.indices[i] == buf[0] {
return self.children[i].find_case_insensitive_path_helper(
path,
insensitive_path,
buf,
fix_trailing_slash,
);
}
}
} else {
let mut current_char = 0 as char;
let mut off = 0;
for j in 0..min(self.path.len(), 3) {
let i = self.path.len() - j;
if char_start(cached_lower_path[i]) {
current_char = str::from_utf8(&cached_lower_path[i..])
.unwrap()
.chars()
.next()
.unwrap();
off = j;
break;
}
}
current_char.encode_utf8(&mut buf);
buf = shift_n_bytes(buf, off);
for i in 0..self.indices.len() {
if self.indices[i] == buf[0] {
if self.children[i].find_case_insensitive_path_helper(
path,
insensitive_path,
buf,
fix_trailing_slash,
) {
return true;
}
if insensitive_path.len() > self.children[i].path.len() {
let prev_len = insensitive_path.len() - self.children[i].path.len();
insensitive_path.truncate(prev_len);
}
break;
}
}
let up = current_char.to_ascii_uppercase();
if up != current_char {
up.encode_utf8(&mut buf);
buf = shift_n_bytes(buf, off);
for i in 0..self.indices.len() {
if self.indices[i] == buf[0] {
return self.children[i].find_case_insensitive_path_helper(
path,
insensitive_path,
buf,
fix_trailing_slash,
);
}
}
}
}
return fix_trailing_slash && path == [b'/'] && self.value.is_some();
}
return self.children[0].find_case_insensitive_path_match_helper(
path,
insensitive_path,
buf,
fix_trailing_slash,
);
} else {
if self.value.is_some() {
return true;
}
if fix_trailing_slash {
for i in 0..self.indices.len() {
if self.indices[i] == b'/' {
if (self.children[i].path.len() == 1 && self.children[i].value.is_some())
|| (self.children[i].node_type == NodeType::CatchAll
&& self.children[i].children[0].value.is_some())
{
insensitive_path.push(b'/');
return true;
}
return false;
}
}
}
return false;
}
}
if fix_trailing_slash {
if path == [b'/'] {
return true;
}
if lower_path.len() + 1 == self.path.len()
&& self.path[lower_path.len()] == b'/'
&& lower_path[1..].eq_ignore_ascii_case(&self.path[1..lower_path.len()])
&& self.value.is_some()
{
insensitive_path.append(&mut self.path.clone());
return true;
}
}
false
}
fn find_case_insensitive_path_match_helper(
&self,
mut path: &[u8],
insensitive_path: &mut Vec<u8>,
buf: [u8; 4],
fix_trailing_slash: bool,
) -> bool {
match self.node_type {
NodeType::Param => {
let mut end = 0;
while end < path.len() && path[end] != b'/' {
end += 1;
}
let mut path_k = path[..end].to_vec();
insensitive_path.append(&mut path_k);
if end < path.len() {
if !self.children.is_empty() {
path = &path[end..];
return self.children[0].find_case_insensitive_path_helper(
path,
insensitive_path,
buf,
fix_trailing_slash,
);
}
if fix_trailing_slash && path.len() == end + 1 {
return true;
}
return false;
}
if self.value.is_some() {
return true;
} else if fix_trailing_slash
&& self.children.len() == 1
&& self.children[0].path == [b'/']
&& self.children[0].value.is_some()
{
insensitive_path.push(b'/');
return true;
}
false
}
NodeType::CatchAll => {
insensitive_path.append(&mut path.to_vec());
true
}
_ => panic!("invalid node type"),
}
}
}
fn shift_n_bytes(bytes: [u8; 4], n: usize) -> [u8; 4] {
match n {
0 => bytes,
1 => [bytes[1], bytes[2], bytes[3], 0],
2 => [bytes[2], bytes[3], 0, 0],
3 => [bytes[3], 0, 0, 0],
_ => [0; 4],
}
}
fn char_start(b: u8) -> bool {
b & 0xC0 != 0x80
}
fn find_wildcard(path: &[u8]) -> (Option<&[u8]>, Option<usize>, bool) {
for (start, &c) in path.iter().enumerate() {
if c != b':' && c != b'*' {
continue;
};
let mut valid = true;
for (end, &c) in path[start + 1..].iter().enumerate() {
match c {
b'/' => return (Some(&path[start..start + 1 + end]), Some(start), valid),
b':' | b'*' => valid = false,
_ => (),
};
}
return (Some(&path[start..]), Some(start), valid);
}
(None, None, false)
}
#[cfg(test)]
mod tests {
use super::*;
use std::panic;
use std::sync::Mutex;
struct TestRequest {
path: &'static str,
should_be_nil: bool,
route: &'static str,
params: Params,
}
impl TestRequest {
pub fn new(
path: &'static str,
should_be_nil: bool,
route: &'static str,
params: Params,
) -> TestRequest {
TestRequest {
path,
should_be_nil,
route,
params,
}
}
}
type TestRequests = Vec<TestRequest>;
fn check_requests<T: Fn() -> String>(tree: &mut Node<T>, requests: TestRequests) {
for request in requests {
let res = tree.get_value(request.path);
match res {
Err(_) => {
if !request.should_be_nil {
panic!("Expected non-nil value for route '{}'", request.path);
}
}
Ok(result) => {
if request.should_be_nil {
panic!("Expected nil value for route '{}'", request.path);
}
let value = (result.value)();
if value != request.route {
panic!(
"Wrong value for route '{}'. Expected '{}', found '{}')",
request.path, value, request.route
);
}
assert_eq!(
result.params, request.params,
"Wrong params for route '{}'",
request.path
);
}
};
}
}
fn check_priorities<F: Fn() -> String>(n: &mut Node<F>) -> u32 {
let mut prio: u32 = 0;
for i in 0..n.children.len() {
prio += check_priorities(&mut *n.children[i]);
}
if n.value.is_some() {
prio += 1;
}
if n.priority != prio {
panic!(
"priority mismatch for node '{}': found '{}', expected '{}'",
str::from_utf8(&n.path).unwrap(),
n.priority,
prio
)
}
prio
}
fn fake_value(val: &'static str) -> impl Fn() -> String {
move || val.to_string()
}
#[test]
fn params() {
let params = Params(vec![
Param {
key: "hello".to_owned(),
value: "world".to_owned(),
},
Param {
key: "rust-is".to_string(),
value: "awesome".to_string(),
},
]);
assert_eq!(params.by_name("hello"), Some("world"));
assert_eq!(params.by_name("rust-is"), Some("awesome"));
}
#[test]
fn test_tree_add_and_get() {
let mut tree = Node::default();
let routes = vec![
"/hi",
"/contact",
"/co",
"/c",
"/a",
"/ab",
"/doc/",
"/doc/go_faq.html",
"/doc/go1.html",
"/ʯ",
"/β",
];
for route in routes {
tree.add_route(route, fake_value(route));
}
check_requests(
&mut tree,
vec![
TestRequest::new("/a", false, "/a", Params::default()),
TestRequest::new("/", true, "", Params::default()),
TestRequest::new("/hi", false, "/hi", Params::default()),
TestRequest::new("/contact", false, "/contact", Params::default()),
TestRequest::new("/co", false, "/co", Params::default()),
TestRequest::new("/con", true, "", Params::default()),
TestRequest::new("/cona", true, "", Params::default()),
TestRequest::new("/no", true, "", Params::default()),
TestRequest::new("/ab", false, "/ab", Params::default()),
TestRequest::new("/ʯ", false, "/ʯ", Params::default()),
TestRequest::new("/β", false, "/β", Params::default()),
],
);
check_priorities(&mut tree);
}
#[test]
fn test_tree_wildcard() {
let mut tree = Node::default();
let routes = vec![
"/",
"/cmd/:tool/:sub",
"/cmd/:tool/",
"/src/*filepath",
"/search/",
"/search/:query",
"/user_:name",
"/user_:name/about",
"/files/:dir/*filepath",
"/doc/",
"/doc/go_faq.html",
"/doc/go1.html",
"/info/:user/public",
"/info/:user/project/:project",
];
for route in routes {
tree.add_route(route, fake_value(route));
}
check_requests(
&mut tree,
vec![
TestRequest::new("/", false, "/", Params::default()),
TestRequest::new(
"/cmd/test/",
false,
"/cmd/:tool/",
Params(vec![Param::new("tool", "test")]),
),
TestRequest::new(
"/cmd/test",
true,
"",
Params(vec![Param::new("tool", "test")]),
),
TestRequest::new(
"/cmd/test/3",
false,
"/cmd/:tool/:sub",
Params(vec![Param::new("tool", "test"), Param::new("sub", "3")]),
),
TestRequest::new(
"/src/",
false,
"/src/*filepath",
Params(vec![Param::new("filepath", "/")]),
),
TestRequest::new(
"/src/some/file.png",
false,
"/src/*filepath",
Params(vec![Param::new("filepath", "/some/file.png")]),
),
TestRequest::new("/search/", false, "/search/", Params::default()),
TestRequest::new(
"/search/someth!ng+in+ünìcodé",
false,
"/search/:query",
Params(vec![Param::new("query", "someth!ng+in+ünìcodé")]),
),
TestRequest::new(
"/search/someth!ng+in+ünìcodé/",
true,
"",
Params(vec![Param::new("query", "someth!ng+in+ünìcodé")]),
),
TestRequest::new(
"/user_rustacean",
false,
"/user_:name",
Params(vec![Param::new("name", "rustacean")]),
),
TestRequest::new(
"/user_rustacean/about",
false,
"/user_:name/about",
Params(vec![Param::new("name", "rustacean")]),
),
TestRequest::new(
"/files/js/inc/framework.js",
false,
"/files/:dir/*filepath",
Params(vec![
Param::new("dir", "js"),
Param::new("filepath", "/inc/framework.js"),
]),
),
TestRequest::new(
"/info/gordon/public",
false,
"/info/:user/public",
Params(vec![Param::new("user", "gordon")]),
),
TestRequest::new(
"/info/gordon/project/go",
false,
"/info/:user/project/:project",
Params(vec![
Param::new("user", "gordon"),
Param::new("project", "go"),
]),
),
],
);
check_priorities(&mut tree);
}
type TestRoute = (&'static str, bool);
fn test_routes(routes: Vec<TestRoute>) {
let tree = Mutex::new(Node::default());
for route in routes {
let recv = panic::catch_unwind(|| {
let mut guard = match tree.lock() {
Ok(guard) => guard,
Err(poisoned) => poisoned.into_inner(),
};
guard.add_route(route.0, ());
});
if route.1 {
if recv.is_ok() {
}
} else if recv.is_err() {
panic!("unexpected panic for route '{}': {:?}", route.0, recv);
}
}
}
#[test]
fn test_tree_wildcard_conflict() {
let routes = vec![
("/cmd/:tool/:sub", false),
("/cmd/vet", true),
("/src/*filepath", false),
("/src/*filepathx", true),
("/src/", true),
("/src1/", false),
("/src1/*filepath", true),
("/src2*filepath", true),
("/search/:query", false),
("/search/invalid", true),
("/user_:name", false),
("/user_x", true),
("/user_:name", true),
("/id:id", false),
("/id/:id", true),
];
test_routes(routes);
}
#[test]
fn test_tree_child_conflict() {
let routes = vec![
("/cmd/vet", false),
("/cmd/:tool/:sub", true),
("/src/AUTHORS", false),
("/src/*filepath", true),
("/user_x", false),
("/user_:name", true),
("/id/:id", false),
("/id:id", true),
("/:id", true),
("/*filepath", true),
];
test_routes(routes);
}
#[test]
fn test_tree_duplicate_path() {
let tree = Mutex::new(Node::default());
let routes = vec![
"/",
"/doc/",
"/src/*filepath",
"/search/:query",
"/user_:name",
];
for route in routes {
let mut recv = panic::catch_unwind(|| {
let mut guard = match tree.lock() {
Ok(guard) => guard,
Err(poisoned) => poisoned.into_inner(),
};
guard.add_route(route, fake_value(route));
});
if recv.is_err() {
panic!("panic inserting route '{}': {:?}", route, recv);
}
recv = panic::catch_unwind(|| {
let mut guard = match tree.lock() {
Ok(guard) => guard,
Err(poisoned) => poisoned.into_inner(),
};
guard.add_route(route, fake_value(route));
});
if recv.is_ok() {
panic!("no panic while inserting duplicate route '{}'", route);
}
}
check_requests(
&mut tree.lock().unwrap_or_else(|poisoned| poisoned.into_inner()),
vec![
TestRequest::new("/", false, "/", Params::default()),
TestRequest::new("/doc/", false, "/doc/", Params::default()),
TestRequest::new(
"/src/some/file.png",
false,
"/src/*filepath",
Params(vec![Param::new("filepath", "/some/file.png")]),
),
TestRequest::new(
"/search/someth!ng+in+ünìcodé",
false,
"/search/:query",
Params(vec![Param::new("query", "someth!ng+in+ünìcodé")]),
),
TestRequest::new(
"/user_rustacean",
false,
"/user_:name",
Params(vec![Param::new("name", "rustacean")]),
),
],
);
}
#[test]
fn test_empty_wildcard_name() {
let tree = Mutex::new(Node::default());
let routes = vec!["/user:", "/user:/", "/cmd/:/", "/src/*"];
for route in routes {
let recv = panic::catch_unwind(|| {
let mut guard = match tree.lock() {
Ok(guard) => guard,
Err(poisoned) => poisoned.into_inner(),
};
guard.add_route(route, fake_value(route));
});
if recv.is_ok() {
panic!(
"no panic while inserting route with empty wildcard name '{}",
route
);
}
}
}
#[test]
fn test_tree_catch_all_conflict() {
let routes = vec![
("/src/*filepath/x", true),
("/src2/", false),
("/src2/*filepath/x", true),
];
test_routes(routes);
}
#[test]
fn test_tree_catch_all_conflict_root() {
let routes = vec![("/", false), ("/*filepath", true)];
test_routes(routes);
}
#[test]
fn test_tree_double_wildcard() {
let panic_msg = "only one wildcard per path segment is allowed";
let routes = vec!["/:foo:bar", "/:foo:bar/", "/:foo*bar"];
for route in routes {
let tree = Mutex::new(Node::default());
let recv = panic::catch_unwind(|| {
let mut guard = match tree.lock() {
Ok(guard) => guard,
Err(poisoned) => poisoned.into_inner(),
};
guard.add_route(route, fake_value(route));
});
if recv.is_ok() {
panic!(panic_msg);
}
}
}
#[test]
fn test_tree_trailing_slash_redirect() {
let tree = Mutex::new(Node::default());
let routes = vec![
"/hi",
"/b/",
"/search/:query",
"/cmd/:tool/",
"/src/*filepath",
"/x",
"/x/y",
"/y/",
"/y/z",
"/0/:id",
"/0/:id/1",
"/1/:id/",
"/1/:id/2",
"/aa",
"/a/",
"/admin",
"/admin/:category",
"/admin/:category/:page",
"/doc",
"/doc/go_faq.html",
"/doc/go1.html",
"/no/a",
"/no/b",
"/api/hello/:name",
];
for route in routes {
let recv = panic::catch_unwind(|| {
let mut guard = match tree.lock() {
Ok(guard) => guard,
Err(poisoned) => poisoned.into_inner(),
};
guard.add_route(route, fake_value(route));
});
if recv.is_err() {
panic!("panic inserting route '{}': {:?}", route, recv);
}
}
let tsr_routes = vec![
"/hi/",
"/b",
"/search/rustacean/",
"/cmd/vet",
"/src",
"/x/",
"/y",
"/0/go/",
"/1/go",
"/a",
"/admin/",
"/admin/config/",
"/admin/config/permissions/",
"/doc/",
];
for route in tsr_routes {
let guard = match tree.lock() {
Ok(guard) => guard,
Err(poisoned) => poisoned.into_inner(),
};
let res = guard.get_value(route);
match res {
Ok(_) => {
panic!("non-nil value for TSR route '{}'", route);
}
Err(tsr) => {
if !tsr {
panic!("expected TSR recommendation for route '{}'", route);
}
}
}
}
let no_tsr_routes = vec!["/", "/no", "/no/", "/_", "/_/", "/api/world/abc"];
for route in no_tsr_routes {
let guard = match tree.lock() {
Ok(guard) => guard,
Err(poisoned) => poisoned.into_inner(),
};
let res = guard.get_value(route);
match res {
Ok(_) => {
panic!("non-nil value for TSR route '{}'", route);
}
Err(tsr) => {
if tsr {
panic!("expected no TSR recommendation for route '{}'", route);
}
}
}
}
}
#[test]
fn test_tree_root_trailing_slash_redirect() {
let mut tree = Node::default();
tree.add_route("/:test", fake_value("/:test"));
let res = tree.get_value("/");
match res {
Ok(_) => {
panic!("non-nil value for route '/'");
}
Err(tsr) => {
if tsr {
panic!("expected no TSR recommendation for route '/'");
}
}
}
}
#[test]
fn test_tree_find_case_insensitive_path() {
let mut tree = Node::default();
let routes = vec![
"/hi",
"/b/",
"/ABC/",
"/search/:query",
"/cmd/:tool/",
"/src/*filepath",
"/x",
"/x/y",
"/y/",
"/y/z",
"/0/:id",
"/0/:id/1",
"/1/:id/",
"/1/:id/2",
"/aa",
"/a/",
"/doc",
"/doc/go_faq.html",
"/doc/go1.html",
"/doc/go/away",
"/no/a",
"/no/b",
"/Π",
"/u/apfêl/",
"/u/äpfêl/",
"/u/öpfêl",
"/v/Äpfêl/",
"/v/Öpfêl",
"/w/♬",
"/w/♭/",
"/w/𠜎",
"/w/𠜏/",
"/loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong",
];
for route in &routes {
tree.add_route(route, fake_value(route));
}
for route in &routes {
let out = tree.find_case_insensitive_path(route, true);
match out {
None => panic!("Route '{}' not found!", route),
Some(out) => {
if out != *route {
panic!("Wrong result for route '{}': {}", route, out);
}
}
};
}
for route in &routes {
let out = tree.find_case_insensitive_path(route, false);
match out {
None => panic!("Route '{}' not found!", route),
Some(out) => {
if out != *route {
panic!("Wrong result for route '{}': {}", route, out);
}
}
};
}
let tests = vec![
("/HI", "/hi", false),
("/HI/", "/hi", true),
("/B", "/b/", true),
("/B/", "/b/", false),
("/abc", "/ABC/", true),
("/abc/", "/ABC/", false),
("/aBc", "/ABC/", true),
("/aBc/", "/ABC/", false),
("/abC", "/ABC/", true),
("/abC/", "/ABC/", false),
("/SEARCH/QUERY", "/search/QUERY", false),
("/SEARCH/QUERY/", "/search/QUERY", true),
("/CMD/TOOL/", "/cmd/TOOL/", false),
("/CMD/TOOL", "/cmd/TOOL/", true),
("/SRC/FILE/PATH", "/src/FILE/PATH", false),
("/x/Y", "/x/y", false),
("/x/Y/", "/x/y", true),
("/X/y", "/x/y", false),
("/X/y/", "/x/y", true),
("/X/Y", "/x/y", false),
("/X/Y/", "/x/y", true),
("/Y/", "/y/", false),
("/Y", "/y/", true),
("/Y/z", "/y/z", false),
("/Y/z/", "/y/z", true),
("/Y/Z", "/y/z", false),
("/Y/Z/", "/y/z", true),
("/y/Z", "/y/z", false),
("/y/Z/", "/y/z", true),
("/Aa", "/aa", false),
("/Aa/", "/aa", true),
("/AA", "/aa", false),
("/AA/", "/aa", true),
("/aA", "/aa", false),
("/aA/", "/aa", true),
("/A/", "/a/", false),
("/A", "/a/", true),
("/DOC", "/doc", false),
("/DOC/", "/doc", true),
("/NO", "", true),
("/DOC/GO", "", true),
("/w/♬/", "/w/♬", true),
("/w/♭", "/w/♭/", true),
("/w/𠜎/", "/w/𠜎", true),
("/w/𠜏", "/w/𠜏/", true),
(
"/lOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOng/",
"/loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong",
true),
];
struct Test {
inn: &'static str,
out: &'static str,
slash: bool,
};
let tests: Vec<Test> = tests
.into_iter()
.map(|test| Test {
inn: test.0,
out: test.1,
slash: test.2,
})
.collect();
for test in &tests {
let res = tree.find_case_insensitive_path(test.inn, true);
match res {
None => (),
Some(res) => {
if res != test.out {
panic!("Wrong result for route '{}': {}", res, test.out);
}
}
};
}
for test in &tests {
let res = tree.find_case_insensitive_path(test.inn, false);
match res {
None => (),
Some(res) => {
if test.slash {
panic!("Found without fixTrailingSlash: {}; got {}", test.inn, res);
}
if res != test.out {
panic!("Wrong result for route '{}': {}", res, test.out);
}
}
};
}
}
#[test]
#[should_panic(expected = "conflicts with existing wildcard")]
fn test_tree_wildcard_conflict_ex() {
let conflicts = vec![
"/who/are/foo",
"/who/are/foo/",
"/who/are/foo/bar",
"/conxxx",
"xxx",
"/conooo/xxx",
];
for conflict in conflicts {
let mut tree = Node::default();
let routes = vec!["/con:tact", "/who/are/*you", "/who/foo/hello"];
for route in routes {
tree.add_route(route, fake_value(route));
}
tree.add_route(conflict, fake_value(conflict));
}
}
#[test]
fn test_tree_get_parent_values() {
let requests = vec![
("/", vec![]),
("/users", vec!["/"]),
("/users/:id", vec!["/", "/users"]),
("/users/:id/edit", vec!["/", "/users", "/users/:id"]),
("/blog", vec!["/"]),
("/blog/pages", vec!["/", "/blog"]),
("/blog/pages/:id", vec!["/", "/blog", "/blog/pages"]),
("/t/:id/other/another", vec!["/"]),
("/other/:id", vec!["/"]),
("/userst/other/another", vec!["/", "/users"]),
("/wild/*wildcard", vec!["/"]),
];
let mut tree = Node::default();
for request in &requests {
tree.add_route(request.0, fake_value(request.0))
}
for request in requests {
let res = tree.get_value(request.0);
if let Ok(res) = res {
assert_eq!(
res
.parent_values
.iter()
.map(|x| x())
.collect::<Vec<String>>(),
request.1
);
}
}
}
}