diff --git a/README.md b/README.md index 798989af5e..070348cce7 100644 --- a/README.md +++ b/README.md @@ -55,16 +55,17 @@ Here is how sample server in `example.rs` looks like: ```rust extern crate rustc_serialize; extern crate nickel; +extern crate regex; #[macro_use] extern crate nickel_macros; +use std::collections::BTreeMap; +use std::io::Write; use nickel::status::StatusCode::{self, NotFound, BadRequest}; use nickel::{ Nickel, NickelError, Continue, Halt, Request, QueryString, JsonBody, StaticFilesHandler, HttpRouter, Action }; - -use std::collections::BTreeMap; -use std::io::Write; +use regex::Regex; use rustc_serialize::json::{Json, ToJson}; #[derive(RustcDecodable, RustcEncodable)] @@ -104,6 +105,13 @@ fn main() { // go to http://localhost:6767/bar to see this route in action router.get("/bar", middleware!("This is the /bar handler")); + let hello_regex = Regex::new("/hello/(?P[a-zA-Z]+)").unwrap(); + + // go to http://localhost:6767/hello/moomah to see this route in action + router.get(hello_regex, middleware! { |request| + format!("Hello {}", request.param("name")) + }); + // go to http://localhost:6767/some/crazy/route to see this route in action router.get("/some/*/route", middleware! { "This matches /some/crazy/route but not /some/super/crazy/route" diff --git a/examples/example.rs b/examples/example.rs index 932ecd6166..2bb69a195a 100644 --- a/examples/example.rs +++ b/examples/example.rs @@ -1,15 +1,16 @@ extern crate rustc_serialize; extern crate nickel; +extern crate regex; #[macro_use] extern crate nickel_macros; +use std::collections::BTreeMap; +use std::io::Write; use nickel::status::StatusCode::{self, NotFound, BadRequest}; use nickel::{ Nickel, NickelError, Continue, Halt, Request, QueryString, JsonBody, StaticFilesHandler, HttpRouter, Action }; - -use std::collections::BTreeMap; -use std::io::Write; +use regex::Regex; use rustc_serialize::json::{Json, ToJson}; #[derive(RustcDecodable, RustcEncodable)] @@ -49,6 +50,13 @@ fn main() { // go to http://localhost:6767/bar to see this route in action router.get("/bar", middleware!("This is the /bar handler")); + let hello_regex = Regex::new("/hello/(?P[a-zA-Z]+)").unwrap(); + + // go to http://localhost:6767/hello/moomah to see this route in action + router.get(hello_regex, middleware! { |request| + format!("Hello {}", request.param("name")) + }); + // go to http://localhost:6767/some/crazy/route to see this route in action router.get("/some/*/route", middleware! { "This matches /some/crazy/route but not /some/super/crazy/route" diff --git a/examples/macro_example.rs b/examples/macro_example.rs index 4094074765..deb031fb55 100644 --- a/examples/macro_example.rs +++ b/examples/macro_example.rs @@ -1,14 +1,16 @@ extern crate url; extern crate nickel; +extern crate regex; extern crate rustc_serialize; #[macro_use] extern crate nickel_macros; +use std::io::Write; use nickel::status::StatusCode::{self, NotFound}; use nickel::{ Nickel, NickelError, Continue, Halt, Request, Response, QueryString, JsonBody, StaticFilesHandler, MiddlewareResult, HttpRouter, Action }; -use std::io::Write; +use regex::Regex; #[derive(RustcDecodable, RustcEncodable)] struct Person { @@ -43,6 +45,8 @@ fn main() { // go to http://localhost:6767/thoughtram_logo_brain.png to see static file serving in action server.utilize(StaticFilesHandler::new("examples/assets/")); + let hello_regex = Regex::new("/hello/(?P[a-zA-Z]+)").unwrap(); + // The return type for a route can be anything that implements `ResponseFinalizer` server.utilize(router!( // go to http://localhost:6767/user/4711 to see this route in action @@ -63,6 +67,11 @@ fn main() { (200u16, "This is the /bar handler") } + // go to http://localhost:6767/hello/moomah to see this route in action + get hello_regex => |request, response| { + format!("Hello {}", request.param("name")) + } + // FIXME // // go to http://localhost:6767/redirect to see this route in action // get "/redirect" => |request, response| { diff --git a/src/nickel.rs b/src/nickel.rs index 136e65966e..3de1d529e2 100644 --- a/src/nickel.rs +++ b/src/nickel.rs @@ -1,6 +1,6 @@ use std::fmt::Display; use std::net::ToSocketAddrs; -use router::{Router, HttpRouter}; +use router::{Router, HttpRouter, IntoMatcher}; use middleware::{MiddlewareStack, Middleware, ErrorHandler}; use server::Server; use hyper::method::Method; @@ -21,10 +21,9 @@ pub struct Nickel{ } impl HttpRouter for Nickel { - fn add_route(&mut self, method: Method, uri: &str, handler: H) { + fn add_route(&mut self, method: Method, matcher: M, handler: H) { let mut router = Router::new(); - // FIXME: Inference failure in nightly 22/10/2014 - router.add_route::(method, uri, handler); + router.add_route(method, matcher, handler); self.utilize(router); } } diff --git a/src/router/http_router.rs b/src/router/http_router.rs index 0ed8648b7c..a4534c1198 100644 --- a/src/router/http_router.rs +++ b/src/router/http_router.rs @@ -1,5 +1,6 @@ use hyper::method::Method; use middleware::Middleware; +use router::IntoMatcher; pub trait HttpRouter { /// Registers a handler to be used for a specified method. @@ -10,10 +11,12 @@ pub trait HttpRouter { /// ```{rust} /// extern crate hyper; /// extern crate nickel; + /// extern crate regex; /// #[macro_use] extern crate nickel_macros; /// /// use nickel::{Nickel, HttpRouter}; /// use hyper::method::Method::{Get, Post, Put, Delete}; + /// use regex::Regex; /// /// fn main() { /// let read_handler = middleware! { "Get request! "}; @@ -27,9 +30,13 @@ pub trait HttpRouter { /// server.add_route(Post, "/foo", modify_handler); /// server.add_route(Put, "/foo", modify_handler); /// server.add_route(Delete, "/foo", modify_handler); + /// + /// // Regex path + /// let regex = Regex::new("/(foo|bar)").unwrap(); + /// server.add_route(Get, regex, read_handler); /// } /// ``` - fn add_route(&mut self, Method, &str, H); + fn add_route(&mut self, Method, M, H); /// Registers a handler to be used for a specific GET request. /// Handlers are assigned to paths and paths are allowed to contain @@ -102,8 +109,8 @@ pub trait HttpRouter { /// server.utilize(router); /// } /// ``` - fn get(&mut self, uri: &str, handler: H) { - self.add_route(Method::Get, uri, handler); + fn get(&mut self, matcher: M, handler: H) { + self.add_route(Method::Get, matcher, handler); } /// Registers a handler to be used for a specific POST request. @@ -125,8 +132,8 @@ pub trait HttpRouter { /// }); /// # } /// ``` - fn post(&mut self, uri: &str, handler: H) { - self.add_route(Method::Post, uri, handler); + fn post(&mut self, matcher: M, handler: H) { + self.add_route(Method::Post, matcher, handler); } /// Registers a handler to be used for a specific PUT request. @@ -148,8 +155,8 @@ pub trait HttpRouter { /// }); /// # } /// ``` - fn put(&mut self, uri: &str, handler: H) { - self.add_route(Method::Put, uri, handler); + fn put(&mut self, matcher: M, handler: H) { + self.add_route(Method::Put, matcher, handler); } /// Registers a handler to be used for a specific DELETE request. @@ -170,7 +177,7 @@ pub trait HttpRouter { /// }); /// # } /// ``` - fn delete(&mut self, uri: &str, handler: H) { - self.add_route(Method::Delete, uri, handler); + fn delete(&mut self, matcher: M, handler: H) { + self.add_route(Method::Delete, matcher, handler); } } diff --git a/src/router/into_matcher.rs b/src/router/into_matcher.rs new file mode 100644 index 0000000000..6a1bfc5c3b --- /dev/null +++ b/src/router/into_matcher.rs @@ -0,0 +1,61 @@ +use super::Matcher; +use regex::{Regex, Captures}; + +pub trait IntoMatcher { + fn into_matcher(self) -> Matcher; +} + +impl IntoMatcher for Regex { + fn into_matcher(self) -> Matcher { + let path = self.as_str().to_string(); + Matcher::new(path, self) + } +} + +impl<'a> IntoMatcher for &'a str { + fn into_matcher(self) -> Matcher { + self.to_string().into_matcher() + } +} + +lazy_static! { + static ref REGEX_VAR_SEQ: Regex = Regex::new(r":([,a-zA-Z0-9_-]*)").unwrap(); +} + +impl IntoMatcher for String { + fn into_matcher(self) -> Matcher { + static FORMAT_VAR: &'static str = ":format"; + static VAR_SEQ: &'static str = "[,a-zA-Z0-9_-]*"; + static VAR_SEQ_WITH_SLASH: &'static str = "[,/a-zA-Z0-9_-]*"; + // matches request params (e.g. ?foo=true&bar=false) + static REGEX_PARAM_SEQ: &'static str = "(\\?[a-zA-Z0-9%_=&-]*)?"; + + let with_format = if self.contains(FORMAT_VAR) { + self + } else { + format!("{}(\\.{})?", self, FORMAT_VAR) + }; + + // first mark all double wildcards for replacement. + // We can't directly replace them since the replacement + // does contain the * symbol as well, which would get + // overwritten with the next replace call + let wildcarded = with_format.replace("**", "___DOUBLE_WILDCARD___") + // then replace the regular wildcard symbols (*) with the + // appropriate regex + .replace("*", VAR_SEQ) + // now replace the previously marked double wild cards (**) + .replace("___DOUBLE_WILDCARD___", VAR_SEQ_WITH_SLASH); + + // Add a named capture for each :(variable) symbol + let named_captures = REGEX_VAR_SEQ.replace_all(&wildcarded, |captures: &Captures| { + // There should only ever be one match (after subgroup 0) + let c = captures.iter().skip(1).next().unwrap(); + format!("(?P<{}>[,a-zA-Z0-9%_-]*)", c.unwrap()) + }); + + let line_regex = format!("^{}{}$", named_captures, REGEX_PARAM_SEQ); + let regex = Regex::new(&line_regex).unwrap(); + Matcher::new(with_format, regex) + } +} diff --git a/src/router/matcher.rs b/src/router/matcher.rs new file mode 100644 index 0000000000..659fe40bf8 --- /dev/null +++ b/src/router/matcher.rs @@ -0,0 +1,29 @@ +use std::borrow::{IntoCow, Cow}; +use std::ops::Deref; +use regex::Regex; + +pub struct Matcher { + pub path: Cow<'static, str>, + pub regex: Regex +} + +impl Matcher { + pub fn new>(path: P, regex: Regex) -> Matcher { + Matcher { + path: path.into_cow(), + regex: regex + } + } + + pub fn path(&self) -> &str { + &self.path + } +} + +impl Deref for Matcher { + type Target = Regex; + + fn deref(&self) -> &Regex { + &self.regex + } +} diff --git a/src/router/mod.rs b/src/router/mod.rs index 9bba75ba3b..c562659829 100644 --- a/src/router/mod.rs +++ b/src/router/mod.rs @@ -1,47 +1,10 @@ //!Router asigns handlers to paths and resolves them per request pub use self::http_router::HttpRouter; pub use self::router::{Router, Route, RouteResult}; +pub use self::matcher::Matcher; +pub use self::into_matcher::IntoMatcher; pub mod http_router; pub mod router; - -/// The path_utils collects some small helper methods that operate on the path -mod path_utils { - use regex::{Regex, Captures}; - - // matches named variables (e.g. :userid) - lazy_static! { - static ref REGEX_VAR_SEQ: Regex = Regex::new(r":([,a-zA-Z0-9_-]*)").unwrap(); - } - static VAR_SEQ: &'static str = "[,a-zA-Z0-9_-]*"; - static VAR_SEQ_WITH_SLASH: &'static str = "[,/a-zA-Z0-9_-]*"; - - // matches request params (e.g. ?foo=true&bar=false) - static REGEX_PARAM_SEQ: &'static str = "(\\?[a-zA-Z0-9%_=&-]*)?"; - - pub fn create_regex(route_path: &str) -> Regex { - let updated_path = - route_path.to_string() - // first mark all double wildcards for replacement. - // We can't directly replace them since the replacement - // does contain the * symbol as well, which would get - // overwritten with the next replace call - .replace("**", "___DOUBLE_WILDCARD___") - // then replace the regular wildcard symbols (*) with the - // appropriate regex - .replace("*", VAR_SEQ) - // now replace the previously marked double wild cards (**) - .replace("___DOUBLE_WILDCARD___", VAR_SEQ_WITH_SLASH); - - // Add a named capture for each :(variable) symbol - let named_captures = REGEX_VAR_SEQ.replace_all(&updated_path, |captures: &Captures| { - // There should only ever be one match (after subgroup 0) - let c = captures.iter().skip(1).next().unwrap(); - format!("(?P<{}>[,a-zA-Z0-9%_-]*)", c.unwrap()) - }); - - let line_regex = format!("^{}{}$", named_captures, REGEX_PARAM_SEQ); - - Regex::new(&line_regex).unwrap() - } -} +mod matcher; +mod into_matcher; diff --git a/src/router/router.rs b/src/router/router.rs index 2c4c7944e3..99b8672a21 100644 --- a/src/router/router.rs +++ b/src/router/router.rs @@ -1,21 +1,19 @@ use middleware::{Middleware, Continue, MiddlewareResult}; -use super::path_utils; use hyper::uri::RequestUri::AbsolutePath; use request::Request; use response::Response; use router::HttpRouter; use hyper::method::Method; use hyper::status::StatusCode; -use regex::Regex; +use router::{IntoMatcher, Matcher}; /// A Route is the basic data structure that stores both the path /// and the handler that gets executed for the route. /// The path can contain variable pattern such as `user/:userid/invoices` pub struct Route { - pub path: String, pub method: Method, pub handler: Box, - matcher: Regex + matcher: Matcher } /// A RouteResult is what the router returns when `match_route` is called. @@ -56,6 +54,9 @@ impl Router { } pub fn match_route<'a>(&'a self, method: &Method, path: &str) -> Option> { + // Strip off the querystring when matching a route + let path = path.splitn(1, '?').next().unwrap(); + self.routes .iter() .find(|item| item.method == *method && item.matcher.is_match(path)) @@ -82,18 +83,9 @@ fn extract_params(route: &Route, path: &str) -> Vec<(String, String)> { } impl HttpRouter for Router { - fn add_route(&mut self, method: Method, path: &str, handler: H) { - static FORMAT_VAR: &'static str = ":format"; - - let with_format = if path.contains(FORMAT_VAR) { - path.to_string() - } else { - format!("{}(\\.{})?", path, FORMAT_VAR) - }; - + fn add_route(&mut self, method: Method, matcher: M, handler: H) { let route = Route { - matcher: path_utils::create_regex(&with_format), - path: with_format, + matcher: matcher.into_matcher(), method: method, handler: Box::new(handler), }; @@ -110,7 +102,7 @@ impl Middleware for Router { AbsolutePath(ref url) => self.match_route(&req.origin.method, &**url), _ => None }; - debug!("route_result.route.path: {:?}", route_result.as_ref().map(|r| &*r.route.path)); + debug!("route_result.route.path: {:?}", route_result.as_ref().map(|r| r.route.matcher.path())); match route_result { Some(route_result) => { @@ -126,30 +118,40 @@ impl Middleware for Router { #[test] fn creates_regex_with_captures () { - let regex = path_utils::create_regex("foo/:uid/bar/:groupid"); - let caps = regex.captures("foo/4711/bar/5490").unwrap(); + let matcher = "foo/:uid/bar/:groupid".into_matcher(); + let caps = matcher.captures("foo/4711/bar/5490").unwrap(); + assert_eq!(matcher.path(), "foo/:uid/bar/:groupid(\\.:format)?"); assert_eq!(caps.at(1).unwrap(), "4711"); assert_eq!(caps.at(2).unwrap(), "5490"); - let regex = path_utils::create_regex("foo/*/:uid/bar/:groupid"); - let caps = regex.captures("foo/test/4711/bar/5490").unwrap(); + let matcher = "foo/*/:uid/bar/:groupid".into_matcher(); + let caps = matcher.captures("foo/test/4711/bar/5490").unwrap(); + assert_eq!(matcher.path(), "foo/*/:uid/bar/:groupid(\\.:format)?"); assert_eq!(caps.at(1).unwrap(), "4711"); assert_eq!(caps.at(2).unwrap(), "5490"); - let regex = path_utils::create_regex("foo/**/:uid/bar/:groupid"); - let caps = regex.captures("foo/test/another/4711/bar/5490").unwrap(); + let matcher = "foo/**/:uid/bar/:groupid".into_matcher(); + let caps = matcher.captures("foo/test/another/4711/bar/5490").unwrap(); + assert_eq!(matcher.path(), "foo/**/:uid/bar/:groupid(\\.:format)?"); assert_eq!(caps.at(1).unwrap(), "4711"); assert_eq!(caps.at(2).unwrap(), "5490"); + + let matcher = "foo/**/:format/bar/:groupid".into_matcher(); + let caps = matcher.captures("foo/test/another/4711/bar/5490").unwrap(); + + assert_eq!(matcher.path(), "foo/**/:format/bar/:groupid"); + assert_eq!(caps.name("format").unwrap(), "4711"); + assert_eq!(caps.name("groupid").unwrap(), "5490"); } #[test] fn creates_valid_regex_for_routes () { - let regex1 = path_utils::create_regex("foo/:uid/bar/:groupid"); - let regex2 = path_utils::create_regex("foo/*/bar"); - let regex3 = path_utils::create_regex("foo/**/bar"); + let regex1 = "foo/:uid/bar/:groupid".into_matcher(); + let regex2 = "foo/*/bar".into_matcher(); + let regex3 = "foo/**/bar".into_matcher(); assert_eq!(regex1.is_match("foo/4711/bar/5490"), true); assert_eq!(regex1.is_match("foo/4711/bar/5490?foo=true&bar=false"), true); @@ -242,3 +244,76 @@ fn can_match_var_routes () { assert_eq!(route_result.param("format"), "markdown"); } +#[test] +fn regex_path() { + use regex::Regex; + + let route_store = &mut Router::new(); + let handler = middleware! { "hello from foo" }; + + let regex = Regex::new("/(foo|bar)").unwrap(); + route_store.add_route(Method::Get, regex, handler); + + let route_result = route_store.match_route(&Method::Get, "/foo"); + assert!(route_result.is_some()); + + let route_result = route_store.match_route(&Method::Get, "/bar"); + assert!(route_result.is_some()); + + let route_result = route_store.match_route(&Method::Get, "/bar?foo"); + assert!(route_result.is_some()); + + let route_result = route_store.match_route(&Method::Get, "/baz"); + assert!(route_result.is_none()); +} + +#[test] +fn regex_path_named() { + use regex::Regex; + + let route_store = &mut Router::new(); + let handler = middleware! { "hello from foo" }; + + let regex = Regex::new("/(?Pfoo|bar)/b").unwrap(); + route_store.add_route(Method::Get, regex, handler); + + let route_result = route_store.match_route(&Method::Get, "/foo/b"); + assert!(route_result.is_some()); + + let route_result = route_result.unwrap(); + assert_eq!(route_result.param("a"), "foo"); + + let route_result = route_store.match_route(&Method::Get, "/bar/b"); + assert!(route_result.is_some()); + + let route_result = route_result.unwrap(); + assert_eq!(route_result.param("a"), "bar"); + + let route_result = route_store.match_route(&Method::Get, "/baz/b"); + assert!(route_result.is_none()); +} + +#[test] +fn ignores_querystring() { + use regex::Regex; + + let route_store = &mut Router::new(); + let handler = middleware! { "hello from foo" }; + + let regex = Regex::new("/(?Pfoo|bar)/b").unwrap(); + route_store.add_route(Method::Get, regex, handler); + route_store.add_route(Method::Get, "/:foo", handler); + + // Should ignore the querystring + let route_result = route_store.match_route(&Method::Get, "/moo?foo"); + assert!(route_result.is_some()); + + let route_result = route_result.unwrap(); + assert_eq!(route_result.param("foo"), "moo"); + + let route_result = route_store.match_route(&Method::Get, "/bar/b?foo"); + assert!(route_result.is_some()); + + let route_result = route_result.unwrap(); + assert_eq!(route_result.param("a"), "bar"); +}