rouille/
router.rs

1// Copyright (c) 2016 The Rouille developers
2// Licensed under the Apache License, Version 2.0
3// <LICENSE-APACHE or
4// http://www.apache.org/licenses/LICENSE-2.0> or the MIT
5// license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
6// at your option. All files in the project carrying such
7// notice may not be copied, modified, or distributed except
8// according to those terms.
9
10/// Equivalent to a `match` expression but for routes.
11///
12/// # Example
13///
14/// ```no_run
15/// # #[macro_use] extern crate rouille; fn main() {
16/// # let request: rouille::Request = unsafe { std::mem::uninitialized() };
17/// let _result = router!(request,
18///     // first route
19///     (GET) (/) => {
20///         12
21///     },
22///
23///     // second route
24///     (GET) (/hello) => {
25///         43 * 7
26///     },
27///
28///     // ... other routes here ...
29///
30///     // default route
31///     _ => 5
32/// );
33/// # }
34/// ```
35///
36/// # Details
37///
38/// The macro will take each route one by one and execute the first one that matches, similar to the
39/// `match` language construct. The whole `router!` expression then returns what the body
40/// returns, therefore all the bodies must return the same type of data.
41///
42/// You can use parameters by putting them inside `{}`:
43///
44/// ```ignore
45/// (GET) (/{id}/foo) => {
46///     ...
47/// },
48/// ```
49///
50/// If you use parameters inside `{}`, then a variable with the same name will be available in the
51/// code in the body.
52/// Each parameter gets parsed through the `FromStr` trait. If the parsing fails, the route is
53/// ignored. If you get an error because the type of the parameter couldn't be inferred, you can
54/// also specify the type inside the brackets:
55///
56/// ```ignore
57/// (GET) (/{id: u32}/foo) => {
58///     ...
59/// },
60/// ```
61///
62///
63/// # Alternative syntax (**string-style**)
64///
65/// You can also define url routes using strings. This allows using characters that are not valid rust
66/// `ident`s (e.g. periods and numbers).
67///
68/// ```ignore
69/// (GET) ["/hello/2"] => { ... },
70/// ```
71///
72/// You can use parameters by putting them inside `{}`, and adding an `identity: type` pair. Note
73/// that `identity` names **must** match the parameter names used in the URL string and an
74/// accompanying `type` is required. The declared identities (variables) will be parsed through the
75/// `FromStr` trait and made available in the code of the route's body. If the parsing fails, the
76/// route is ignored.
77///
78/// ```ignore
79/// (GET) ["/add/{a}/plus/{b}", a: u32, b: u32] => {
80///     let c = a + b;
81///     ...
82/// },
83/// ```
84///
85/// # Some other things to note
86///
87/// - The right of the `=>` must be a block (must be surrounded by `{` and `}`).
88/// - The default handler (with `_`) must be present or will get a compilation error.
89/// - The pattern of the URL must be inside parentheses for ident-style syntax
90///   and brackets for string-style syntax. This is to bypass limitations of Rust's macros system.
91/// - String-style and token-style definitions are mutually exclusive. Either all routes are defined with
92///   tokens or all routes are defined with strings.
93/// - When using URL parameters with **string-style** syntax, the parameter names in the URL and `identity: type`
94///   pairs must be the same, e.g. `... ["/users/{name}", name: String] ...` .
95///   This can't be checked at compile time so bad route definitions will cause a runtime `panic`.
96///
97#[macro_export]
98macro_rules! router {
99    // -----------------
100    // --- New style ---
101    // -----------------
102    ($request:expr,
103     $(($method:ident) [$url_pattern:expr $(, $param:ident: $param_type:ty)*] => $handle:expr,)*
104     _ => $default:expr $(,)*) => {
105        {
106            let request = &$request;
107
108            // ignoring the GET parameters (everything after `?`)
109            let request_url = request.raw_url();
110            let request_url = {
111                let pos = request_url.find('?').unwrap_or(request_url.len());
112                &request_url[..pos]
113            };
114
115            let mut ret = None;
116            $({
117                if ret.is_none() && request.method() == stringify!($method) {
118                    ret = $crate::router!(__param_dispatch request_url, $url_pattern => $handle ; $($param: $param_type),*);
119                }
120            })+
121
122            if let Some(ret) = ret {
123                ret
124            } else {
125                $default
126            }
127        }
128    };
129
130    // No url parameters, just check the url and evaluate the `$handle`
131    (__param_dispatch $request_url:ident, $url_pattern:expr => $handle:expr ; ) => {
132        $crate::router!(__check_url_match $request_url, $url_pattern => $handle)
133    };
134
135    // Url parameters found, check and parse the url against the provided pattern
136    (__param_dispatch $request_url:ident, $url_pattern:expr => $handle:expr ; $($param:ident: $param_type:ty),*) => {
137        $crate::router!(__check_parse_pattern $request_url, $url_pattern => $handle ; $($param: $param_type),*)
138    };
139
140    (__check_url_match $request_url:ident, $url_pattern:expr => $handle:expr) => {
141        if $request_url == $url_pattern {
142            Some($handle)
143        } else {
144            None
145        }
146    };
147
148    // Compare each url segment while attempting to parse any url parameters.
149    // If parsing fails, return `None` so this route gets skipped.
150    // If parsing is successful, recursively bind each url parameter to the given identity
151    // before evaluating the `$handle`
152    // Note: Url parameters need to be held in the `RouilleUrlParams` struct since
153    //       we need to be able to "evaluate to None" (if url segments don't match or parsing fails)
154    //       and we can't actually "return None" since we'd be returning from whatever scope the macro is being used in.
155    (__check_parse_pattern $request_url_str:ident, $url_pattern:expr => $handle:expr ; $($param:ident: $param_type:ty),*) => {
156        {
157            let request_url = $request_url_str.split("/")
158                .map(|s| $crate::percent_encoding::percent_decode(s.as_bytes()).decode_utf8_lossy().into_owned())
159                .collect::<Vec<_>>();
160            let url_pattern = $url_pattern.split("/").collect::<Vec<_>>();
161            if request_url.len() != url_pattern.len() {
162                None
163            } else {
164                struct RouilleUrlParams {
165                    $( $param: Option<$param_type> ),*
166                }
167                impl RouilleUrlParams {
168                    fn new() -> Self {
169                        Self {
170                            $( $param: None ),*
171                        }
172                    }
173                }
174                let url_params = (|| {
175                    let mut url_params = RouilleUrlParams::new();
176                    for (actual, desired) in request_url.iter().zip(url_pattern.iter()) {
177                        if let Some(key) = desired.strip_prefix("{").and_then(|d| d.strip_suffix("}")) {
178                            $crate::router!(__insert_param $request_url_str, url_params, key, actual ; $($param: $param_type)*)
179                        } else if actual != desired {
180                            return None
181                        }
182                    }
183                    Some(url_params)
184                })();
185                if let Some(url_params) = url_params {
186                    $crate::router!(__build_resp $request_url_str, url_params, $handle ; $($param: $param_type)*)
187                } else {
188                    None
189                }
190            }
191        }
192    };
193
194    // We walked through all the given url parameter identities and couldn't find one that
195    // matches the parameter name defined in the url-string
196    //   e.g. `(GET) ("/name/{title}", name: String)
197    (__insert_param $request_url:ident, $url_params:ident, $key:expr, $actual:expr ; ) => {
198        panic!("Unable to match url parameter name, `{}`, to an `identity: type` pair in url: {:?}", $key, $request_url);
199    };
200
201    // Walk through all the given url parameter identities. If they match the current
202    // `$key` (a parameter name in the string-url), then set them in the `$url_params` struct
203    (__insert_param $request_url:ident, $url_params:ident, $key:expr, $actual:expr ; $param:tt: $param_type:tt $($params:tt: $param_types:tt)*) => {
204        if $key == stringify!($param) {
205            $crate::router!(__bind_url_param $url_params, $actual, $param, $param_type)
206        } else {
207            $crate::router!(__insert_param $request_url, $url_params, $key, $actual ; $($params: $param_types)*);
208        }
209    };
210
211    (__bind_url_param $url_params:ident, $actual:expr, $param:ident, $param_type:ty) => {
212        {
213            match $actual.parse::<$param_type>() {
214                Ok(value) => $url_params.$param = Some(value),
215                // it's safe to `return` here since we're in a closure
216                Err(_) => return None,
217            }
218        }
219    };
220
221    // No more url parameters to bind
222    (__build_resp $request_url:ident, $url_params:expr, $handle:expr ; ) => {
223        { Some($handle) }
224    };
225
226    // There's still some params to bind
227    (__build_resp $request_url:ident, $url_params:expr, $handle:expr ; $param:tt: $param_type:tt $($params:tt: $param_types:tt)*) => {
228        $crate::router!(__bind_param $request_url, $url_params, $handle, $param: $param_type ; $($params: $param_types)*)
229    };
230
231    // Recursively pull out and bind a url param
232    (__bind_param $request_url:ident, $url_params:expr, $handle:expr, $param:ident: $param_type:ty ; $($params:tt: $param_types:tt)*) => {
233        {
234            let $param = match $url_params.$param {
235                Some(p) => p,
236                None => {
237                    let param_name = stringify!($param);
238                    panic!("Url parameter identity, `{}`, does not have a matching `{{{}}}` segment in url: {:?}",
239                           param_name, param_name, $request_url);
240                }
241            };
242            $crate::router!(__build_resp $request_url, $url_params, $handle ; $($params: $param_types)*)
243        }
244    };
245
246
247    // -----------------
248    // --- Old style ---
249    // -----------------
250    ($request:expr, $(($method:ident) ($($pat:tt)+) => $value:block,)* _ => $def:expr $(,)*) => {
251        {
252            let request = &$request;
253
254            // ignoring the GET parameters (everything after `?`)
255            let request_url = request.raw_url();
256            let request_url = {
257                let pos = request_url.find('?').unwrap_or(request_url.len());
258                &request_url[..pos]
259            };
260
261            let mut ret = None;
262
263            $({
264                if ret.is_none() && request.method() == stringify!($method) {
265                    ret = $crate::router!(__check_pattern request_url $value $($pat)+);
266                }
267            })+
268
269            if let Some(ret) = ret {
270                ret
271            } else {
272                $def
273            }
274        }
275    };
276
277    (__check_pattern $url:ident $value:block /{$p:ident} $($rest:tt)*) => (
278        if let Some(url) = $url.strip_prefix('/') {
279            let url = &$url[1..];
280            let pat_end = url.find('/').unwrap_or(url.len());
281            let rest_url = &url[pat_end..];
282
283            if let Ok($p) = url[0 .. pat_end].parse() {
284                $crate::router!(__check_pattern rest_url $value $($rest)*)
285            } else {
286                None
287            }
288        } else {
289            None
290        }
291    );
292
293    (__check_pattern $url:ident $value:block /{$p:ident: $t:ty} $($rest:tt)*) => (
294        if let Some(url) = $url.strip_prefix('/') {
295            let url = &$url[1..];
296            let pat_end = url.find('/').unwrap_or(url.len());
297            let rest_url = &url[pat_end..];
298
299            if let Ok($p) = $crate::percent_encoding::percent_decode(url[0 .. pat_end].as_bytes())
300                .decode_utf8_lossy().parse() {
301                let $p: $t = $p;
302                $crate::router!(__check_pattern rest_url $value $($rest)*)
303            } else {
304                None
305            }
306        } else {
307            None
308        }
309    );
310
311    (__check_pattern $url:ident $value:block /$p:ident $($rest:tt)*) => (
312        {
313            let required = concat!("/", stringify!($p));
314            if let Some(rest_url) = $url.strip_prefix(required) {
315                $crate::router!(__check_pattern rest_url $value $($rest)*)
316            } else {
317                None
318            }
319        }
320    );
321
322    (__check_pattern $url:ident $value:block - $($rest:tt)*) => (
323        {
324            if let Some(rest_url) = $url.strip_prefix('-') {
325                $crate::router!(__check_pattern rest_url $value $($rest)*)
326            } else {
327                None
328            }
329        }
330    );
331
332    (__check_pattern $url:ident $value:block) => (
333        if $url.len() == 0 { Some($value) } else { None }
334    );
335
336    (__check_pattern $url:ident $value:block /) => (
337        if $url == "/" { Some($value) } else { None }
338    );
339
340    (__check_pattern $url:ident $value:block $p:ident $($rest:tt)*) => (
341        {
342            let required = stringify!($p);
343            if let Some(rest_url) = $url.strip_prefix(required) {
344                $crate::router!(__check_pattern rest_url $value $($rest)*)
345            } else {
346                None
347            }
348        }
349    );
350}
351
352#[allow(unused_variables)]
353#[cfg(test)]
354mod tests {
355    use Request;
356
357    // -- old-style tests --
358    #[test]
359    fn old_style_basic() {
360        let request = Request::fake_http("GET", "/", vec![], vec![]);
361
362        assert_eq!(
363            1,
364            router!(request,
365                (GET) (/hello) => { 0 },
366                (GET) (/{_val:u32}) => { 0 },
367                (GET) (/) => { 1 },
368                _ => 0
369            )
370        );
371    }
372
373    #[test]
374    fn old_style_dash() {
375        let request = Request::fake_http("GET", "/a-b", vec![], vec![]);
376
377        assert_eq!(
378            1,
379            router!(request,
380                (GET) (/a/b) => { 0 },
381                (GET) (/a_b) => { 0 },
382                (GET) (/a-b) => { 1 },
383                _ => 0
384            )
385        );
386    }
387
388    #[test]
389    fn old_style_params() {
390        let request = Request::fake_http("GET", "/hello/5", vec![], vec![]);
391
392        assert_eq!(
393            1,
394            router!(request,
395                (GET) (/hello/) => { 0 },
396                (GET) (/hello/{id:u32}) => { if id == 5 { 1 } else { 0 } },
397                (GET) (/hello/{_id:String}) => { 0 },
398                _ => 0
399            )
400        );
401    }
402
403    #[test]
404    fn old_style_trailing_comma() {
405        let request = Request::fake_http("GET", "/hello/5", vec![], vec![]);
406
407        assert_eq!(
408            1,
409            router!(request,
410                (GET) (/hello/) => { 0 },
411                (GET) (/hello/{id:u32}) => { if id == 5 { 1 } else { 0 } },
412                (GET) (/hello/{_id:String}) => { 0 },
413                _ => 0,
414            )
415        );
416    }
417
418    #[test]
419    fn old_style_trailing_commas() {
420        let request = Request::fake_http("GET", "/hello/5", vec![], vec![]);
421
422        assert_eq!(
423            1,
424            router!(request,
425                (GET) (/hello/) => { 0 },
426                (GET) (/hello/{id:u32}) => { if id == 5 { 1 } else { 0 } },
427                (GET) (/hello/{_id:String}) => { 0 },
428                _ => 0,,,,
429            )
430        );
431    }
432
433    // -- new-style tests --
434    #[test]
435    fn multiple_params() {
436        let request = Request::fake_http("GET", "/math/3.2/plus/4", vec![], vec![]);
437        let resp = router!(request,
438            (GET) ["/hello"] => { 1. },
439            (GET) ["/math/{a}/plus/{b}", a: u32 , b: u32] => { 7. },
440            (GET) ["/math/{a}/plus/{b}", a: f32 , b: u32] => { a + (b as f32) },
441            _ => 0.
442        );
443        assert_eq!(7.2, resp);
444    }
445
446    #[test]
447    fn basic() {
448        let request = Request::fake_http("GET", "/", vec![], vec![]);
449
450        assert_eq!(
451            1,
452            router!(request,
453                (GET) ["/hello"] => { 0 },
454                (GET) ["/{_val}", _val: u32] => { 0 },
455                (GET) ["/"] => { 1 },
456                _ => 0
457            )
458        );
459    }
460
461    #[test]
462    fn dash() {
463        let request = Request::fake_http("GET", "/a-b", vec![], vec![]);
464
465        assert_eq!(
466            1,
467            router!(request,
468                (GET) ["/a/b"] => { 0 },
469                (GET) ["/a_b"] => { 0 },
470                (GET) ["/a-b"] => { 1 },
471                _ => 0
472            )
473        );
474    }
475
476    #[test]
477    fn numbers() {
478        let request = Request::fake_http("GET", "/5", vec![], vec![]);
479
480        assert_eq!(
481            1,
482            router!(request,
483                (GET) ["/a"] => { 0 },
484                (GET) ["/3"] => { 0 },
485                (GET) ["/5"] => { 1 },
486                _ => 0
487            )
488        );
489    }
490
491    #[test]
492    fn trailing_comma() {
493        let request = Request::fake_http("GET", "/5", vec![], vec![]);
494
495        assert_eq!(
496            1,
497            router!(request,
498                (GET) ["/a"] => { 0 },
499                (GET) ["/3"] => { 0 },
500                (GET) ["/5"] => { 1 },
501                _ => 0,
502            )
503        );
504    }
505
506    #[test]
507    fn trailing_commas() {
508        let request = Request::fake_http("GET", "/5", vec![], vec![]);
509
510        assert_eq!(
511            1,
512            router!(request,
513                (GET) ["/a"] => { 0 },
514                (GET) ["/3"] => { 0 },
515                (GET) ["/5"] => { 1 },
516                _ => 0,,,,
517            )
518        );
519    }
520
521    #[test]
522    fn files() {
523        let request = Request::fake_http("GET", "/robots.txt", vec![], vec![]);
524
525        assert_eq!(
526            1,
527            router!(request,
528                (GET) ["/a"] => { 0 },
529                (GET) ["/3/2/1"] => { 0 },
530                (GET) ["/robots.txt"] => { 1 },
531                _ => 0
532            )
533        );
534    }
535
536    #[test]
537    fn skip_failed_parse_float() {
538        let request = Request::fake_http("GET", "/hello/5.1", vec![], vec![]);
539
540        assert_eq!(
541            1,
542            router!(request,
543                (GET) ["/hello/"] => { 0 },
544                (GET) ["/hello/{_id}", _id: u32] => { 0 },
545                (GET) ["/hello/{id}", id: f32] => { if id == 5.1 { 1 } else { 0 } },
546                _ => 0
547            )
548        );
549    }
550
551    #[test]
552    fn skip_failed_parse_string() {
553        let request = Request::fake_http("GET", "/word/wow", vec![], vec![]);
554        let resp = router!(request,
555            (GET) ["/hello"] => { "hello".to_string() },
556            (GET) ["/word/{int}", int: u32] => { int.to_string() },
557            (GET) ["/word/{word}", word: String] => { word },
558            _ => "default".to_string()
559        );
560        assert_eq!("wow", resp);
561    }
562
563    #[test]
564    fn url_parameter_ownership() {
565        let request = Request::fake_http("GET", "/word/one/two/three/four", vec![], vec![]);
566        let resp = router!(request,
567            (GET) ["/hello"] => { "hello".to_string() },
568            (GET) ["/word/{int}", int: u32] => { int.to_string() },
569            (GET) ["/word/{a}/{b}/{c}/{d}", a: String, b: String, c: String, d: String] => {
570                fn expects_strings(a: String, b: String, c: String, d: String) -> String {
571                    format!("{}{}{}{}", a, b, c, d)
572                }
573                expects_strings(a, b, c, d)
574            },
575            _ => "default".to_string()
576        );
577        assert_eq!("onetwothreefour", resp);
578    }
579
580    #[test]
581    #[should_panic(
582        expected = "Url parameter identity, `id`, does not have a matching `{id}` segment in url: \"/hello/james\""
583    )]
584    fn identity_not_present_in_url_string() {
585        let request = Request::fake_http("GET", "/hello/james", vec![], vec![]);
586
587        assert_eq!(
588            1,
589            router!(request,
590                (GET) ["/hello/"] => { 0 },
591                (GET) ["/hello/{name}", name: String, id: u32] => { 1 }, // this should fail
592                _ => 0
593            )
594        );
595    }
596
597    #[test]
598    #[should_panic(
599        expected = "Unable to match url parameter name, `name`, to an `identity: type` pair in url: \"/hello/1/james\""
600    )]
601    fn parameter_with_no_matching_identity() {
602        let request = Request::fake_http("GET", "/hello/1/james", vec![], vec![]);
603
604        assert_eq!(
605            1,
606            router!(request,
607                (GET) ["/hello/"] => { 0 },
608                (GET) ["/hello/{id}/{name}"] => { 0 },           // exact match should be ignored
609                (GET) ["/hello/{id}/{name}", id: u32] => { id }, // this one should fail
610                _ => 0
611            )
612        );
613    }
614
615    #[test]
616    fn encoded() {
617        let request = Request::fake_http("GET", "/hello/%3Fa/test", vec![], vec![]);
618
619        assert_eq!(
620            "?a",
621            router!(request,
622           (GET) ["/hello/{val}/test", val: String] => { val },
623           _ => String::from(""))
624        );
625    }
626
627    #[test]
628    fn encoded_old() {
629        let request = Request::fake_http("GET", "/hello/%3Fa/test", vec![], vec![]);
630
631        assert_eq!(
632            "?a",
633            router!(request,
634           (GET) (/hello/{val: String}/test) => { val },
635           _ => String::from(""))
636        );
637    }
638
639    #[test]
640    fn param_slash() {
641        let request = Request::fake_http("GET", "/hello%2F5", vec![], vec![]);
642
643        router!(request,
644            (GET) ["/{a}", a: String] => { assert_eq!(a, "hello/5") },
645            _ => panic!()
646        );
647    }
648}