#[doc(hidden)]
pub use lazy_static;
#[doc(hidden)]
pub use regex::{Regex, Captures};
#[doc(hidden)]
pub use hyper::Method;
use std::fmt;
use std::collections::BTreeMap;
#[derive(Debug, PartialEq)]
pub enum Error {
NotFound,
MethodNotAllowed,
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", match self {
&Error::NotFound => "Not found",
&Error::MethodNotAllowed => "Method not allowed",
})
}
}
impl std::error::Error for Error {}
pub fn pattern(src: &str) -> String {
use lazy_static::lazy_static;
lazy_static! {
static ref TAIL: Regex = Regex::new(r"^\\\*|/\\\*").unwrap();
static ref NAMED: Regex = Regex::new(
":([0-9a-zA-Z][_0-9a-zA-Z]*)"
).unwrap();
}
let dest = regex::escape(src);
let dest = TAIL.replace(&dest, "(?P<__tail__>/.*)");
let dest = NAMED.replace_all(&dest, |caps: &Captures| {
format!("(?P<{}>[^/]+)", &caps[1])
});
format!("^{}$", dest)
}
#[derive(Debug, Clone)]
pub struct Params {
named: BTreeMap<String, String>,
tail: Option<String>,
}
impl Params {
pub fn get<'a>(&'a self, name: &str) -> Option<&'a str> {
self.named.get(name).map(|s| &s[..])
}
pub fn tail(&self) -> Option<&str> {
self.tail.as_ref().map(|s| &s[..])
}
pub fn len(&self) -> usize {
self.named.len() + (if self.tail.is_some() { 1 } else { 0 })
}
pub fn is_empty(&self) -> bool {
self.named.is_empty() && self.tail.is_none()
}
pub fn from_captures<'t>(
names: regex::CaptureNames,
caps: Captures<'t>
) -> Self {
let mut named = BTreeMap::new();
let mut tail = None;
for (name, value) in names.zip(caps.iter()) {
match name {
Some("__tail__") => {
tail = value.map(|s| s.as_str().to_string());
},
Some(name) => {
if let Some(value) = value {
named.insert(
name.to_string(),
value.as_str().to_string()
);
}
},
None => (),
}
}
Params { named, tail }
}
}
#[macro_export]
macro_rules! match_request {
($request_method:expr, $request_path:expr, {
$($pattern:literal => {
$($method:ident => $result:expr),* $(,)?
}),* $(,)?
}) => {{
let result = $crate::_match_request_regex!(
$request_method,
$request_path,
{
$($crate::pattern($pattern) => {
$($method => $result),*
}),*
}
);
result.map(move |(value, names, captures)| {
(value, $crate::Params::from_captures(
names,
captures
))
})
}};
}
#[macro_export]
macro_rules! match_request_regex {
($request_method:expr, $request_path:expr, {
$($pattern:expr => {
$($method:ident => $result:expr),* $(,)?
}),* $(,)?
}) => {{
let result = $crate::_match_request_regex!(
$request_method,
$request_path,
{$($pattern => {$($method => $result),*}),*}
);
result.map(move |(value, _names, captures)| {
(value, captures)
})
}};
}
#[doc(hidden)]
#[macro_export]
macro_rules! _match_request_regex {
($request_method:expr, $request_path:expr, {
$($pattern:expr => {
$($method:ident => $value:expr),* $(,)?
}),* $(,)?
}) => {{
use $crate::{Error, Regex, Method};
use $crate::lazy_static::lazy_static;
let path = $request_path;
let method = $request_method;
loop {
$({
lazy_static! {
static ref RE: Regex = Regex::new(
$pattern.as_ref()
).unwrap();
}
if let Some(captures) = RE.captures(path) {
match method {
$(&Method::$method => {
break Ok((
$value,
RE.capture_names(),
captures,
));
},)*
_ => {
break Err(Error::MethodNotAllowed);
},
}
}
};)*
break Err(Error::NotFound);
}
}};
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn match_exact_path() {
let result = match_request!(&Method::GET, "/bar", {
"/foo" => {
GET => 111
},
"/bar" => {
GET => 222
},
"/baz" => {
GET => 333
}
});
let (value, params) = result.unwrap();
assert!(params.is_empty());
assert_eq!(params.len(), 0);
assert_eq!(value, 222);
}
#[test]
fn match_method() {
let result = match_request!(&Method::PUT, "/foo", {
"/foo" => {
GET => 111,
POST => 123,
PUT => 456,
DELETE => 789
},
"/bar" => {
GET => 222
},
"/baz" => {
GET => 333
}
});
let (value, params) = result.unwrap();
assert!(params.is_empty());
assert_eq!(params.len(), 0);
assert_eq!(value, 456);
}
#[test]
fn no_matching_path() {
let result = match_request!(&Method::GET, "/asdf", {
"/foo" => {
GET => 111,
},
"/bar" => {
GET => 222
},
"/baz" => {
GET => 333
}
});
assert_eq!(result.unwrap_err(), Error::NotFound);
}
#[test]
fn no_matching_method() {
let result = match_request!(&Method::DELETE, "/foo", {
"/foo" => {
GET => 111,
POST => 123,
PUT => 456,
},
"/bar" => {
GET => 222,
},
"/baz" => {
GET => 333,
},
});
assert_eq!(result.unwrap_err(), Error::MethodNotAllowed);
}
#[test]
fn match_named_params() {
let result = match_request!(&Method::GET, "/user/11/articles/foo-bar", {
"/user/:id/articles/:slug" => {
GET => 123
},
});
let (value, params) = result.unwrap();
assert_eq!(params.get("id").unwrap(), "11");
assert_eq!(params.get("slug").unwrap(), "foo-bar");
assert_eq!(params.len(), 2);
assert!(!params.is_empty());
assert_eq!(value, 123);
}
#[test]
fn tail_capture_includes_leading_forward_slash() {
let result = match_request!(&Method::GET, "/user/11/articles/foo-bar", {
"/user/*" => {
GET => 123
},
});
let (value, params) = result.unwrap();
assert_eq!(params.tail().unwrap(), "/11/articles/foo-bar");
assert_eq!(params.len(), 1);
assert_eq!(value, 123);
}
#[test]
fn capture_entire_path() {
let result = match_request!(&Method::GET, "/user/11/articles/foo-bar", {
"/foo/bar" => {
GET => 111,
},
"*" => {
GET => 123,
},
});
let (value, params) = result.unwrap();
assert_eq!(params.tail().unwrap(), "/user/11/articles/foo-bar");
assert_eq!(params.len(), 1);
assert_eq!(value, 123);
}
#[test]
fn pattern_must_match_full_path() {
let result = match_request!(&Method::GET, "/foo/bar", {
"/bar" => {
GET => 123
},
"/foo" => {
GET => 456
},
});
assert_eq!(result.unwrap_err(), Error::NotFound);
}
#[test]
fn regex_capture_groups() {
let result = match_request_regex!(&Method::GET, "/user/11/articles/foo-bar", {
r"^/user/(\d+)/articles/([a-z-]+)$" => {
GET => 123
},
});
let (value, captures) = result.unwrap();
assert_eq!(captures.get(0).unwrap().as_str(), "/user/11/articles/foo-bar");
assert_eq!(captures.get(1).unwrap().as_str(), "11");
assert_eq!(captures.get(2).unwrap().as_str(), "foo-bar");
assert_eq!(captures.len(), 3);
assert_eq!(value, 123);
}
}