Skip to content

Commit

Permalink
feat(router): allow Regex paths
Browse files Browse the repository at this point in the history
  • Loading branch information
Ryman committed Apr 5, 2015
1 parent 3bbd767 commit 9fd6e06
Show file tree
Hide file tree
Showing 9 changed files with 245 additions and 86 deletions.
14 changes: 11 additions & 3 deletions README.md
Expand Up @@ -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)]
Expand Down Expand Up @@ -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<name>[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"
Expand Down
14 changes: 11 additions & 3 deletions 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)]
Expand Down Expand Up @@ -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<name>[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"
Expand Down
11 changes: 10 additions & 1 deletion 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 {
Expand Down Expand Up @@ -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<name>[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
Expand All @@ -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| {
Expand Down
7 changes: 3 additions & 4 deletions 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;
Expand All @@ -21,10 +21,9 @@ pub struct Nickel{
}

impl HttpRouter for Nickel {
fn add_route<H: Middleware>(&mut self, method: Method, uri: &str, handler: H) {
fn add_route<M: IntoMatcher, H: Middleware>(&mut self, method: Method, matcher: M, handler: H) {
let mut router = Router::new();
// FIXME: Inference failure in nightly 22/10/2014
router.add_route::<H>(method, uri, handler);
router.add_route(method, matcher, handler);
self.utilize(router);
}
}
Expand Down
25 changes: 16 additions & 9 deletions 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.
Expand All @@ -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! "};
Expand All @@ -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<H: Middleware>(&mut self, Method, &str, H);
fn add_route<M: IntoMatcher, H: Middleware>(&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
Expand Down Expand Up @@ -102,8 +109,8 @@ pub trait HttpRouter {
/// server.utilize(router);
/// }
/// ```
fn get<H: Middleware>(&mut self, uri: &str, handler: H) {
self.add_route(Method::Get, uri, handler);
fn get<M: IntoMatcher, H: Middleware>(&mut self, matcher: M, handler: H) {
self.add_route(Method::Get, matcher, handler);
}

/// Registers a handler to be used for a specific POST request.
Expand All @@ -125,8 +132,8 @@ pub trait HttpRouter {
/// });
/// # }
/// ```
fn post<H: Middleware>(&mut self, uri: &str, handler: H) {
self.add_route(Method::Post, uri, handler);
fn post<M: IntoMatcher, H: Middleware>(&mut self, matcher: M, handler: H) {
self.add_route(Method::Post, matcher, handler);
}

/// Registers a handler to be used for a specific PUT request.
Expand All @@ -148,8 +155,8 @@ pub trait HttpRouter {
/// });
/// # }
/// ```
fn put<H: Middleware>(&mut self, uri: &str, handler: H) {
self.add_route(Method::Put, uri, handler);
fn put<M: IntoMatcher, H: Middleware>(&mut self, matcher: M, handler: H) {
self.add_route(Method::Put, matcher, handler);
}

/// Registers a handler to be used for a specific DELETE request.
Expand All @@ -170,7 +177,7 @@ pub trait HttpRouter {
/// });
/// # }
/// ```
fn delete<H: Middleware>(&mut self, uri: &str, handler: H) {
self.add_route(Method::Delete, uri, handler);
fn delete<M: IntoMatcher, H: Middleware>(&mut self, matcher: M, handler: H) {
self.add_route(Method::Delete, matcher, handler);
}
}
61 changes: 61 additions & 0 deletions 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)
}
}
29 changes: 29 additions & 0 deletions 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<P: IntoCow<'static, str>>(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
}
}
45 changes: 4 additions & 41 deletions 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;

0 comments on commit 9fd6e06

Please sign in to comment.