diff --git a/Cargo.toml b/Cargo.toml index ca6fdbfae..414fba9c7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,8 +37,6 @@ regex = "0.2" slab = "0.4" sha1 = "0.2" url = "1.5" -lazy_static = "0.2" -route-recognizer = "0.1" # tokio bytes = "0.4" diff --git a/src/application.rs b/src/application.rs index 819ab2aeb..5a152669c 100644 --- a/src/application.rs +++ b/src/application.rs @@ -2,13 +2,12 @@ use std::rc::Rc; use std::string::ToString; use std::collections::HashMap; -use route_recognizer::Router; - use task::Task; +use payload::Payload; use route::{RouteHandler, FnHandler}; use router::Handler; use resource::Resource; -use payload::Payload; +use recognizer::{RouteRecognizer, check_pattern}; use httprequest::HttpRequest; use httpresponse::HttpResponse; @@ -24,13 +23,12 @@ pub struct Application { impl Application where S: 'static { pub(crate) fn prepare(self, prefix: String) -> Box { - let mut router = Router::new(); let mut handlers = HashMap::new(); - let prefix = if prefix.ends_with('/') {prefix } else { prefix + "/" }; + let prefix = if prefix.ends_with('/') { prefix } else { prefix + "/" }; + let mut routes = Vec::new(); for (path, handler) in self.resources { - let path = prefix.clone() + path.trim_left_matches('/'); - router.add(path.as_str(), handler); + routes.push((path, handler)) } for (path, mut handler) in self.handlers { @@ -43,7 +41,7 @@ impl Application where S: 'static state: Rc::new(self.state), default: self.default, handlers: handlers, - router: router } + router: RouteRecognizer::new(prefix, routes) } ) } } @@ -95,6 +93,7 @@ impl Application where S: 'static { // add resource if !self.resources.contains_key(&path) { + check_pattern(&path); self.resources.insert(path.clone(), Resource::default()); } @@ -213,6 +212,7 @@ impl ApplicationBuilder where S: 'static { // add resource let path = path.to_string(); if !parts.resources.contains_key(&path) { + check_pattern(&path); parts.resources.insert(path.clone(), Resource::default()); } f(parts.resources.get_mut(&path).unwrap()); @@ -286,16 +286,20 @@ struct InnerApplication { state: Rc, default: Resource, handlers: HashMap>>, - router: Router>, + router: RouteRecognizer>, } impl Handler for InnerApplication { fn handle(&self, req: HttpRequest, payload: Payload) -> Task { - if let Ok(h) = self.router.recognize(req.path()) { - h.handler.handle( - req.with_match_info(h.params), payload, Rc::clone(&self.state)) + if let Some((params, h)) = self.router.recognize(req.path()) { + if let Some(params) = params { + h.handle( + req.with_match_info(params), payload, Rc::clone(&self.state)) + } else { + h.handle(req, payload, Rc::clone(&self.state)) + } } else { for (prefix, handler) in &self.handlers { if req.path().starts_with(prefix) { diff --git a/src/dev.rs b/src/dev.rs index 1a822e72e..71c6725c4 100644 --- a/src/dev.rs +++ b/src/dev.rs @@ -17,6 +17,7 @@ pub use payload::{Payload, PayloadItem, PayloadError}; pub use router::RoutingMap; pub use resource::{Reply, Resource}; pub use route::{Route, RouteFactory, RouteHandler}; +pub use recognizer::Params; pub use server::HttpServer; pub use context::HttpContext; pub use staticfiles::StaticFiles; @@ -25,7 +26,6 @@ pub use staticfiles::StaticFiles; pub use http::{Method, StatusCode}; pub use cookie::{Cookie, CookieBuilder}; pub use cookie::{ParseError as CookieParseError}; -pub use route_recognizer::Params; pub use http_range::{HttpRange, HttpRangeParseError}; // dev specific diff --git a/src/httprequest.rs b/src/httprequest.rs index 51b75f0b2..576a00159 100644 --- a/src/httprequest.rs +++ b/src/httprequest.rs @@ -3,10 +3,10 @@ use std::str; use url::form_urlencoded; use http::{header, Method, Version, Uri, HeaderMap}; -use Params; use {Cookie, CookieParseError}; use {HttpRange, HttpRangeParseError}; use error::ParseError; +use recognizer::Params; #[derive(Debug)] @@ -29,7 +29,7 @@ impl HttpRequest { uri: uri, version: version, headers: headers, - params: Params::new(), + params: Params::empty(), cookies: Vec::new(), } } diff --git a/src/lib.rs b/src/lib.rs index ec15e54c6..e1ebba87a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,9 +9,7 @@ extern crate log; extern crate time; extern crate bytes; extern crate sha1; -// extern crate regex; -// #[macro_use] -// extern crate lazy_static; +extern crate regex; #[macro_use] extern crate futures; extern crate tokio_core; @@ -23,7 +21,6 @@ extern crate http; extern crate httparse; extern crate http_range; extern crate mime_guess; -extern crate route_recognizer; extern crate url; extern crate actix; @@ -36,10 +33,11 @@ mod httprequest; mod httpresponse; mod payload; mod resource; +mod recognizer; mod route; mod router; -mod task; mod reader; +mod task; mod staticfiles; mod server; mod wsframe; @@ -54,8 +52,9 @@ pub use httprequest::HttpRequest; pub use httpresponse::{Body, HttpResponse, HttpResponseBuilder}; pub use payload::{Payload, PayloadItem, PayloadError}; pub use router::{Router, RoutingMap}; -pub use resource::{Reply, Resource}; pub use route::{Route, RouteFactory, RouteHandler}; +pub use resource::{Reply, Resource}; +pub use recognizer::{Params, RouteRecognizer}; pub use server::HttpServer; pub use context::HttpContext; pub use staticfiles::StaticFiles; @@ -64,5 +63,4 @@ pub use staticfiles::StaticFiles; pub use http::{Method, StatusCode}; pub use cookie::{Cookie, CookieBuilder}; pub use cookie::{ParseError as CookieParseError}; -pub use route_recognizer::Params; pub use http_range::{HttpRange, HttpRangeParseError}; diff --git a/src/main.rs b/src/main.rs index a55be82f3..1bf366010 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,6 +18,7 @@ impl Route for MyRoute { type State = (); fn request(req: HttpRequest, payload: Payload, ctx: &mut HttpContext) -> Reply { + println!("PARAMS: {:?} {:?}", req.match_info().get("name"), req.match_info()); if !payload.eof() { ctx.add_stream(payload); Reply::stream(MyRoute{req: Some(req)}) @@ -105,7 +106,7 @@ fn main() { HttpServer::new( RoutingMap::default() .app("/blah", Application::default() - .resource("/test", |r| { + .resource("/test/{name}", |r| { r.get::(); r.post::(); }) diff --git a/src/recognizer.rs b/src/recognizer.rs new file mode 100644 index 000000000..b95013759 --- /dev/null +++ b/src/recognizer.rs @@ -0,0 +1,164 @@ +use std::rc::Rc; +use std::collections::HashMap; + +use regex::{Regex, RegexSet, Captures}; + + +#[doc(hidden)] +pub struct RouteRecognizer { + prefix: usize, + patterns: RegexSet, + routes: Vec<(Pattern, T)>, +} + +impl RouteRecognizer { + pub fn new(prefix: String, routes: Vec<(String, T)>) -> Self { + let mut paths = Vec::new(); + let mut handlers = Vec::new(); + for item in routes { + let pat = parse(&item.0); + handlers.push((Pattern::new(&pat), item.1)); + paths.push(pat); + }; + let regset = RegexSet::new(&paths); + + RouteRecognizer { + prefix: prefix.len() - 1, + patterns: regset.unwrap(), + routes: handlers, + } + } + + pub fn recognize(&self, path: &str) -> Option<(Option, &T)> { + if let Some(idx) = self.patterns.matches(&path[self.prefix..]).into_iter().next() + { + let (ref pattern, ref route) = self.routes[idx]; + Some((pattern.match_info(&path[self.prefix..]), route)) + } else { + None + } + } +} + +struct Pattern { + re: Regex, + names: Rc>, +} + +impl Pattern { + fn new(pattern: &str) -> Self { + let re = Regex::new(pattern).unwrap(); + let names = re.capture_names() + .enumerate() + .filter_map(|(i, name)| name.map(|name| (name.to_owned(), i))) + .collect(); + + Pattern { + re, + names: Rc::new(names), + } + } + + fn match_info(&self, text: &str) -> Option { + let captures = match self.re.captures(text) { + Some(captures) => captures, + None => return None, + }; + + Some(Params::new(Rc::clone(&self.names), text, captures)) + } +} + +pub(crate) fn check_pattern(path: &str) { + if let Err(err) = Regex::new(&parse(path)) { + panic!("Wrong path pattern: \"{}\" {}", path, err); + } +} + +fn parse(pattern: &str) -> String { + const DEFAULT_PATTERN: &'static str = "[^/]+"; + + let mut re = String::from("^/"); + let mut in_param = false; + let mut in_param_pattern = false; + let mut param_name = String::new(); + let mut param_pattern = String::from(DEFAULT_PATTERN); + + for (index, ch) in pattern.chars().enumerate() { + // All routes must have a leading slash so its optional to have one + if index == 0 && ch == '/' { + continue; + } + + if in_param { + // In parameter segment: `{....}` + if ch == '}' { + re.push_str(&format!(r"(?P<{}>{})", ¶m_name, ¶m_pattern)); + + param_name.clear(); + param_pattern = String::from(DEFAULT_PATTERN); + + in_param_pattern = false; + in_param = false; + } else if ch == ':' { + // The parameter name has been determined; custom pattern land + in_param_pattern = true; + param_pattern.clear(); + } else if in_param_pattern { + // Ignore leading whitespace for pattern + if !(ch == ' ' && param_pattern.is_empty()) { + param_pattern.push(ch); + } + } else { + param_name.push(ch); + } + } else if ch == '{' { + in_param = true; + } else { + re.push(ch); + } + } + + re.push('$'); + re +} + +#[derive(Debug)] +pub struct Params { + text: String, + matches: Vec>, + names: Rc>, +} + +impl Params { + pub(crate) fn new(names: Rc>, text: &str, captures: Captures) -> Self + { + Params { + names, + text: text.into(), + matches: captures + .iter() + .map(|capture| capture.map(|m| (m.start(), m.end()))) + .collect(), + } + } + + pub(crate) fn empty() -> Self + { + Params { + text: String::new(), + names: Rc::new(HashMap::new()), + matches: Vec::new(), + } + } + + fn by_idx(&self, index: usize) -> Option<&str> { + self.matches + .get(index + 1) + .and_then(|m| m.map(|(start, end)| &self.text[start..end])) + } + + pub fn get(&self, key: &str) -> Option<&str> { + self.names.get(key).and_then(|&i| self.by_idx(i - 1)) + } +} diff --git a/src/resource.rs b/src/resource.rs index 171b117d5..762a41179 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -32,6 +32,7 @@ use httpcodes::HTTPMethodNotAllowed; /// .finish(); /// } pub struct Resource { + name: String, state: PhantomData, routes: HashMap>>, default: Box>, @@ -40,6 +41,7 @@ pub struct Resource { impl Default for Resource { fn default() -> Self { Resource { + name: String::new(), state: PhantomData, routes: HashMap::new(), default: Box::new(HTTPMethodNotAllowed)} @@ -49,6 +51,11 @@ impl Default for Resource { impl Resource where S: 'static { + /// Set resource name + pub fn set_name(&mut self, name: T) { + self.name = name.to_string(); + } + /// Register handler for specified method. pub fn handler(&mut self, method: Method, handler: F) where F: Fn(HttpRequest, Payload, &S) -> R + 'static, diff --git a/src/router.rs b/src/router.rs index d9f21668b..8042822c5 100644 --- a/src/router.rs +++ b/src/router.rs @@ -1,12 +1,12 @@ use std::rc::Rc; use std::string::ToString; use std::collections::HashMap; -use route_recognizer::{Router as Recognizer}; use task::Task; use payload::Payload; use route::RouteHandler; use resource::Resource; +use recognizer::{RouteRecognizer, check_pattern}; use application::Application; use httpcodes::HTTPNotFound; use httprequest::HttpRequest; @@ -18,15 +18,20 @@ pub(crate) trait Handler: 'static { /// Server routing map pub struct Router { apps: HashMap>, - resources: Recognizer, + resources: RouteRecognizer, } impl Router { pub(crate) fn call(&self, req: HttpRequest, payload: Payload) -> Task { - if let Ok(h) = self.resources.recognize(req.path()) { - h.handler.handle(req.with_match_info(h.params), payload, Rc::new(())) + if let Some((params, h)) = self.resources.recognize(req.path()) { + if let Some(params) = params { + h.handle( + req.with_match_info(params), payload, Rc::new(())) + } else { + h.handle(req, payload, Rc::new(())) + } } else { for (prefix, app) in &self.apps { if req.path().starts_with(prefix) { @@ -40,17 +45,26 @@ impl Router { /// Request routing map builder /// -/// Route supports glob patterns: * for a single wildcard segment and :param -/// for matching storing that segment of the request url in the Params object, -/// which is stored in the request. +/// Resource may have variable path also. For instance, a resource with +/// the path '/a/{name}/c' would match all incoming requests with paths +/// such as '/a/b/c', '/a/1/c', and '/a/etc/c'. /// -/// For instance, to route Get requests on any route matching /users/:userid/:friend and +/// A variable part is specified in the form {identifier}, where +/// the identifier can be used later in a request handler to access the matched +/// value for that part. This is done by looking up the identifier +/// in the Params object returned by `Request.match_info()` method. +/// +/// By default, each part matches the regular expression [^{}/]+. +/// +/// You can also specify a custom regex in the form {identifier:regex}: +/// +/// For instance, to route Get requests on any route matching /users/{userid}/{friend} and /// store userid and friend in the exposed Params object: /// /// ```rust,ignore /// let mut map = RoutingMap::default(); /// -/// map.resource("/users/:userid/:friendid", |r| r.get::()); +/// map.resource("/users/{userid}/{friend}", |r| r.get::()); /// ``` pub struct RoutingMap { parts: Option, @@ -134,6 +148,7 @@ impl RoutingMap { // add resource let path = path.to_string(); if !parts.resources.contains_key(&path) { + check_pattern(&path); parts.resources.insert(path.clone(), Resource::default()); } // configure resource @@ -147,15 +162,14 @@ impl RoutingMap { { let parts = self.parts.take().expect("Use after finish"); - let mut router = Recognizer::new(); - + let mut routes = Vec::new(); for (path, resource) in parts.resources { - router.add(path.as_str(), resource); + routes.push((path, resource)) } Router { apps: parts.apps, - resources: router, + resources: RouteRecognizer::new("/".to_owned(), routes), } } } diff --git a/tests/test_httprequest.rs b/tests/test_httprequest.rs index 8d1c594e4..d25778841 100644 --- a/tests/test_httprequest.rs +++ b/tests/test_httprequest.rs @@ -79,14 +79,15 @@ fn test_request_query() { #[test] fn test_request_match_info() { - let req = HttpRequest::new(Method::GET, Uri::try_from("/?id=test").unwrap(), + let req = HttpRequest::new(Method::GET, Uri::try_from("/value/?id=test").unwrap(), Version::HTTP_11, HeaderMap::new()); - let mut params = Params::new(); - params.insert("key".to_owned(), "value".to_owned()); + let rec = RouteRecognizer::new("/".to_owned(), vec![("/{key}/".to_owned(), 1)]); + let (params, _) = rec.recognize(req.path()).unwrap(); + let params = params.unwrap(); let req = req.with_match_info(params); - assert_eq!(req.match_info().find("key"), Some("value")); + assert_eq!(req.match_info().get("key"), Some("value")); } #[test]