├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── Cargo.toml ├── README.md ├── examples ├── custom_404.rs ├── simple.rs ├── simple_with_macro.rs ├── struct_handler.rs └── url_for.rs └── src ├── lib.rs ├── macros.rs ├── router.rs └── url_for.rs /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *~ 3 | *# 4 | *.o 5 | *.so 6 | *.swp 7 | *.dylib 8 | *.dSYM 9 | *.dll 10 | *.rlib 11 | *.dummy 12 | *.exe 13 | *-test 14 | /bin/main 15 | /bin/test-internal 16 | /bin/test-external 17 | /doc/ 18 | /target/ 19 | /build/ 20 | /.rust/ 21 | rusti.sh 22 | watch.sh 23 | /examples/* 24 | !/examples/*.rs 25 | /deps 26 | Cargo.lock 27 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | rust: 3 | - nightly 4 | - stable 5 | - beta 6 | sudo: false 7 | env: 8 | global: 9 | secure: MjdqCPMV5iaByoY3m545Iw/S7UHbX5d3ZgU9H3c62Vo2WqJ12EWk4H0Z0HU+Ptu1+FriplfbnKKcfiE9zHam2yBs2XfhysWm/nOCk5eTetSpapYddWkwQaoEgxcan4kUyJiG0v4ZylorZea7d2wa9iKZKFQPChnvCI+Xd/9dlJs= 10 | after_success: 'curl https://raw.githubusercontent.com/iron/build-doc/master/build-doc.sh 11 | | sh ' 12 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | **router** uses the same conventions as **[Iron](https://github.com/iron/iron)**. 4 | 5 | ### Overview 6 | 7 | * Fork middleware-seed to your own account 8 | * Create a feature branch, namespaced by. 9 | * bug/... 10 | * feat/... 11 | * test/... 12 | * doc/... 13 | * refactor/... 14 | * Make commits to your feature branch. Prefix each commit like so: 15 | * (feat) Added a new feature 16 | * (fix) Fixed inconsistent tests [Fixes #0] 17 | * (refactor) ... 18 | * (cleanup) ... 19 | * (test) ... 20 | * (doc) ... 21 | * Make a pull request with your changes directly to master. Include a 22 | description of your changes. 23 | * Wait for one of the reviewers to look at your code and either merge it or 24 | give feedback which you should adapt to. 25 | 26 | #### Thank you for contributing! 27 | 28 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | 3 | name = "router" 4 | authors = ["Jonathan Reem "] 5 | license = "MIT" 6 | version = "0.7.0-pre1" 7 | description = "A router for the Iron framework." 8 | repository = "https://github.com/iron/router" 9 | documentation = "http://ironframework.io/doc/router/index.html" 10 | keywords = ["iron", "web", "http", "routing", "router"] 11 | 12 | [dependencies] 13 | route-recognizer = "0.1" 14 | iron = { git = "https://github.com/iron/iron", branch = "master" } 15 | url = "1.1" 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Project moved to [`iron/iron`](https://github.com/iron/iron) 2 | 3 | This project has been moved to the `iron/iron` monorepo. You can find the latest version of `router` at https://github.com/iron/iron/tree/master/router. 4 | 5 | All pull requests and issues should be created there instead. 6 | -------------------------------------------------------------------------------- /examples/custom_404.rs: -------------------------------------------------------------------------------- 1 | extern crate iron; 2 | extern crate router; 3 | 4 | // To run, $ cargo run --example custom_404 5 | // To use, go to http://localhost:3000/foobar to see the custom 404 6 | // Or, go to http://localhost:3000 for a standard 200 OK 7 | 8 | use iron::{Iron, Request, Response, IronResult, AfterMiddleware, Chain, StatusCode}; 9 | use iron::error::{IronError}; 10 | use router::{Router, NoRoute}; 11 | 12 | struct Custom404; 13 | 14 | impl AfterMiddleware for Custom404 { 15 | fn catch(&self, _: &mut Request, err: IronError) -> IronResult { 16 | println!("Hitting custom 404 middleware"); 17 | 18 | if err.error.is::() { 19 | Ok(Response::with((StatusCode::NOT_FOUND, "Custom 404 response"))) 20 | } else { 21 | Err(err) 22 | } 23 | } 24 | } 25 | 26 | fn main() { 27 | let mut router = Router::new(); 28 | router.get("/", handler, "example"); 29 | 30 | let mut chain = Chain::new(router); 31 | chain.link_after(Custom404); 32 | 33 | Iron::new(chain).http("localhost:3000"); 34 | } 35 | 36 | fn handler(_: &mut Request) -> IronResult { 37 | Ok(Response::with((StatusCode::OK, "Handling response"))) 38 | } 39 | -------------------------------------------------------------------------------- /examples/simple.rs: -------------------------------------------------------------------------------- 1 | extern crate iron; 2 | extern crate router; 3 | 4 | // To run, $ cargo run --example simple 5 | // To use, go to http://localhost:3000/test and see output "test" 6 | // Or, go to http://localhost:3000 to see a default "OK" 7 | 8 | use iron::{Iron, Request, Response, IronResult, StatusCode}; 9 | use router::{Router}; 10 | 11 | fn main() { 12 | let mut router = Router::new(); 13 | router.get("/", handler, "handler"); 14 | router.get("/:query", query_handler, "query_handler"); 15 | 16 | Iron::new(router).http("localhost:3000"); 17 | 18 | fn handler(_: &mut Request) -> IronResult { 19 | Ok(Response::with((StatusCode::OK, "OK"))) 20 | } 21 | 22 | fn query_handler(req: &mut Request) -> IronResult { 23 | let ref query = req.extensions.get::() 24 | .unwrap().find("query").unwrap_or("/"); 25 | Ok(Response::with((StatusCode::OK, *query))) 26 | } 27 | 28 | 29 | } 30 | -------------------------------------------------------------------------------- /examples/simple_with_macro.rs: -------------------------------------------------------------------------------- 1 | extern crate iron; 2 | #[macro_use] 3 | extern crate router; 4 | 5 | // To run, $ cargo run --example simple_with_macro 6 | // To use, go to http://localhost:3000/test and see output "test" 7 | // Or, go to http://localhost:3000 to see a default "OK" 8 | 9 | use iron::{Iron, Request, Response, IronResult, StatusCode}; 10 | use router::{Router}; 11 | 12 | fn main() { 13 | let router = router!(root: get "/" => handler, query: get "/:query" => query_handler); 14 | 15 | Iron::new(router).http("localhost:3000"); 16 | 17 | fn handler(_: &mut Request) -> IronResult { 18 | Ok(Response::with((StatusCode::OK, "OK"))) 19 | } 20 | 21 | fn query_handler(req: &mut Request) -> IronResult { 22 | let ref query = req.extensions.get::() 23 | .unwrap().find("query").unwrap_or("/"); 24 | Ok(Response::with((StatusCode::OK, *query))) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/struct_handler.rs: -------------------------------------------------------------------------------- 1 | extern crate iron; 2 | extern crate router; 3 | 4 | use iron::{Handler, StatusCode, IronResult, Response, Request, Iron}; 5 | use router::Router; 6 | 7 | struct MessageHandler { 8 | message: String 9 | } 10 | 11 | impl Handler for MessageHandler { 12 | fn handle(&self, _: &mut Request) -> IronResult { 13 | Ok(Response::with((StatusCode::OK, self.message.clone()))) 14 | } 15 | } 16 | 17 | fn main() { 18 | let handler = MessageHandler { 19 | message: "You've found the index page!".to_string() 20 | }; 21 | 22 | let mut router = Router::new(); 23 | router.get("/", handler, "index"); 24 | 25 | Iron::new(router).http("localhost:3000"); 26 | } 27 | -------------------------------------------------------------------------------- /examples/url_for.rs: -------------------------------------------------------------------------------- 1 | extern crate iron; 2 | #[macro_use] extern crate router; 3 | 4 | // To run, $ cargo run --example url_for 5 | // Go to http://localhost:3000 to see "Please go to: /test?extraparam=foo", dynamically generated 6 | // from the original route. 7 | // Go to http://localhost:3000/test to see "test". 8 | // Go to http://localhost:3000/foo to see "foo". 9 | 10 | use iron::prelude::*; 11 | use iron::StatusCode; 12 | use router::Router; 13 | 14 | fn main() { 15 | let router = router!{ 16 | id_1: get "/" => handler, 17 | id_2: get "/:query" => query_handler 18 | }; 19 | 20 | Iron::new(router).http("localhost:3000"); 21 | 22 | fn handler(r: &mut Request) -> IronResult { 23 | Ok(Response::with(( 24 | StatusCode::OK, 25 | format!("Please go to: {}", 26 | url_for!(r, "id_2", 27 | "query" => "test", 28 | "extraparam" => "foo")) 29 | ))) 30 | } 31 | 32 | fn query_handler(req: &mut Request) -> IronResult { 33 | let ref query = req.extensions.get::() 34 | .unwrap().find("query").unwrap_or("/"); 35 | Ok(Response::with((StatusCode::OK, *query))) 36 | } 37 | 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny(missing_docs)] 2 | #![cfg_attr(test, deny(warnings))] 3 | 4 | //! `Router` provides fast and flexible routing for Iron. 5 | 6 | extern crate iron; 7 | extern crate route_recognizer as recognizer; 8 | extern crate url; 9 | 10 | pub use router::{Router, NoRoute, TrailingSlash}; 11 | pub use recognizer::Params; 12 | pub use url_for::url_for; 13 | 14 | mod router; 15 | mod macros; 16 | mod url_for; 17 | -------------------------------------------------------------------------------- /src/macros.rs: -------------------------------------------------------------------------------- 1 | /// Create and populate a router. 2 | /// 3 | /// ```ignore 4 | /// let router = router!(index: get "/" => index, 5 | /// query: get "/:query" => queryHandler, 6 | /// post: post "/" => postHandler); 7 | /// ``` 8 | /// 9 | /// Is equivalent to: 10 | /// 11 | /// ```ignore 12 | /// let mut router = Router::new(); 13 | /// router.get("/", index, "index"); 14 | /// router.get("/:query", queryHandler, "query"); 15 | /// router.post("/", postHandler, "post"); 16 | /// ``` 17 | /// 18 | /// The method name must be lowercase, supported methods: 19 | /// 20 | /// `get`, `post`, `put`, `delete`, `head`, `patch`, `options` and `any`. 21 | #[macro_export] 22 | macro_rules! router { 23 | ($($route_id:ident: $method:ident $glob:expr => $handler:expr),+ $(,)*) => ({ 24 | let mut router = $crate::Router::new(); 25 | $(router.$method($glob, $handler, stringify!($route_id));)* 26 | router 27 | }); 28 | } 29 | 30 | /// Generate a URL based off of the requested one. 31 | /// 32 | /// ```ignore 33 | /// url_for!(request, "foo", 34 | /// "query" => "test", 35 | /// "extraparam" => "foo") 36 | /// ``` 37 | /// 38 | /// Is equivalent to: 39 | /// 40 | /// ```ignore 41 | /// router::url_for(request, "foo", { 42 | /// let mut rv = ::std::collections::HashMap::new(); 43 | /// rv.insert("query".to_owned(), "test".to_owned()); 44 | /// rv.insert("extraparam".to_owned(), "foo".to_owned()); 45 | /// rv 46 | /// }) 47 | /// ``` 48 | #[macro_export] 49 | macro_rules! url_for { 50 | ($request:expr, $route_id:expr $(,$key:expr => $value:expr)* $(,)*) => ( 51 | $crate::url_for($request, $route_id, { 52 | // Underscore-prefix suppresses `unused_mut` warning 53 | // Also works on stable rust! 54 | let mut _params = ::std::collections::HashMap::::new(); 55 | $(_params.insert($key.into(), $value.into());)* 56 | _params 57 | }) 58 | ) 59 | } 60 | 61 | #[cfg(test)] 62 | mod tests { 63 | use iron::{Response, Request, IronResult}; 64 | 65 | //simple test to check that all methods expand without error 66 | #[test] 67 | fn methods() { 68 | fn handler(_: &mut Request) -> IronResult {Ok(Response::new())} 69 | let _ = router!(a: get "/foo" => handler, 70 | b: post "/bar/" => handler, 71 | c: put "/bar/baz" => handler, 72 | d: delete "/bar/baz" => handler, 73 | e: head "/foo" => handler, 74 | f: patch "/bar/baz" => handler, 75 | g: options "/foo" => handler, 76 | h: any "/" => handler); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/router.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::error::Error; 3 | use std::fmt; 4 | use std::sync::Arc; 5 | 6 | use iron::{Request, Response, Handler, IronResult, IronError}; 7 | use iron::{StatusCode, method, Method, headers}; 8 | use iron::typemap::Key; 9 | use iron::modifiers::Redirect; 10 | 11 | use recognizer::Router as Recognizer; 12 | use recognizer::{Match, Params}; 13 | 14 | 15 | pub struct RouterInner { 16 | // The routers, specialized by method. 17 | pub routers: HashMap>>, 18 | // Routes that accept any method. 19 | pub wildcard: Recognizer>, 20 | // Used in URL generation. 21 | pub route_ids: HashMap 22 | } 23 | 24 | /// `Router` provides an interface for creating complex routes as middleware 25 | /// for the Iron framework. 26 | pub struct Router { 27 | inner: Arc 28 | } 29 | 30 | impl Router { 31 | /// Construct a new, empty `Router`. 32 | /// 33 | /// ``` 34 | /// # use router::Router; 35 | /// let router = Router::new(); 36 | /// ``` 37 | pub fn new() -> Router { 38 | Router { 39 | inner: Arc::new(RouterInner { 40 | routers: HashMap::new(), 41 | wildcard: Recognizer::new(), 42 | route_ids: HashMap::new() 43 | }) 44 | } 45 | } 46 | 47 | fn mut_inner(&mut self) -> &mut RouterInner { 48 | Arc::get_mut(&mut self.inner).expect("Cannot modify router at this point.") 49 | } 50 | 51 | /// Add a new route to a `Router`, matching both a method and glob pattern. 52 | /// 53 | /// `route` supports glob patterns: `*` for a single wildcard segment and 54 | /// `:param` for matching storing that segment of the request url in the `Params` 55 | /// object, which is stored in the request `extensions`. 56 | /// 57 | /// For instance, to route `Get` requests on any route matching 58 | /// `/users/:userid/:friend` and store `userid` and `friend` in 59 | /// the exposed Params object: 60 | /// 61 | /// ```ignore 62 | /// let mut router = Router::new(); 63 | /// router.route(method::Get, "/users/:userid/:friendid", controller, "user_friend"); 64 | /// ``` 65 | /// 66 | /// `route_id` is a unique name for your route, and is used when generating an URL with 67 | /// `url_for`. 68 | /// 69 | /// The controller provided to route can be any `Handler`, which allows 70 | /// extreme flexibility when handling routes. For instance, you could provide 71 | /// a `Chain`, a `Handler`, which contains an authorization middleware and 72 | /// a controller function, so that you can confirm that the request is 73 | /// authorized for this route before handling it. 74 | pub fn route, H: Handler, I: AsRef>(&mut self, method: method::Method, glob: S, handler: H, route_id: I) -> &mut Router { 75 | self.mut_inner().routers 76 | .entry(method) 77 | .or_insert(Recognizer::new()) 78 | .add(glob.as_ref(), Box::new(handler)); 79 | self.route_id(route_id.as_ref(), glob.as_ref()); 80 | self 81 | } 82 | 83 | fn route_id(&mut self, id: &str, glob: &str) { 84 | let inner = self.mut_inner(); 85 | let ref mut route_ids = inner.route_ids; 86 | 87 | match route_ids.get(id) { 88 | Some(other_glob) if glob != other_glob => panic!("Duplicate route_id: {}", id), 89 | _ => () 90 | }; 91 | 92 | route_ids.insert(id.to_owned(), glob.to_owned()); 93 | } 94 | 95 | /// Like route, but specialized to the `Get` method. 96 | pub fn get, H: Handler, I: AsRef>(&mut self, glob: S, handler: H, route_id: I) -> &mut Router { 97 | self.route(Method::GET, glob, handler, route_id) 98 | } 99 | 100 | /// Like route, but specialized to the `Post` method. 101 | pub fn post, H: Handler, I: AsRef>(&mut self, glob: S, handler: H, route_id: I) -> &mut Router { 102 | self.route(Method::POST, glob, handler, route_id) 103 | } 104 | 105 | /// Like route, but specialized to the `Put` method. 106 | pub fn put, H: Handler, I: AsRef>(&mut self, glob: S, handler: H, route_id: I) -> &mut Router { 107 | self.route(Method::PUT, glob, handler, route_id) 108 | } 109 | 110 | /// Like route, but specialized to the `Delete` method. 111 | pub fn delete, H: Handler, I: AsRef>(&mut self, glob: S, handler: H, route_id: I) -> &mut Router { 112 | self.route(Method::DELETE, glob, handler, route_id) 113 | } 114 | 115 | /// Like route, but specialized to the `Head` method. 116 | pub fn head, H: Handler, I: AsRef>(&mut self, glob: S, handler: H, route_id: I) -> &mut Router { 117 | self.route(Method::HEAD, glob, handler, route_id) 118 | } 119 | 120 | /// Like route, but specialized to the `Patch` method. 121 | pub fn patch, H: Handler, I: AsRef>(&mut self, glob: S, handler: H, route_id: I) -> &mut Router { 122 | self.route(Method::PATCH, glob, handler, route_id) 123 | } 124 | 125 | /// Like route, but specialized to the `Options` method. 126 | pub fn options, H: Handler, I: AsRef>(&mut self, glob: S, handler: H, route_id: I) -> &mut Router { 127 | self.route(Method::OPTIONS, glob, handler, route_id) 128 | } 129 | 130 | /// Route will match any method, including gibberish. 131 | /// In case of ambiguity, handlers specific to methods will be preferred. 132 | pub fn any, H: Handler, I: AsRef>(&mut self, glob: S, handler: H, route_id: I) -> &mut Router { 133 | self.mut_inner().wildcard.add(glob.as_ref(), Box::new(handler)); 134 | self.route_id(route_id.as_ref(), glob.as_ref()); 135 | self 136 | } 137 | 138 | fn recognize(&self, method: &method::Method, path: &str) 139 | -> Option>> { 140 | self.inner.routers.get(method).and_then(|router| router.recognize(path).ok()) 141 | .or(self.inner.wildcard.recognize(path).ok()) 142 | } 143 | 144 | fn handle_options(&self, path: &str) -> Response { 145 | static METHODS: &'static [method::Method] = 146 | &[Method::GET, Method::POST, Method::PUT, 147 | Method::DELETE, Method::HEAD, Method::PATCH]; 148 | 149 | // Get all the available methods and return them. 150 | let mut options = vec![]; 151 | 152 | for method in METHODS.iter() { 153 | self.inner.routers.get(method).map(|router| { 154 | if let Some(_) = router.recognize(path).ok() { 155 | options.push(method.clone()); 156 | } 157 | }); 158 | } 159 | // If GET is there, HEAD is also there. 160 | if options.contains(&Method::GET) && !options.contains(&Method::HEAD) { 161 | options.push(Method::HEAD); 162 | } 163 | 164 | let mut res = Response::with(StatusCode::OK); 165 | for option in options { 166 | res.headers.append(headers::ALLOW, option.as_str().parse().unwrap()); 167 | } 168 | res 169 | } 170 | 171 | // Tests for a match by adding or removing a trailing slash. 172 | fn redirect_slash(&self, req : &Request) -> Option { 173 | let mut url = req.url.clone(); 174 | let mut path = url.path().join("/"); 175 | 176 | if let Some(last_char) = path.chars().last() { 177 | { 178 | let mut path_segments = url.as_mut().path_segments_mut().unwrap(); 179 | if last_char == '/' { 180 | // We didn't recognize anything without a trailing slash; try again with one appended. 181 | path.pop(); 182 | path_segments.pop(); 183 | } else { 184 | // We didn't recognize anything with a trailing slash; try again without it. 185 | path.push('/'); 186 | path_segments.push(""); 187 | } 188 | } 189 | } 190 | 191 | self.recognize(&req.method, &path).and( 192 | Some(IronError::new(TrailingSlash, 193 | (StatusCode::MOVED_PERMANENTLY, Redirect(url)))) 194 | ) 195 | } 196 | 197 | fn handle_method(&self, req: &mut Request, path: &str) -> Option> { 198 | if let Some(matched) = self.recognize(&req.method, path) { 199 | req.extensions.insert::(matched.params); 200 | req.extensions.insert::(self.inner.clone()); 201 | Some(matched.handler.handle(req)) 202 | } else { self.redirect_slash(req).and_then(|redirect| Some(Err(redirect))) } 203 | } 204 | } 205 | 206 | impl Key for Router { type Value = Params; } 207 | 208 | impl Key for RouterInner { type Value = Arc; } 209 | 210 | impl Handler for Router { 211 | fn handle(&self, req: &mut Request) -> IronResult { 212 | let path = req.url.path().join("/"); 213 | 214 | self.handle_method(req, &path).unwrap_or_else(|| 215 | match req.method { 216 | Method::OPTIONS => Ok(self.handle_options(&path)), 217 | // For HEAD, fall back to GET. Hyper ensures no response body is written. 218 | Method::HEAD => { 219 | req.method = Method::GET; 220 | self.handle_method(req, &path).unwrap_or( 221 | Err(IronError::new(NoRoute, StatusCode::NOT_FOUND)) 222 | ) 223 | } 224 | _ => Err(IronError::new(NoRoute, StatusCode::NOT_FOUND)) 225 | } 226 | ) 227 | } 228 | } 229 | 230 | /// The error thrown by router if there is no matching route, 231 | /// it is always accompanied by a NotFound response. 232 | #[derive(Debug, PartialEq, Eq)] 233 | pub struct NoRoute; 234 | 235 | impl fmt::Display for NoRoute { 236 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 237 | f.write_str("No matching route found.") 238 | } 239 | } 240 | 241 | impl Error for NoRoute { 242 | fn description(&self) -> &str { "No Route" } 243 | } 244 | 245 | /// The error thrown by router if a request was redirected 246 | /// by adding or removing a trailing slash. 247 | #[derive(Debug, PartialEq, Eq)] 248 | pub struct TrailingSlash; 249 | 250 | impl fmt::Display for TrailingSlash { 251 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 252 | f.write_str("The request had a trailing slash.") 253 | } 254 | } 255 | 256 | impl Error for TrailingSlash { 257 | fn description(&self) -> &str { "Trailing Slash" } 258 | } 259 | 260 | #[cfg(test)] 261 | mod test { 262 | use super::Router; 263 | use iron::{headers, method, Method, StatusCode, Request, Response}; 264 | 265 | #[test] 266 | fn test_handle_options_post() { 267 | let mut router = Router::new(); 268 | router.post("/", |_: &mut Request| { 269 | Ok(Response::with((StatusCode::OK, ""))) 270 | }, ""); 271 | let resp = router.handle_options("/"); 272 | let headers : Vec = resp.headers.get_all(headers::ALLOW).into_iter().map(|s| s.to_str().unwrap().parse().unwrap()).collect(); 273 | let expected = vec![Method::POST]; 274 | assert_eq!(expected, headers); 275 | } 276 | 277 | #[test] 278 | fn test_handle_options_get_head() { 279 | let mut router = Router::new(); 280 | router.get("/", |_: &mut Request| { 281 | Ok(Response::with((StatusCode::OK, ""))) 282 | }, ""); 283 | let resp = router.handle_options("/"); 284 | let headers : Vec = resp.headers.get_all(headers::ALLOW).into_iter().map(|s| s.to_str().unwrap().parse().unwrap()).collect(); 285 | let expected = vec![method::Method::GET, method::Method::HEAD]; 286 | assert_eq!(expected, headers); 287 | } 288 | 289 | #[test] 290 | fn test_handle_any_ok() { 291 | let mut router = Router::new(); 292 | router.post("/post", |_: &mut Request| { 293 | Ok(Response::with((StatusCode::OK, ""))) 294 | }, ""); 295 | router.any("/post", |_: &mut Request| { 296 | Ok(Response::with((StatusCode::OK, ""))) 297 | }, ""); 298 | router.put("/post", |_: &mut Request| { 299 | Ok(Response::with((StatusCode::OK, ""))) 300 | }, ""); 301 | router.any("/get", |_: &mut Request| { 302 | Ok(Response::with((StatusCode::OK, ""))) 303 | }, "any"); 304 | 305 | assert!(router.recognize(&Method::GET, "/post").is_some()); 306 | assert!(router.recognize(&Method::GET, "/get").is_some()); 307 | } 308 | 309 | #[test] 310 | fn test_request() { 311 | let mut router = Router::new(); 312 | router.post("/post", |_: &mut Request| { 313 | Ok(Response::with((StatusCode::OK, ""))) 314 | }, ""); 315 | router.get("/post", |_: &mut Request| { 316 | Ok(Response::with((StatusCode::OK, ""))) 317 | }, ""); 318 | 319 | assert!(router.recognize(&Method::POST, "/post").is_some()); 320 | assert!(router.recognize(&Method::GET, "/post").is_some()); 321 | assert!(router.recognize(&Method::PUT, "/post").is_none()); 322 | assert!(router.recognize(&Method::GET, "/post/").is_none()); 323 | } 324 | 325 | #[test] 326 | fn test_not_found() { 327 | let mut router = Router::new(); 328 | router.put("/put", |_: &mut Request| { 329 | Ok(Response::with((StatusCode::OK, ""))) 330 | }, ""); 331 | assert!(router.recognize(&Method::PATCH, "/patch").is_none()); 332 | } 333 | 334 | #[test] 335 | #[should_panic] 336 | fn test_same_route_id() { 337 | let mut router = Router::new(); 338 | router.put("/put", |_: &mut Request| { 339 | Ok(Response::with((StatusCode::OK, ""))) 340 | }, "my_route_id"); 341 | router.get("/get", |_: &mut Request| { 342 | Ok(Response::with((StatusCode::OK, ""))) 343 | }, "my_route_id"); 344 | } 345 | 346 | #[test] 347 | fn test_wildcard_regression() { 348 | let mut router = Router::new(); 349 | router.options("*", |_: &mut Request| Ok(Response::with((StatusCode::OK, ""))), "id1"); 350 | router.put("/upload/*filename", |_: &mut Request| Ok(Response::with((StatusCode::OK, ""))), "id2"); 351 | assert!(router.recognize(&Method::OPTIONS, "/foo").is_some()); 352 | assert!(router.recognize(&Method::PUT, "/foo").is_none()); 353 | assert!(router.recognize(&Method::PUT, "/upload/foo").is_some()); 354 | } 355 | } 356 | -------------------------------------------------------------------------------- /src/url_for.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use url::Url; 4 | 5 | use iron::prelude::*; 6 | use router::RouterInner; 7 | 8 | /// Generate a URL based off of the currently requested URL. 9 | /// 10 | /// The `route_id` used during route registration will be used here again. 11 | /// 12 | /// `params` will be inserted as route parameters if fitting, the rest will be appended as query 13 | /// parameters. 14 | pub fn url_for(request: &Request, route_id: &str, params: HashMap) -> ::iron::Url { 15 | let inner = request.extensions.get::().expect("Couldn\'t find router set up properly."); 16 | let glob = inner.route_ids.get(route_id).expect("No route with that ID"); 17 | 18 | let mut url = request.url.clone(); 19 | url_for_impl(url.as_mut(), glob, params); 20 | url 21 | } 22 | 23 | fn url_for_impl(url: &mut Url, glob: &str, mut params: HashMap) { 24 | { 25 | let mut url_path_segments = url.path_segments_mut().unwrap(); 26 | url_path_segments.clear(); 27 | for path_segment in glob.split('/') { 28 | if path_segment.len() > 1 && (path_segment.starts_with(':') || path_segment.starts_with('*')) { 29 | let key = &path_segment[1..]; 30 | match params.remove(key) { 31 | Some(x) => url_path_segments.push(&x), 32 | None => panic!("No value for key {}", key) 33 | }; 34 | } else { 35 | url_path_segments.push(path_segment); 36 | } 37 | } 38 | } 39 | 40 | // Now add on the remaining parameters that had no path match. 41 | url.set_query(None); 42 | if !params.is_empty() { 43 | url.query_pairs_mut() 44 | .extend_pairs(params.into_iter()); 45 | } 46 | 47 | url.set_fragment(None); 48 | } 49 | 50 | #[cfg(test)] 51 | mod test { 52 | use super::url_for_impl; 53 | use std::collections::HashMap; 54 | 55 | #[test] 56 | fn test_no_trailing_slash() { 57 | let mut url = "http://localhost/foo/bar/baz".parse().unwrap(); 58 | url_for_impl(&mut url, "/foo/:user", { 59 | let mut rv = HashMap::new(); 60 | rv.insert("user".into(), "bam".into()); 61 | rv 62 | }); 63 | assert_eq!(url.to_string(), "http://localhost/foo/bam"); 64 | } 65 | 66 | #[test] 67 | fn test_trailing_slash() { 68 | let mut url = "http://localhost/foo/bar/baz".parse().unwrap(); 69 | url_for_impl(&mut url, "/foo/:user/", { 70 | let mut rv = HashMap::new(); 71 | rv.insert("user".into(), "bam".into()); 72 | rv 73 | }); 74 | assert_eq!(url.to_string(), "http://localhost/foo/bam/"); 75 | } 76 | } 77 | --------------------------------------------------------------------------------