1
0
Fork 0
mirror of https://github.com/actix/actix-web.git synced 2024-12-30 03:50:42 +00:00

new router recognizer

This commit is contained in:
Nikolay Kim 2017-10-16 19:21:24 -07:00
parent 35107f64e7
commit f59f68eded
10 changed files with 229 additions and 42 deletions

View file

@ -37,8 +37,6 @@ regex = "0.2"
slab = "0.4" slab = "0.4"
sha1 = "0.2" sha1 = "0.2"
url = "1.5" url = "1.5"
lazy_static = "0.2"
route-recognizer = "0.1"
# tokio # tokio
bytes = "0.4" bytes = "0.4"

View file

@ -2,13 +2,12 @@ use std::rc::Rc;
use std::string::ToString; use std::string::ToString;
use std::collections::HashMap; use std::collections::HashMap;
use route_recognizer::Router;
use task::Task; use task::Task;
use payload::Payload;
use route::{RouteHandler, FnHandler}; use route::{RouteHandler, FnHandler};
use router::Handler; use router::Handler;
use resource::Resource; use resource::Resource;
use payload::Payload; use recognizer::{RouteRecognizer, check_pattern};
use httprequest::HttpRequest; use httprequest::HttpRequest;
use httpresponse::HttpResponse; use httpresponse::HttpResponse;
@ -24,13 +23,12 @@ pub struct Application<S=()> {
impl<S> Application<S> where S: 'static impl<S> Application<S> where S: 'static
{ {
pub(crate) fn prepare(self, prefix: String) -> Box<Handler> { pub(crate) fn prepare(self, prefix: String) -> Box<Handler> {
let mut router = Router::new();
let mut handlers = HashMap::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 { for (path, handler) in self.resources {
let path = prefix.clone() + path.trim_left_matches('/'); routes.push((path, handler))
router.add(path.as_str(), handler);
} }
for (path, mut handler) in self.handlers { for (path, mut handler) in self.handlers {
@ -43,7 +41,7 @@ impl<S> Application<S> where S: 'static
state: Rc::new(self.state), state: Rc::new(self.state),
default: self.default, default: self.default,
handlers: handlers, handlers: handlers,
router: router } router: RouteRecognizer::new(prefix, routes) }
) )
} }
} }
@ -95,6 +93,7 @@ impl<S> Application<S> where S: 'static {
// add resource // add resource
if !self.resources.contains_key(&path) { if !self.resources.contains_key(&path) {
check_pattern(&path);
self.resources.insert(path.clone(), Resource::default()); self.resources.insert(path.clone(), Resource::default());
} }
@ -213,6 +212,7 @@ impl<S> ApplicationBuilder<S> where S: 'static {
// add resource // add resource
let path = path.to_string(); let path = path.to_string();
if !parts.resources.contains_key(&path) { if !parts.resources.contains_key(&path) {
check_pattern(&path);
parts.resources.insert(path.clone(), Resource::default()); parts.resources.insert(path.clone(), Resource::default());
} }
f(parts.resources.get_mut(&path).unwrap()); f(parts.resources.get_mut(&path).unwrap());
@ -286,16 +286,20 @@ struct InnerApplication<S> {
state: Rc<S>, state: Rc<S>,
default: Resource<S>, default: Resource<S>,
handlers: HashMap<String, Box<RouteHandler<S>>>, handlers: HashMap<String, Box<RouteHandler<S>>>,
router: Router<Resource<S>>, router: RouteRecognizer<Resource<S>>,
} }
impl<S: 'static> Handler for InnerApplication<S> { impl<S: 'static> Handler for InnerApplication<S> {
fn handle(&self, req: HttpRequest, payload: Payload) -> Task { fn handle(&self, req: HttpRequest, payload: Payload) -> Task {
if let Ok(h) = self.router.recognize(req.path()) { if let Some((params, h)) = self.router.recognize(req.path()) {
h.handler.handle( if let Some(params) = params {
req.with_match_info(h.params), payload, Rc::clone(&self.state)) h.handle(
req.with_match_info(params), payload, Rc::clone(&self.state))
} else {
h.handle(req, payload, Rc::clone(&self.state))
}
} else { } else {
for (prefix, handler) in &self.handlers { for (prefix, handler) in &self.handlers {
if req.path().starts_with(prefix) { if req.path().starts_with(prefix) {

View file

@ -17,6 +17,7 @@ pub use payload::{Payload, PayloadItem, PayloadError};
pub use router::RoutingMap; pub use router::RoutingMap;
pub use resource::{Reply, Resource}; pub use resource::{Reply, Resource};
pub use route::{Route, RouteFactory, RouteHandler}; pub use route::{Route, RouteFactory, RouteHandler};
pub use recognizer::Params;
pub use server::HttpServer; pub use server::HttpServer;
pub use context::HttpContext; pub use context::HttpContext;
pub use staticfiles::StaticFiles; pub use staticfiles::StaticFiles;
@ -25,7 +26,6 @@ pub use staticfiles::StaticFiles;
pub use http::{Method, StatusCode}; pub use http::{Method, StatusCode};
pub use cookie::{Cookie, CookieBuilder}; pub use cookie::{Cookie, CookieBuilder};
pub use cookie::{ParseError as CookieParseError}; pub use cookie::{ParseError as CookieParseError};
pub use route_recognizer::Params;
pub use http_range::{HttpRange, HttpRangeParseError}; pub use http_range::{HttpRange, HttpRangeParseError};
// dev specific // dev specific

View file

@ -3,10 +3,10 @@ use std::str;
use url::form_urlencoded; use url::form_urlencoded;
use http::{header, Method, Version, Uri, HeaderMap}; use http::{header, Method, Version, Uri, HeaderMap};
use Params;
use {Cookie, CookieParseError}; use {Cookie, CookieParseError};
use {HttpRange, HttpRangeParseError}; use {HttpRange, HttpRangeParseError};
use error::ParseError; use error::ParseError;
use recognizer::Params;
#[derive(Debug)] #[derive(Debug)]
@ -29,7 +29,7 @@ impl HttpRequest {
uri: uri, uri: uri,
version: version, version: version,
headers: headers, headers: headers,
params: Params::new(), params: Params::empty(),
cookies: Vec::new(), cookies: Vec::new(),
} }
} }

View file

@ -9,9 +9,7 @@ extern crate log;
extern crate time; extern crate time;
extern crate bytes; extern crate bytes;
extern crate sha1; extern crate sha1;
// extern crate regex; extern crate regex;
// #[macro_use]
// extern crate lazy_static;
#[macro_use] #[macro_use]
extern crate futures; extern crate futures;
extern crate tokio_core; extern crate tokio_core;
@ -23,7 +21,6 @@ extern crate http;
extern crate httparse; extern crate httparse;
extern crate http_range; extern crate http_range;
extern crate mime_guess; extern crate mime_guess;
extern crate route_recognizer;
extern crate url; extern crate url;
extern crate actix; extern crate actix;
@ -36,10 +33,11 @@ mod httprequest;
mod httpresponse; mod httpresponse;
mod payload; mod payload;
mod resource; mod resource;
mod recognizer;
mod route; mod route;
mod router; mod router;
mod task;
mod reader; mod reader;
mod task;
mod staticfiles; mod staticfiles;
mod server; mod server;
mod wsframe; mod wsframe;
@ -54,8 +52,9 @@ pub use httprequest::HttpRequest;
pub use httpresponse::{Body, HttpResponse, HttpResponseBuilder}; pub use httpresponse::{Body, HttpResponse, HttpResponseBuilder};
pub use payload::{Payload, PayloadItem, PayloadError}; pub use payload::{Payload, PayloadItem, PayloadError};
pub use router::{Router, RoutingMap}; pub use router::{Router, RoutingMap};
pub use resource::{Reply, Resource};
pub use route::{Route, RouteFactory, RouteHandler}; pub use route::{Route, RouteFactory, RouteHandler};
pub use resource::{Reply, Resource};
pub use recognizer::{Params, RouteRecognizer};
pub use server::HttpServer; pub use server::HttpServer;
pub use context::HttpContext; pub use context::HttpContext;
pub use staticfiles::StaticFiles; pub use staticfiles::StaticFiles;
@ -64,5 +63,4 @@ pub use staticfiles::StaticFiles;
pub use http::{Method, StatusCode}; pub use http::{Method, StatusCode};
pub use cookie::{Cookie, CookieBuilder}; pub use cookie::{Cookie, CookieBuilder};
pub use cookie::{ParseError as CookieParseError}; pub use cookie::{ParseError as CookieParseError};
pub use route_recognizer::Params;
pub use http_range::{HttpRange, HttpRangeParseError}; pub use http_range::{HttpRange, HttpRangeParseError};

View file

@ -18,6 +18,7 @@ impl Route for MyRoute {
type State = (); type State = ();
fn request(req: HttpRequest, payload: Payload, ctx: &mut HttpContext<Self>) -> Reply<Self> { fn request(req: HttpRequest, payload: Payload, ctx: &mut HttpContext<Self>) -> Reply<Self> {
println!("PARAMS: {:?} {:?}", req.match_info().get("name"), req.match_info());
if !payload.eof() { if !payload.eof() {
ctx.add_stream(payload); ctx.add_stream(payload);
Reply::stream(MyRoute{req: Some(req)}) Reply::stream(MyRoute{req: Some(req)})
@ -105,7 +106,7 @@ fn main() {
HttpServer::new( HttpServer::new(
RoutingMap::default() RoutingMap::default()
.app("/blah", Application::default() .app("/blah", Application::default()
.resource("/test", |r| { .resource("/test/{name}", |r| {
r.get::<MyRoute>(); r.get::<MyRoute>();
r.post::<MyRoute>(); r.post::<MyRoute>();
}) })

164
src/recognizer.rs Normal file
View file

@ -0,0 +1,164 @@
use std::rc::Rc;
use std::collections::HashMap;
use regex::{Regex, RegexSet, Captures};
#[doc(hidden)]
pub struct RouteRecognizer<T> {
prefix: usize,
patterns: RegexSet,
routes: Vec<(Pattern, T)>,
}
impl<T> RouteRecognizer<T> {
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<Params>, &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<HashMap<String, usize>>,
}
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<Params> {
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<{}>{})", &param_name, &param_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<Option<(usize, usize)>>,
names: Rc<HashMap<String, usize>>,
}
impl Params {
pub(crate) fn new(names: Rc<HashMap<String, usize>>, 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))
}
}

View file

@ -32,6 +32,7 @@ use httpcodes::HTTPMethodNotAllowed;
/// .finish(); /// .finish();
/// } /// }
pub struct Resource<S=()> { pub struct Resource<S=()> {
name: String,
state: PhantomData<S>, state: PhantomData<S>,
routes: HashMap<Method, Box<RouteHandler<S>>>, routes: HashMap<Method, Box<RouteHandler<S>>>,
default: Box<RouteHandler<S>>, default: Box<RouteHandler<S>>,
@ -40,6 +41,7 @@ pub struct Resource<S=()> {
impl<S> Default for Resource<S> { impl<S> Default for Resource<S> {
fn default() -> Self { fn default() -> Self {
Resource { Resource {
name: String::new(),
state: PhantomData, state: PhantomData,
routes: HashMap::new(), routes: HashMap::new(),
default: Box::new(HTTPMethodNotAllowed)} default: Box::new(HTTPMethodNotAllowed)}
@ -49,6 +51,11 @@ impl<S> Default for Resource<S> {
impl<S> Resource<S> where S: 'static { impl<S> Resource<S> where S: 'static {
/// Set resource name
pub fn set_name<T: ToString>(&mut self, name: T) {
self.name = name.to_string();
}
/// Register handler for specified method. /// Register handler for specified method.
pub fn handler<F, R>(&mut self, method: Method, handler: F) pub fn handler<F, R>(&mut self, method: Method, handler: F)
where F: Fn(HttpRequest, Payload, &S) -> R + 'static, where F: Fn(HttpRequest, Payload, &S) -> R + 'static,

View file

@ -1,12 +1,12 @@
use std::rc::Rc; use std::rc::Rc;
use std::string::ToString; use std::string::ToString;
use std::collections::HashMap; use std::collections::HashMap;
use route_recognizer::{Router as Recognizer};
use task::Task; use task::Task;
use payload::Payload; use payload::Payload;
use route::RouteHandler; use route::RouteHandler;
use resource::Resource; use resource::Resource;
use recognizer::{RouteRecognizer, check_pattern};
use application::Application; use application::Application;
use httpcodes::HTTPNotFound; use httpcodes::HTTPNotFound;
use httprequest::HttpRequest; use httprequest::HttpRequest;
@ -18,15 +18,20 @@ pub(crate) trait Handler: 'static {
/// Server routing map /// Server routing map
pub struct Router { pub struct Router {
apps: HashMap<String, Box<Handler>>, apps: HashMap<String, Box<Handler>>,
resources: Recognizer<Resource>, resources: RouteRecognizer<Resource>,
} }
impl Router { impl Router {
pub(crate) fn call(&self, req: HttpRequest, payload: Payload) -> Task pub(crate) fn call(&self, req: HttpRequest, payload: Payload) -> Task
{ {
if let Ok(h) = self.resources.recognize(req.path()) { if let Some((params, h)) = self.resources.recognize(req.path()) {
h.handler.handle(req.with_match_info(h.params), payload, Rc::new(())) if let Some(params) = params {
h.handle(
req.with_match_info(params), payload, Rc::new(()))
} else {
h.handle(req, payload, Rc::new(()))
}
} else { } else {
for (prefix, app) in &self.apps { for (prefix, app) in &self.apps {
if req.path().starts_with(prefix) { if req.path().starts_with(prefix) {
@ -40,17 +45,26 @@ impl Router {
/// Request routing map builder /// Request routing map builder
/// ///
/// Route supports glob patterns: * for a single wildcard segment and :param /// Resource may have variable path also. For instance, a resource with
/// for matching storing that segment of the request url in the Params object, /// the path '/a/{name}/c' would match all incoming requests with paths
/// which is stored in the request. /// 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: /// store userid and friend in the exposed Params object:
/// ///
/// ```rust,ignore /// ```rust,ignore
/// let mut map = RoutingMap::default(); /// let mut map = RoutingMap::default();
/// ///
/// map.resource("/users/:userid/:friendid", |r| r.get::<MyRoute>()); /// map.resource("/users/{userid}/{friend}", |r| r.get::<MyRoute>());
/// ``` /// ```
pub struct RoutingMap { pub struct RoutingMap {
parts: Option<RoutingMapParts>, parts: Option<RoutingMapParts>,
@ -134,6 +148,7 @@ impl RoutingMap {
// add resource // add resource
let path = path.to_string(); let path = path.to_string();
if !parts.resources.contains_key(&path) { if !parts.resources.contains_key(&path) {
check_pattern(&path);
parts.resources.insert(path.clone(), Resource::default()); parts.resources.insert(path.clone(), Resource::default());
} }
// configure resource // configure resource
@ -147,15 +162,14 @@ impl RoutingMap {
{ {
let parts = self.parts.take().expect("Use after finish"); 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 { for (path, resource) in parts.resources {
router.add(path.as_str(), resource); routes.push((path, resource))
} }
Router { Router {
apps: parts.apps, apps: parts.apps,
resources: router, resources: RouteRecognizer::new("/".to_owned(), routes),
} }
} }
} }

View file

@ -79,14 +79,15 @@ fn test_request_query() {
#[test] #[test]
fn test_request_match_info() { 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()); Version::HTTP_11, HeaderMap::new());
let mut params = Params::new(); let rec = RouteRecognizer::new("/".to_owned(), vec![("/{key}/".to_owned(), 1)]);
params.insert("key".to_owned(), "value".to_owned()); let (params, _) = rec.recognize(req.path()).unwrap();
let params = params.unwrap();
let req = req.with_match_info(params); 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] #[test]