use super::error::HttpError;
use super::handler::RouteHandler;
use crate::from_map::MapError;
use crate::from_map::MapValue;
use crate::server::ServerContext;
use crate::ApiEndpoint;
use crate::ApiEndpointBodyContentType;
use http::Method;
use http::StatusCode;
use percent_encoding::percent_decode_str;
use std::collections::BTreeMap;
use std::collections::BTreeSet;
#[derive(Debug)]
pub struct HttpRouter<Context: ServerContext> {
root: Box<HttpRouterNode<Context>>,
}
#[derive(Debug)]
struct HttpRouterNode<Context: ServerContext> {
methods: BTreeMap<String, ApiEndpoint<Context>>,
edges: Option<HttpRouterEdges<Context>>,
}
#[derive(Debug)]
enum HttpRouterEdges<Context: ServerContext> {
Literals(BTreeMap<String, Box<HttpRouterNode<Context>>>),
VariableSingle(String, Box<HttpRouterNode<Context>>),
VariableRest(String, Box<HttpRouterNode<Context>>),
}
#[derive(Debug, PartialEq)]
pub enum PathSegment {
Literal(String),
VarnameSegment(String),
VarnameWildcard(String),
}
impl PathSegment {
pub fn from(segment: &str) -> PathSegment {
if segment.starts_with('{') || segment.ends_with('}') {
assert!(
segment.starts_with('{'),
"{}",
"HTTP URI path segment variable missing leading \"{\""
);
assert!(
segment.ends_with('}'),
"{}",
"HTTP URI path segment variable missing trailing \"}\""
);
let var = &segment[1..segment.len() - 1];
let (var, pat) = if let Some(index) = var.find(':') {
(&var[..index], Some(&var[index + 1..]))
} else {
(var, None)
};
assert!(
!var.is_empty(),
"HTTP URI path segment variable name must not be empty",
);
if let Some(pat) = pat {
assert!(
pat == ".*",
"Only the pattern '.*' is currently supported"
);
PathSegment::VarnameWildcard(var.to_string())
} else {
PathSegment::VarnameSegment(var.to_string())
}
} else {
PathSegment::Literal(segment.to_string())
}
}
}
#[derive(Debug)]
pub struct InputPath<'a>(&'a str);
impl<'a> From<&'a str> for InputPath<'a> {
fn from(s: &'a str) -> Self {
Self(s)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum VariableValue {
String(String),
Components(Vec<String>),
}
pub type VariableSet = BTreeMap<String, VariableValue>;
impl MapValue for VariableValue {
fn as_value(&self) -> Result<&str, MapError> {
match self {
VariableValue::String(s) => Ok(s.as_str()),
VariableValue::Components(_) => Err(MapError(
"cannot deserialize sequence as a single value".to_string(),
)),
}
}
fn as_seq(&self) -> Result<Box<dyn Iterator<Item = String>>, MapError> {
match self {
VariableValue::String(_) => Err(MapError(
"cannot deserialize a single value as a sequence".to_string(),
)),
VariableValue::Components(v) => Ok(Box::new(v.clone().into_iter())),
}
}
}
#[derive(Debug)]
pub struct RouterLookupResult<'a, Context: ServerContext> {
pub handler: &'a dyn RouteHandler<Context>,
pub variables: VariableSet,
pub body_content_type: ApiEndpointBodyContentType,
}
impl<Context: ServerContext> HttpRouterNode<Context> {
pub fn new() -> Self {
HttpRouterNode { methods: BTreeMap::new(), edges: None }
}
}
impl<Context: ServerContext> HttpRouter<Context> {
pub fn new() -> Self {
HttpRouter { root: Box::new(HttpRouterNode::new()) }
}
pub fn insert(&mut self, endpoint: ApiEndpoint<Context>) {
let method = endpoint.method.clone();
let path = endpoint.path.clone();
let all_segments = route_path_to_segments(path.as_str());
let mut all_segments = all_segments.into_iter();
let mut varnames: BTreeSet<String> = BTreeSet::new();
let mut node: &mut Box<HttpRouterNode<Context>> = &mut self.root;
while let Some(raw_segment) = all_segments.next() {
let segment = PathSegment::from(raw_segment);
node = match segment {
PathSegment::Literal(lit) => {
let edges = node.edges.get_or_insert(
HttpRouterEdges::Literals(BTreeMap::new()),
);
match edges {
HttpRouterEdges::VariableSingle(varname, _)
| HttpRouterEdges::VariableRest(varname, _) => {
panic!(
"URI path \"{}\": attempted to register route \
for literal path segment \"{}\" when a route \
exists for variable path segment (variable \
name: \"{}\")",
path, lit, varname
);
}
HttpRouterEdges::Literals(ref mut literals) => literals
.entry(lit)
.or_insert_with(|| Box::new(HttpRouterNode::new())),
}
}
PathSegment::VarnameSegment(new_varname) => {
insert_var(&path, &mut varnames, &new_varname);
let edges = node.edges.get_or_insert(
HttpRouterEdges::VariableSingle(
new_varname.clone(),
Box::new(HttpRouterNode::new()),
),
);
match edges {
HttpRouterEdges::Literals(_) => panic!(
"URI path \"{}\": attempted to register route for \
variable path segment (variable name: \"{}\") \
when a route already exists for a literal path \
segment",
path, new_varname
),
HttpRouterEdges::VariableRest(varname, _) => panic!(
"URI path \"{}\": attempted to register route for \
variable path segment (variable name: \"{}\") \
when a route already exists for the remainder of \
the path as {}",
path, new_varname, varname,
),
HttpRouterEdges::VariableSingle(
varname,
ref mut node,
) => {
if *new_varname != *varname {
panic!(
"URI path \"{}\": attempted to use \
variable name \"{}\", but a different \
name (\"{}\") has already been used for \
this",
path, new_varname, varname
);
}
node
}
}
}
PathSegment::VarnameWildcard(new_varname) => {
if all_segments.next().is_some() {
panic!(
"URI path \"{}\": attempted to match segments \
after the wildcard variable \"{}\"",
path, new_varname,
);
}
insert_var(&path, &mut varnames, &new_varname);
let edges = node.edges.get_or_insert(
HttpRouterEdges::VariableRest(
new_varname.clone(),
Box::new(HttpRouterNode::new()),
),
);
match edges {
HttpRouterEdges::Literals(_) => panic!(
"URI path \"{}\": attempted to register route for \
variable path regex (variable name: \"{}\") when \
a route already exists for a literal path segment",
path, new_varname
),
HttpRouterEdges::VariableSingle(varname, _) => panic!(
"URI path \"{}\": attempted to register route for \
variable path regex (variable name: \"{}\") when \
a route already exists for a segment {}",
path, new_varname, varname,
),
HttpRouterEdges::VariableRest(
varname,
ref mut node,
) => {
if *new_varname != *varname {
panic!(
"URI path \"{}\": attempted to use \
variable name \"{}\", but a different \
name (\"{}\") has already been used for \
this",
path, new_varname, varname
);
}
node
}
}
}
};
}
let methodname = method.as_str().to_uppercase();
if node.methods.get(&methodname).is_some() {
panic!(
"URI path \"{}\": attempted to create duplicate route for \
method \"{}\"",
path, method,
);
}
node.methods.insert(methodname, endpoint);
}
pub fn lookup_route<'a, 'b>(
&'a self,
method: &'b Method,
path: InputPath<'b>,
) -> Result<RouterLookupResult<'a, Context>, HttpError> {
let all_segments = input_path_to_segments(&path).map_err(|_| {
HttpError::for_bad_request(
None,
String::from("invalid path encoding"),
)
})?;
let mut all_segments = all_segments.into_iter();
let mut node = &self.root;
let mut variables = VariableSet::new();
while let Some(segment) = all_segments.next() {
let segment_string = segment.to_string();
node = match &node.edges {
None => None,
Some(HttpRouterEdges::Literals(edges)) => {
edges.get(&segment_string)
}
Some(HttpRouterEdges::VariableSingle(varname, ref node)) => {
variables.insert(
varname.clone(),
VariableValue::String(segment_string),
);
Some(node)
}
Some(HttpRouterEdges::VariableRest(varname, node)) => {
let mut rest = vec![segment];
while let Some(segment) = all_segments.next() {
rest.push(segment);
}
variables.insert(
varname.clone(),
VariableValue::Components(rest),
);
assert!(node.edges.is_none());
Some(node)
}
}
.ok_or_else(|| {
HttpError::for_not_found(
None,
String::from("no route found (no path in router)"),
)
})?
}
match &node.edges {
Some(HttpRouterEdges::VariableRest(varname, new_node)) => {
variables
.insert(varname.clone(), VariableValue::Components(vec![]));
assert!(new_node.edges.is_none());
node = new_node;
}
_ => {}
}
if node.methods.is_empty() {
return Err(HttpError::for_not_found(
None,
String::from("route has no handlers"),
));
}
let methodname = method.as_str().to_uppercase();
node.methods
.get(&methodname)
.map(|handler| RouterLookupResult {
handler: &*handler.handler,
variables,
body_content_type: handler.body_content_type.clone(),
})
.ok_or_else(|| {
HttpError::for_status(None, StatusCode::METHOD_NOT_ALLOWED)
})
}
}
fn insert_var(
path: &str,
varnames: &mut BTreeSet<String>,
new_varname: &String,
) -> () {
if varnames.contains(new_varname) {
panic!(
"URI path \"{}\": variable name \"{}\" is used more than once",
path, new_varname
);
}
varnames.insert(new_varname.clone());
}
impl<'a, Context: ServerContext> IntoIterator for &'a HttpRouter<Context> {
type Item = (String, String, &'a ApiEndpoint<Context>);
type IntoIter = HttpRouterIter<'a, Context>;
fn into_iter(self) -> Self::IntoIter {
HttpRouterIter::new(self)
}
}
pub struct HttpRouterIter<'a, Context: ServerContext> {
method:
Box<dyn Iterator<Item = (&'a String, &'a ApiEndpoint<Context>)> + 'a>,
path: Vec<(PathSegment, Box<PathIter<'a, Context>>)>,
}
type PathIter<'a, Context> =
dyn Iterator<Item = (PathSegment, &'a Box<HttpRouterNode<Context>>)> + 'a;
impl<'a, Context: ServerContext> HttpRouterIter<'a, Context> {
fn new(router: &'a HttpRouter<Context>) -> Self {
HttpRouterIter {
method: Box::new(router.root.methods.iter()),
path: vec![(
PathSegment::Literal("".to_string()),
HttpRouterIter::iter_node(&router.root),
)],
}
}
fn iter_node(
node: &'a HttpRouterNode<Context>,
) -> Box<PathIter<'a, Context>> {
match &node.edges {
Some(HttpRouterEdges::Literals(map)) => Box::new(
map.iter()
.map(|(s, node)| (PathSegment::Literal(s.clone()), node)),
),
Some(HttpRouterEdges::VariableSingle(varname, node)) => {
Box::new(std::iter::once((
PathSegment::VarnameSegment(varname.clone()),
node,
)))
}
Some(HttpRouterEdges::VariableRest(varname, node)) => {
Box::new(std::iter::once((
PathSegment::VarnameSegment(varname.clone()),
node,
)))
}
None => Box::new(std::iter::empty()),
}
}
fn path(&self) -> String {
let components: Vec<String> = self.path[1..]
.iter()
.map(|(c, _)| match c {
PathSegment::Literal(s) => s.clone(),
PathSegment::VarnameSegment(s) => format!("{{{}}}", s),
PathSegment::VarnameWildcard(s) => format!("{{{}:.*}}", s),
})
.collect();
format!("/{}", components.join("/"))
}
}
impl<'a, Context: ServerContext> Iterator for HttpRouterIter<'a, Context> {
type Item = (String, String, &'a ApiEndpoint<Context>);
fn next(&mut self) -> Option<Self::Item> {
if self.path.is_empty() {
return None;
}
loop {
match self.method.next() {
Some((m, ref e)) => break Some((self.path(), m.clone(), e)),
None => {
match self.path.last_mut() {
None => break None,
Some((_, ref mut last)) => match last.next() {
None => {
self.path.pop();
assert!(self.method.next().is_none());
}
Some((path_component, node)) => {
self.path.push((
path_component,
HttpRouterIter::iter_node(node),
));
self.method = Box::new(node.methods.iter());
}
},
}
}
}
}
}
}
fn input_path_to_segments(path: &InputPath) -> Result<Vec<String>, String> {
path.0
.split('/')
.filter(|segment| !segment.is_empty())
.map(|segment| match segment {
"." | ".." => Err("dot-segments are not permitted".to_string()),
_ => Ok(percent_decode_str(segment)
.decode_utf8()
.map_err(|e| e.to_string())?
.to_string()),
})
.collect()
}
pub fn route_path_to_segments(path: &str) -> Vec<&str> {
if !matches!(path.chars().next(), Some('/')) {
panic!("route paths must begin with a '/': '{}'", path);
}
let mut ret = path.split('/').skip(1).collect::<Vec<_>>();
for segment in &ret[..ret.len() - 1] {
if segment.is_empty() {
panic!("path segments may not be empty: '{}'", path);
}
}
if ret[ret.len() - 1] == "" {
ret.pop();
}
ret
}
#[cfg(test)]
mod test {
use super::super::error::HttpError;
use super::super::handler::HttpRouteHandler;
use super::super::handler::RequestContext;
use super::super::handler::RouteHandler;
use super::input_path_to_segments;
use super::HttpRouter;
use super::PathSegment;
use crate::api_description::ApiEndpointBodyContentType;
use crate::from_map::from_map;
use crate::router::VariableValue;
use crate::ApiEndpoint;
use crate::ApiEndpointResponse;
use http::Method;
use http::StatusCode;
use hyper::Body;
use hyper::Response;
use serde::Deserialize;
use std::collections::BTreeMap;
async fn test_handler(
_: RequestContext<()>,
) -> Result<Response<Body>, HttpError> {
panic!("test handler is not supposed to run");
}
fn new_handler() -> Box<dyn RouteHandler<()>> {
HttpRouteHandler::new(test_handler)
}
fn new_handler_named(name: &str) -> Box<dyn RouteHandler<()>> {
HttpRouteHandler::new_with_name(test_handler, name)
}
fn new_endpoint(
handler: Box<dyn RouteHandler<()>>,
method: Method,
path: &str,
) -> ApiEndpoint<()> {
ApiEndpoint {
operation_id: "test_handler".to_string(),
handler,
method,
path: path.to_string(),
parameters: vec![],
body_content_type: ApiEndpointBodyContentType::default(),
response: ApiEndpointResponse::default(),
summary: None,
description: None,
tags: vec![],
extension_mode: Default::default(),
visible: true,
deprecated: false,
}
}
#[test]
#[should_panic(
expected = "HTTP URI path segment variable name must not be empty"
)]
fn test_variable_name_empty() {
let mut router = HttpRouter::new();
router.insert(new_endpoint(new_handler(), Method::GET, "/foo/{}"));
}
#[test]
#[should_panic(
expected = "HTTP URI path segment variable missing trailing \"}\""
)]
fn test_variable_name_bad_end() {
let mut router = HttpRouter::new();
router.insert(new_endpoint(
new_handler(),
Method::GET,
"/foo/{asdf/foo",
));
}
#[test]
#[should_panic(
expected = "HTTP URI path segment variable missing leading \"{\""
)]
fn test_variable_name_bad_start() {
let mut router = HttpRouter::new();
router.insert(new_endpoint(
new_handler(),
Method::GET,
"/foo/asdf}/foo",
));
}
#[test]
#[should_panic(expected = "URI path \"/boo\": attempted to create \
duplicate route for method \"GET\"")]
fn test_duplicate_route1() {
let mut router = HttpRouter::new();
router.insert(new_endpoint(new_handler(), Method::GET, "/boo"));
router.insert(new_endpoint(new_handler(), Method::GET, "/boo"));
}
#[test]
#[should_panic(expected = "URI path \"/foo/bar/\": attempted to create \
duplicate route for method \"GET\"")]
fn test_duplicate_route2() {
let mut router = HttpRouter::new();
router.insert(new_endpoint(new_handler(), Method::GET, "/foo/bar"));
router.insert(new_endpoint(new_handler(), Method::GET, "/foo/bar/"));
}
#[test]
#[should_panic(expected = "path segments may not be empty: '//'")]
fn test_duplicate_route3() {
let mut router = HttpRouter::new();
router.insert(new_endpoint(new_handler(), Method::GET, "/"));
router.insert(new_endpoint(new_handler(), Method::GET, "//"));
}
#[test]
#[should_panic(expected = "URI path \"/projects/{id}/insts/{id}\": \
variable name \"id\" is used more than once")]
fn test_duplicate_varname() {
let mut router = HttpRouter::new();
router.insert(new_endpoint(
new_handler(),
Method::GET,
"/projects/{id}/insts/{id}",
));
}
#[test]
#[should_panic(expected = "URI path \"/projects/{id}\": attempted to use \
variable name \"id\", but a different name \
(\"project_id\") has already been used for \
this")]
fn test_inconsistent_varname() {
let mut router = HttpRouter::new();
router.insert(new_endpoint(
new_handler(),
Method::GET,
"/projects/{project_id}",
));
router.insert(new_endpoint(
new_handler(),
Method::GET,
"/projects/{id}",
));
}
#[test]
#[should_panic(expected = "URI path \"/projects/{id}\": attempted to \
register route for variable path segment \
(variable name: \"id\") when a route already \
exists for a literal path segment")]
fn test_variable_after_literal() {
let mut router = HttpRouter::new();
router.insert(new_endpoint(
new_handler(),
Method::GET,
"/projects/default",
));
router.insert(new_endpoint(
new_handler(),
Method::GET,
"/projects/{id}",
));
}
#[test]
#[should_panic(expected = "URI path \"/projects/default\": attempted to \
register route for literal path segment \
\"default\" when a route exists for variable \
path segment (variable name: \"id\")")]
fn test_literal_after_variable() {
let mut router = HttpRouter::new();
router.insert(new_endpoint(
new_handler(),
Method::GET,
"/projects/{id}",
));
router.insert(new_endpoint(
new_handler(),
Method::GET,
"/projects/default",
));
}
#[test]
#[should_panic(expected = "URI path \"/projects/default\": attempted to \
register route for literal path segment \
\"default\" when a route exists for variable \
path segment (variable name: \"rest\")")]
fn test_literal_after_regex() {
let mut router = HttpRouter::new();
router.insert(new_endpoint(
new_handler(),
Method::GET,
"/projects/{rest:.*}",
));
router.insert(new_endpoint(
new_handler(),
Method::GET,
"/projects/default",
));
}
#[test]
#[should_panic(expected = "Only the pattern '.*' is currently supported")]
fn test_bogus_regex() {
let mut router = HttpRouter::new();
router.insert(new_endpoint(
new_handler(),
Method::GET,
"/word/{rest:[a-z]*}",
));
}
#[test]
#[should_panic(expected = "URI path \"/some/{more:.*}/{stuff}\": \
attempted to match segments after the \
wildcard variable \"more\"")]
fn test_more_after_regex() {
let mut router = HttpRouter::new();
router.insert(new_endpoint(
new_handler(),
Method::GET,
"/some/{more:.*}/{stuff}",
));
}
#[test]
fn test_slash_after_wildcard_is_fine_dot_dot_dot_for_now() {
let mut router = HttpRouter::new();
router.insert(new_endpoint(
new_handler(),
Method::GET,
"/some/{more:.*}/",
));
}
#[test]
fn test_error_cases() {
let mut router = HttpRouter::new();
let error = router.lookup_route(&Method::GET, "/".into()).unwrap_err();
assert_eq!(error.status_code, StatusCode::NOT_FOUND);
let error =
router.lookup_route(&Method::GET, "////".into()).unwrap_err();
assert_eq!(error.status_code, StatusCode::NOT_FOUND);
let error =
router.lookup_route(&Method::GET, "/foo/bar".into()).unwrap_err();
assert_eq!(error.status_code, StatusCode::NOT_FOUND);
let error = router
.lookup_route(&Method::GET, "//foo///bar".into())
.unwrap_err();
assert_eq!(error.status_code, StatusCode::NOT_FOUND);
router.insert(new_endpoint(new_handler(), Method::GET, "/foo/bar"));
assert!(router.lookup_route(&Method::GET, "/foo/bar".into()).is_ok());
assert!(router.lookup_route(&Method::GET, "/foo/bar/".into()).is_ok());
assert!(router.lookup_route(&Method::GET, "//foo/bar".into()).is_ok());
assert!(router.lookup_route(&Method::GET, "//foo//bar".into()).is_ok());
assert!(router
.lookup_route(&Method::GET, "//foo//bar//".into())
.is_ok());
assert!(router
.lookup_route(&Method::GET, "///foo///bar///".into())
.is_ok());
let error = router.lookup_route(&Method::GET, "/".into()).unwrap_err();
assert_eq!(error.status_code, StatusCode::NOT_FOUND);
let error =
router.lookup_route(&Method::GET, "/foo".into()).unwrap_err();
assert_eq!(error.status_code, StatusCode::NOT_FOUND);
let error =
router.lookup_route(&Method::GET, "//foo".into()).unwrap_err();
assert_eq!(error.status_code, StatusCode::NOT_FOUND);
let error = router
.lookup_route(&Method::GET, "/foo/bar/baz".into())
.unwrap_err();
assert_eq!(error.status_code, StatusCode::NOT_FOUND);
let error =
router.lookup_route(&Method::PUT, "/foo/bar".into()).unwrap_err();
assert_eq!(error.status_code, StatusCode::METHOD_NOT_ALLOWED);
let error =
router.lookup_route(&Method::PUT, "/foo/bar/".into()).unwrap_err();
assert_eq!(error.status_code, StatusCode::METHOD_NOT_ALLOWED);
}
#[test]
fn test_router_basic() {
let mut router = HttpRouter::new();
assert!(router.lookup_route(&Method::GET, "/".into()).is_err());
router.insert(new_endpoint(new_handler_named("h1"), Method::GET, "/"));
let result = router.lookup_route(&Method::GET, "/".into()).unwrap();
assert_eq!(result.handler.label(), "h1");
assert!(result.variables.is_empty());
let result = router.lookup_route(&Method::GET, "//".into()).unwrap();
assert_eq!(result.handler.label(), "h1");
assert!(result.variables.is_empty());
let result = router.lookup_route(&Method::GET, "///".into()).unwrap();
assert_eq!(result.handler.label(), "h1");
assert!(result.variables.is_empty());
assert!(router.lookup_route(&Method::PUT, "/".into()).is_err());
router.insert(new_endpoint(new_handler_named("h2"), Method::PUT, "/"));
let result = router.lookup_route(&Method::PUT, "/".into()).unwrap();
assert_eq!(result.handler.label(), "h2");
assert!(result.variables.is_empty());
let result = router.lookup_route(&Method::GET, "/".into()).unwrap();
assert_eq!(result.handler.label(), "h1");
assert!(router.lookup_route(&Method::DELETE, "/".into()).is_err());
assert!(result.variables.is_empty());
assert!(router.lookup_route(&Method::GET, "/foo".into()).is_err());
router.insert(new_endpoint(
new_handler_named("h3"),
Method::GET,
"/foo",
));
let result = router.lookup_route(&Method::PUT, "/".into()).unwrap();
assert_eq!(result.handler.label(), "h2");
assert!(result.variables.is_empty());
let result = router.lookup_route(&Method::GET, "/".into()).unwrap();
assert_eq!(result.handler.label(), "h1");
assert!(result.variables.is_empty());
let result = router.lookup_route(&Method::GET, "/foo".into()).unwrap();
assert_eq!(result.handler.label(), "h3");
assert!(result.variables.is_empty());
let result = router.lookup_route(&Method::GET, "/foo/".into()).unwrap();
assert_eq!(result.handler.label(), "h3");
assert!(result.variables.is_empty());
let result =
router.lookup_route(&Method::GET, "//foo//".into()).unwrap();
assert_eq!(result.handler.label(), "h3");
assert!(result.variables.is_empty());
let result =
router.lookup_route(&Method::GET, "/foo//".into()).unwrap();
assert_eq!(result.handler.label(), "h3");
assert!(result.variables.is_empty());
assert!(router.lookup_route(&Method::PUT, "/foo".into()).is_err());
assert!(router.lookup_route(&Method::PUT, "/foo/".into()).is_err());
assert!(router.lookup_route(&Method::PUT, "//foo//".into()).is_err());
assert!(router.lookup_route(&Method::PUT, "/foo//".into()).is_err());
}
#[test]
fn test_embedded_non_variable() {
let mut router = HttpRouter::new();
assert!(router
.lookup_route(&Method::GET, "/not{a}variable".into())
.is_err());
router.insert(new_endpoint(
new_handler_named("h4"),
Method::GET,
"/not{a}variable",
));
let result = router
.lookup_route(&Method::GET, "/not{a}variable".into())
.unwrap();
assert_eq!(result.handler.label(), "h4");
assert!(result.variables.is_empty());
assert!(router
.lookup_route(&Method::GET, "/not{b}variable".into())
.is_err());
assert!(router
.lookup_route(&Method::GET, "/notnotavariable".into())
.is_err());
}
#[test]
fn test_variables_basic() {
let mut router = HttpRouter::new();
router.insert(new_endpoint(
new_handler_named("h5"),
Method::GET,
"/projects/{project_id}",
));
assert!(router.lookup_route(&Method::GET, "/projects".into()).is_err());
assert!(router
.lookup_route(&Method::GET, "/projects/".into())
.is_err());
let result = router
.lookup_route(&Method::GET, "/projects/p12345".into())
.unwrap();
assert_eq!(result.handler.label(), "h5");
assert_eq!(
result.variables.keys().collect::<Vec<&String>>(),
vec!["project_id"]
);
assert_eq!(
*result.variables.get("project_id").unwrap(),
VariableValue::String("p12345".to_string())
);
assert!(router
.lookup_route(&Method::GET, "/projects/p12345/child".into())
.is_err());
let result = router
.lookup_route(&Method::GET, "/projects/p12345/".into())
.unwrap();
assert_eq!(result.handler.label(), "h5");
assert_eq!(
*result.variables.get("project_id").unwrap(),
VariableValue::String("p12345".to_string())
);
let result = router
.lookup_route(&Method::GET, "/projects///p12345//".into())
.unwrap();
assert_eq!(result.handler.label(), "h5");
assert_eq!(
*result.variables.get("project_id").unwrap(),
VariableValue::String("p12345".to_string())
);
let result = router
.lookup_route(&Method::GET, "/projects/{project_id}".into())
.unwrap();
assert_eq!(result.handler.label(), "h5");
assert_eq!(
*result.variables.get("project_id").unwrap(),
VariableValue::String("{project_id}".to_string())
);
}
#[test]
fn test_variables_multi() {
let mut router = HttpRouter::new();
router.insert(new_endpoint(
new_handler_named("h6"),
Method::GET,
"/projects/{project_id}/instances/{instance_id}/fwrules/\
{fwrule_id}/info",
));
let result = router
.lookup_route(
&Method::GET,
"/projects/p1/instances/i2/fwrules/fw3/info".into(),
)
.unwrap();
assert_eq!(result.handler.label(), "h6");
assert_eq!(
result.variables.keys().collect::<Vec<&String>>(),
vec!["fwrule_id", "instance_id", "project_id"]
);
assert_eq!(
*result.variables.get("project_id").unwrap(),
VariableValue::String("p1".to_string())
);
assert_eq!(
*result.variables.get("instance_id").unwrap(),
VariableValue::String("i2".to_string())
);
assert_eq!(
*result.variables.get("fwrule_id").unwrap(),
VariableValue::String("fw3".to_string())
);
}
#[test]
fn test_empty_variable() {
let mut router = HttpRouter::new();
router.insert(new_endpoint(
new_handler_named("h7"),
Method::GET,
"/projects/{project_id}/instances",
));
assert!(router
.lookup_route(&Method::GET, "/projects/instances".into())
.is_err());
assert!(router
.lookup_route(&Method::GET, "/projects//instances".into())
.is_err());
assert!(router
.lookup_route(&Method::GET, "/projects///instances".into())
.is_err());
let result = router
.lookup_route(&Method::GET, "/projects/foo/instances".into())
.unwrap();
assert_eq!(result.handler.label(), "h7");
}
#[test]
fn test_variables_glob() {
let mut router = HttpRouter::new();
router.insert(new_endpoint(
new_handler_named("h8"),
Method::OPTIONS,
"/console/{path:.*}",
));
let result = router
.lookup_route(&Method::OPTIONS, "/console/missiles/launch".into())
.unwrap();
assert_eq!(
result.variables.get("path"),
Some(&VariableValue::Components(vec![
"missiles".to_string(),
"launch".to_string()
]))
);
}
#[test]
fn test_variable_rename() {
#[derive(Deserialize)]
#[allow(dead_code)]
struct MyPath {
#[serde(rename = "type")]
t: String,
#[serde(rename = "ref")]
r: String,
#[serde(rename = "@")]
at: String,
}
let mut router = HttpRouter::new();
router.insert(new_endpoint(
new_handler_named("h8"),
Method::OPTIONS,
"/{type}/{ref}/{@}",
));
let result = router
.lookup_route(&Method::OPTIONS, "/console/missiles/launch".into())
.unwrap();
let path =
from_map::<MyPath, VariableValue>(&result.variables).unwrap();
assert_eq!(path.t, "console");
assert_eq!(path.r, "missiles");
assert_eq!(path.at, "launch");
}
#[test]
fn test_iter_null() {
let router = HttpRouter::<()>::new();
let ret: Vec<_> = router.into_iter().map(|x| (x.0, x.1)).collect();
assert_eq!(ret, vec![]);
}
#[test]
fn test_iter() {
let mut router = HttpRouter::new();
router.insert(new_endpoint(
new_handler_named("root"),
Method::GET,
"/",
));
router.insert(new_endpoint(
new_handler_named("i"),
Method::GET,
"/projects/{project_id}/instances",
));
let ret: Vec<_> = router.into_iter().map(|x| (x.0, x.1)).collect();
assert_eq!(
ret,
vec![
("/".to_string(), "GET".to_string(),),
(
"/projects/{project_id}/instances".to_string(),
"GET".to_string(),
),
]
);
}
#[test]
fn test_iter2() {
let mut router = HttpRouter::new();
router.insert(new_endpoint(
new_handler_named("root_get"),
Method::GET,
"/",
));
router.insert(new_endpoint(
new_handler_named("root_post"),
Method::POST,
"/",
));
let ret: Vec<_> = router.into_iter().map(|x| (x.0, x.1)).collect();
assert_eq!(
ret,
vec![
("/".to_string(), "GET".to_string(),),
("/".to_string(), "POST".to_string(),),
]
);
}
#[test]
fn test_segments() {
let segs =
input_path_to_segments(&"//foo/bar/baz%2fbuzz".into()).unwrap();
assert_eq!(segs, vec!["foo", "bar", "baz/buzz"]);
}
#[test]
fn test_path_segment() {
let seg = PathSegment::from("abc");
assert_eq!(seg, PathSegment::Literal("abc".to_string()));
let seg = PathSegment::from("{words}");
assert_eq!(seg, PathSegment::VarnameSegment("words".to_string()));
let seg = PathSegment::from("{rest:.*}");
assert_eq!(seg, PathSegment::VarnameWildcard("rest".to_string()),);
}
#[test]
#[should_panic]
fn test_bad_path_segment1() {
let _ = PathSegment::from("{foo");
}
#[test]
#[should_panic]
fn test_bad_path_segment2() {
let _ = PathSegment::from("bar}");
}
#[test]
#[should_panic]
fn test_bad_path_segment3() {
let _ = PathSegment::from("{}");
}
#[test]
#[should_panic]
fn test_bad_path_segment4() {
let _ = PathSegment::from("{varname:abc+}");
}
#[test]
fn test_map() {
#[derive(Deserialize)]
struct A {
bbb: String,
ccc: Vec<String>,
}
let mut map = BTreeMap::new();
map.insert(
"bbb".to_string(),
VariableValue::String("doggos".to_string()),
);
map.insert(
"ccc".to_string(),
VariableValue::Components(vec![
"lizzie".to_string(),
"brickley".to_string(),
]),
);
match from_map::<A, VariableValue>(&map) {
Ok(a) => {
assert_eq!(a.bbb, "doggos");
assert_eq!(a.ccc, vec!["lizzie", "brickley"]);
}
Err(s) => panic!("unexpected error: {}", s),
}
}
#[test]
fn test_map_bad_value() {
#[allow(dead_code)]
#[derive(Deserialize)]
struct A {
bbb: String,
}
let mut map = BTreeMap::new();
map.insert(
"bbb".to_string(),
VariableValue::Components(vec![
"lizzie".to_string(),
"brickley".to_string(),
]),
);
match from_map::<A, VariableValue>(&map) {
Ok(_) => panic!("unexpected success"),
Err(s) => {
assert_eq!(s, "cannot deserialize sequence as a single value")
}
}
}
#[test]
fn test_map_bad_seq() {
#[allow(dead_code)]
#[derive(Deserialize)]
struct A {
bbb: Vec<String>,
}
let mut map = BTreeMap::new();
map.insert(
"bbb".to_string(),
VariableValue::String("doggos".to_string()),
);
match from_map::<A, VariableValue>(&map) {
Ok(_) => panic!("unexpected success"),
Err(s) => {
assert_eq!(s, "cannot deserialize a single value as a sequence")
}
}
}
}