blob: bf3b216446cf99545f6f7ec775bfa27b44b909c2 [file] [log] [blame]
// Copyright (c) 2016 The Rouille developers
// Licensed under the Apache License, Version 2.0
// <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT
// license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
// at your option. All files in the project carrying such
// notice may not be copied, modified, or distributed except
// according to those terms.
/// Equivalent to a `match` expression but for routes.
///
/// # Example
///
/// ```no_run
/// # #[macro_use] extern crate rouille; fn main() {
/// # let request: rouille::Request = unsafe { std::mem::uninitialized() };
/// let _result = router!(request,
/// // first route
/// (GET) (/) => {
/// 12
/// },
///
/// // second route
/// (GET) (/hello) => {
/// 43 * 7
/// },
///
/// // ... other routes here ...
///
/// // default route
/// _ => 5
/// );
/// # }
/// ```
///
/// # Details
///
/// The macro will take each route one by one and execute the first one that matches, similar to the
/// `match` language construct. The whole `router!` expression then returns what the body
/// returns, therefore all the bodies must return the same type of data.
///
/// You can use parameters by putting them inside `{}`:
///
/// ```ignore
/// (GET) (/{id}/foo) => {
/// ...
/// },
/// ```
///
/// If you use parameters inside `{}`, then a variable with the same name will be available in the
/// code in the body.
/// Each parameter gets parsed through the `FromStr` trait. If the parsing fails, the route is
/// ignored. If you get an error because the type of the parameter couldn't be inferred, you can
/// also specify the type inside the brackets:
///
/// ```ignore
/// (GET) (/{id: u32}/foo) => {
/// ...
/// },
/// ```
///
///
/// # Alternative syntax (**string-style**)
///
/// You can also define url routes using strings. This allows using characters that are not valid rust
/// `ident`s (e.g. periods and numbers).
///
/// ```ignore
/// (GET) ["/hello/2"] => { ... },
/// ```
///
/// You can use parameters by putting them inside `{}`, and adding an `identity: type` pair. Note
/// that `identity` names **must** match the parameter names used in the URL string and an
/// accompanying `type` is required. The declared identities (variables) will be parsed through the
/// `FromStr` trait and made available in the code of the route's body. If the parsing fails, the
/// route is ignored.
///
/// ```ignore
/// (GET) ["/add/{a}/plus/{b}", a: u32, b: u32] => {
/// let c = a + b;
/// ...
/// },
/// ```
///
/// # Some other things to note
///
/// - The right of the `=>` must be a block (must be surrounded by `{` and `}`).
/// - The default handler (with `_`) must be present or will get a compilation error.
/// - The pattern of the URL must be inside parentheses for ident-style syntax
/// and brackets for string-style syntax. This is to bypass limitations of Rust's macros system.
/// - String-style and token-style definitions are mutually exclusive. Either all routes are defined with
/// tokens or all routes are defined with strings.
/// - When using URL parameters with **string-style** syntax, the parameter names in the URL and `identity: type`
/// pairs must be the same, e.g. `... ["/users/{name}", name: String] ...` .
/// This can't be checked at compile time so bad route definitions will cause a runtime `panic`.
///
#[macro_export]
macro_rules! router {
// -----------------
// --- New style ---
// -----------------
($request:expr,
$(($method:ident) [$url_pattern:expr $(, $param:ident: $param_type:ty)*] => $handle:expr,)*
_ => $default:expr $(,)*) => {
{
let request = &$request;
// ignoring the GET parameters (everything after `?`)
let request_url = request.raw_url();
let request_url = {
let pos = request_url.find('?').unwrap_or(request_url.len());
&request_url[..pos]
};
let mut ret = None;
$({
if ret.is_none() && request.method() == stringify!($method) {
ret = router!(__param_dispatch request_url, $url_pattern => $handle ; $($param: $param_type),*);
}
})+
if let Some(ret) = ret {
ret
} else {
$default
}
}
};
// No url parameters, just check the url and evaluate the `$handle`
(__param_dispatch $request_url:ident, $url_pattern:expr => $handle:expr ; ) => {
router!(__check_url_match $request_url, $url_pattern => $handle)
};
// Url parameters found, check and parse the url against the provided pattern
(__param_dispatch $request_url:ident, $url_pattern:expr => $handle:expr ; $($param:ident: $param_type:ty),*) => {
router!(__check_parse_pattern $request_url, $url_pattern => $handle ; $($param: $param_type),*)
};
(__check_url_match $request_url:ident, $url_pattern:expr => $handle:expr) => {
if $request_url == $url_pattern {
Some($handle)
} else {
None
}
};
// Compare each url segment while attempting to parse any url parameters.
// If parsing fails, return `None` so this route gets skipped.
// If parsing is successful, recursively bind each url parameter to the given identity
// before evaluating the `$handle`
// Note: Url parameters need to be held in the `RouilleUrlParams` struct since
// we need to be able to "evaluate to None" (if url segments don't match or parsing fails)
// and we can't actually "return None" since we'd be returning from whatever scope the macro is being used in.
(__check_parse_pattern $request_url_str:ident, $url_pattern:expr => $handle:expr ; $($param:ident: $param_type:ty),*) => {
{
let request_url = $request_url_str.split("/")
.map(|s| $crate::url::percent_encoding::percent_decode(s.as_bytes()).decode_utf8_lossy().into_owned())
.collect::<Vec<_>>();
let url_pattern = $url_pattern.split("/").collect::<Vec<_>>();
if request_url.len() != url_pattern.len() {
None
} else {
struct RouilleUrlParams {
$( $param: Option<$param_type> ),*
}
impl RouilleUrlParams {
fn new() -> Self {
Self {
$( $param: None ),*
}
}
}
let url_params = (|| {
let mut url_params = RouilleUrlParams::new();
for (actual, desired) in request_url.iter().zip(url_pattern.iter()) {
if desired.starts_with("{") && desired.ends_with("}") {
let key = &desired[1..desired.len()-1];
router!(__insert_param $request_url_str, url_params, key, actual ; $($param: $param_type)*)
} else if actual != desired {
return None
}
}
Some(url_params)
})();
if let Some(url_params) = url_params {
router!(__build_resp $request_url_str, url_params, $handle ; $($param: $param_type)*)
} else {
None
}
}
}
};
// We walked through all the given url parameter identities and couldn't find one that
// matches the parameter name defined in the url-string
// e.g. `(GET) ("/name/{title}", name: String)
(__insert_param $request_url:ident, $url_params:ident, $key:expr, $actual:expr ; ) => {
panic!("Unable to match url parameter name, `{}`, to an `identity: type` pair in url: {:?}", $key, $request_url);
};
// Walk through all the given url parameter identities. If they match the current
// `$key` (a parameter name in the string-url), then set them in the `$url_params` struct
(__insert_param $request_url:ident, $url_params:ident, $key:expr, $actual:expr ; $param:tt: $param_type:tt $($params:tt: $param_types:tt)*) => {
if $key == stringify!($param) {
router!(__bind_url_param $url_params, $actual, $param, $param_type)
} else {
router!(__insert_param $request_url, $url_params, $key, $actual ; $($params: $param_types)*);
}
};
(__bind_url_param $url_params:ident, $actual:expr, $param:ident, $param_type:ty) => {
{
match $actual.parse::<$param_type>() {
Ok(value) => $url_params.$param = Some(value),
// it's safe to `return` here since we're in a closure
Err(_) => return None,
}
}
};
// No more url parameters to bind
(__build_resp $request_url:ident, $url_params:expr, $handle:expr ; ) => {
{ Some($handle) }
};
// There's still some params to bind
(__build_resp $request_url:ident, $url_params:expr, $handle:expr ; $param:tt: $param_type:tt $($params:tt: $param_types:tt)*) => {
router!(__bind_param $request_url, $url_params, $handle, $param: $param_type ; $($params: $param_types)*)
};
// Recursively pull out and bind a url param
(__bind_param $request_url:ident, $url_params:expr, $handle:expr, $param:ident: $param_type:ty ; $($params:tt: $param_types:tt)*) => {
{
let $param = match $url_params.$param {
Some(p) => p,
None => {
let param_name = stringify!($param);
panic!("Url parameter identity, `{}`, does not have a matching `{{{}}}` segment in url: {:?}",
param_name, param_name, $request_url);
}
};
router!(__build_resp $request_url, $url_params, $handle ; $($params: $param_types)*)
}
};
// -----------------
// --- Old style ---
// -----------------
($request:expr, $(($method:ident) ($($pat:tt)+) => $value:block,)* _ => $def:expr $(,)*) => {
{
let request = &$request;
// ignoring the GET parameters (everything after `?`)
let request_url = request.raw_url();
let request_url = {
let pos = request_url.find('?').unwrap_or(request_url.len());
&request_url[..pos]
};
let mut ret = None;
$({
if ret.is_none() && request.method() == stringify!($method) {
ret = router!(__check_pattern request_url $value $($pat)+);
}
})+
if let Some(ret) = ret {
ret
} else {
$def
}
}
};
(__check_pattern $url:ident $value:block /{$p:ident} $($rest:tt)*) => (
if !$url.starts_with('/') {
None
} else {
let url = &$url[1..];
let pat_end = url.find('/').unwrap_or(url.len());
let rest_url = &url[pat_end..];
if let Ok($p) = url[0 .. pat_end].parse() {
router!(__check_pattern rest_url $value $($rest)*)
} else {
None
}
}
);
(__check_pattern $url:ident $value:block /{$p:ident: $t:ty} $($rest:tt)*) => (
if !$url.starts_with('/') {
None
} else {
let url = &$url[1..];
let pat_end = url.find('/').unwrap_or(url.len());
let rest_url = &url[pat_end..];
if let Ok($p) = $crate::url::percent_encoding::percent_decode(url[0 .. pat_end].as_bytes())
.decode_utf8_lossy().parse() {
let $p: $t = $p;
router!(__check_pattern rest_url $value $($rest)*)
} else {
None
}
}
);
(__check_pattern $url:ident $value:block /$p:ident $($rest:tt)*) => (
{
let required = concat!("/", stringify!($p));
if $url.starts_with(required) {
let rest_url = &$url[required.len()..];
router!(__check_pattern rest_url $value $($rest)*)
} else {
None
}
}
);
(__check_pattern $url:ident $value:block - $($rest:tt)*) => (
{
if $url.starts_with('-') {
let rest_url = &$url[1..];
router!(__check_pattern rest_url $value $($rest)*)
} else {
None
}
}
);
(__check_pattern $url:ident $value:block) => (
if $url.len() == 0 { Some($value) } else { None }
);
(__check_pattern $url:ident $value:block /) => (
if $url == "/" { Some($value) } else { None }
);
(__check_pattern $url:ident $value:block $p:ident $($rest:tt)*) => (
{
let required = stringify!($p);
if $url.starts_with(required) {
let rest_url = &$url[required.len()..];
router!(__check_pattern rest_url $value $($rest)*)
} else {
None
}
}
);
}
#[cfg(test)]
mod tests {
use Request;
// -- old-style tests --
#[test]
fn old_style_basic() {
let request = Request::fake_http("GET", "/", vec![], vec![]);
assert_eq!(1, router!(request,
(GET) (/hello) => { 0 },
(GET) (/{_val:u32}) => { 0 },
(GET) (/) => { 1 },
_ => 0
));
}
#[test]
fn old_style_dash() {
let request = Request::fake_http("GET", "/a-b", vec![], vec![]);
assert_eq!(1, router!(request,
(GET) (/a/b) => { 0 },
(GET) (/a_b) => { 0 },
(GET) (/a-b) => { 1 },
_ => 0
));
}
#[test]
fn old_style_params() {
let request = Request::fake_http("GET", "/hello/5", vec![], vec![]);
assert_eq!(1, router!(request,
(GET) (/hello/) => { 0 },
(GET) (/hello/{id:u32}) => { if id == 5 { 1 } else { 0 } },
(GET) (/hello/{_id:String}) => { 0 },
_ => 0
));
}
#[test]
fn old_style_trailing_comma() {
let request = Request::fake_http("GET", "/hello/5", vec![], vec![]);
assert_eq!(1, router!(request,
(GET) (/hello/) => { 0 },
(GET) (/hello/{id:u32}) => { if id == 5 { 1 } else { 0 } },
(GET) (/hello/{_id:String}) => { 0 },
_ => 0,
));
}
#[test]
fn old_style_trailing_commas() {
let request = Request::fake_http("GET", "/hello/5", vec![], vec![]);
assert_eq!(1, router!(request,
(GET) (/hello/) => { 0 },
(GET) (/hello/{id:u32}) => { if id == 5 { 1 } else { 0 } },
(GET) (/hello/{_id:String}) => { 0 },
_ => 0,,,,
));
}
// -- new-style tests --
#[test]
fn multiple_params() {
let request = Request::fake_http("GET", "/math/3.2/plus/4", vec![], vec![]);
let resp = router!(request,
(GET) ["/hello"] => { 1. },
(GET) ["/math/{a}/plus/{b}", a: u32 , b: u32] => { 7. },
(GET) ["/math/{a}/plus/{b}", a: f32 , b: u32] => { a + (b as f32) },
_ => 0.
);
assert_eq!(7.2, resp);
}
#[test]
fn basic() {
let request = Request::fake_http("GET", "/", vec![], vec![]);
assert_eq!(1, router!(request,
(GET) ["/hello"] => { 0 },
(GET) ["/{_val}", _val: u32] => { 0 },
(GET) ["/"] => { 1 },
_ => 0
));
}
#[test]
fn dash() {
let request = Request::fake_http("GET", "/a-b", vec![], vec![]);
assert_eq!(1, router!(request,
(GET) ["/a/b"] => { 0 },
(GET) ["/a_b"] => { 0 },
(GET) ["/a-b"] => { 1 },
_ => 0
));
}
#[test]
fn numbers() {
let request = Request::fake_http("GET", "/5", vec![], vec![]);
assert_eq!(1, router!(request,
(GET) ["/a"] => { 0 },
(GET) ["/3"] => { 0 },
(GET) ["/5"] => { 1 },
_ => 0
));
}
#[test]
fn trailing_comma() {
let request = Request::fake_http("GET", "/5", vec![], vec![]);
assert_eq!(1, router!(request,
(GET) ["/a"] => { 0 },
(GET) ["/3"] => { 0 },
(GET) ["/5"] => { 1 },
_ => 0,
));
}
#[test]
fn trailing_commas() {
let request = Request::fake_http("GET", "/5", vec![], vec![]);
assert_eq!(1, router!(request,
(GET) ["/a"] => { 0 },
(GET) ["/3"] => { 0 },
(GET) ["/5"] => { 1 },
_ => 0,,,,
));
}
#[test]
fn files() {
let request = Request::fake_http("GET", "/robots.txt", vec![], vec![]);
assert_eq!(1, router!(request,
(GET) ["/a"] => { 0 },
(GET) ["/3/2/1"] => { 0 },
(GET) ["/robots.txt"] => { 1 },
_ => 0
));
}
#[test]
fn skip_failed_parse_float() {
let request = Request::fake_http("GET", "/hello/5.1", vec![], vec![]);
assert_eq!(1, router!(request,
(GET) ["/hello/"] => { 0 },
(GET) ["/hello/{_id}", _id: u32] => { 0 },
(GET) ["/hello/{id}", id: f32] => { if id == 5.1 { 1 } else { 0 } },
_ => 0
));
}
#[test]
fn skip_failed_parse_string() {
let request = Request::fake_http("GET", "/word/wow", vec![], vec![]);
let resp = router!(request,
(GET) ["/hello"] => { "hello".to_string() },
(GET) ["/word/{int}", int: u32] => { int.to_string() },
(GET) ["/word/{word}", word: String] => { word },
_ => "default".to_string()
);
assert_eq!("wow", resp);
}
#[test]
fn url_parameter_ownership() {
let request = Request::fake_http("GET", "/word/one/two/three/four", vec![], vec![]);
let resp = router!(request,
(GET) ["/hello"] => { "hello".to_string() },
(GET) ["/word/{int}", int: u32] => { int.to_string() },
(GET) ["/word/{a}/{b}/{c}/{d}", a: String, b: String, c: String, d: String] => {
fn expects_strings(a: String, b: String, c: String, d: String) -> String {
format!("{}{}{}{}", a, b, c, d)
}
expects_strings(a, b, c, d)
},
_ => "default".to_string()
);
assert_eq!("onetwothreefour", resp);
}
#[test]
#[should_panic(expected="Url parameter identity, `id`, does not have a matching `{id}` segment in url: \"/hello/james\"")]
fn identity_not_present_in_url_string() {
let request = Request::fake_http("GET", "/hello/james", vec![], vec![]);
assert_eq!(1, router!(request,
(GET) ["/hello/"] => { 0 },
(GET) ["/hello/{name}", name: String, id: u32] => { 1 }, // this should fail
_ => 0
));
}
#[test]
#[should_panic(expected="Unable to match url parameter name, `name`, to an `identity: type` pair in url: \"/hello/1/james\"")]
fn parameter_with_no_matching_identity() {
let request = Request::fake_http("GET", "/hello/1/james", vec![], vec![]);
assert_eq!(1, router!(request,
(GET) ["/hello/"] => { 0 },
(GET) ["/hello/{id}/{name}"] => { 0 }, // exact match should be ignored
(GET) ["/hello/{id}/{name}", id: u32] => { id }, // this one should fail
_ => 0
));
}
#[test]
fn encoded() {
let request = Request::fake_http("GET", "/hello/%3Fa/test", vec![], vec![]);
assert_eq!("?a", router!(request,
(GET) ["/hello/{val}/test", val: String] => { val },
_ => String::from("")));
}
#[test]
fn encoded_old() {
let request = Request::fake_http("GET", "/hello/%3Fa/test", vec![], vec![]);
assert_eq!("?a", router!(request,
(GET) (/hello/{val: String}/test) => { val },
_ => String::from("")));
}
#[test]
fn param_slash() {
let request = Request::fake_http("GET", "/hello%2F5", vec![], vec![]);
router!(request,
(GET) ["/{a}", a: String] => { assert_eq!(a, "hello/5") },
_ => panic!()
);
}
}