mirror of
https://github.com/actix/actix-extras.git
synced 2024-11-27 17:22:57 +01:00
new router recognizer
This commit is contained in:
parent
35107f64e7
commit
f59f68eded
@ -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"
|
||||
|
@ -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<S=()> {
|
||||
impl<S> Application<S> where S: 'static
|
||||
{
|
||||
pub(crate) fn prepare(self, prefix: String) -> Box<Handler> {
|
||||
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<S> Application<S> 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<S> Application<S> 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<S> ApplicationBuilder<S> 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<S> {
|
||||
state: Rc<S>,
|
||||
default: Resource<S>,
|
||||
handlers: HashMap<String, Box<RouteHandler<S>>>,
|
||||
router: Router<Resource<S>>,
|
||||
router: RouteRecognizer<Resource<S>>,
|
||||
}
|
||||
|
||||
|
||||
impl<S: 'static> Handler for InnerApplication<S> {
|
||||
|
||||
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) {
|
||||
|
@ -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
|
||||
|
@ -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(),
|
||||
}
|
||||
}
|
||||
|
12
src/lib.rs
12
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};
|
||||
|
@ -18,6 +18,7 @@ impl Route for MyRoute {
|
||||
type State = ();
|
||||
|
||||
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() {
|
||||
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::<MyRoute>();
|
||||
r.post::<MyRoute>();
|
||||
})
|
||||
|
164
src/recognizer.rs
Normal file
164
src/recognizer.rs
Normal 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<{}>{})", ¶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<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))
|
||||
}
|
||||
}
|
@ -32,6 +32,7 @@ use httpcodes::HTTPMethodNotAllowed;
|
||||
/// .finish();
|
||||
/// }
|
||||
pub struct Resource<S=()> {
|
||||
name: String,
|
||||
state: PhantomData<S>,
|
||||
routes: HashMap<Method, Box<RouteHandler<S>>>,
|
||||
default: Box<RouteHandler<S>>,
|
||||
@ -40,6 +41,7 @@ pub struct Resource<S=()> {
|
||||
impl<S> Default for Resource<S> {
|
||||
fn default() -> Self {
|
||||
Resource {
|
||||
name: String::new(),
|
||||
state: PhantomData,
|
||||
routes: HashMap::new(),
|
||||
default: Box::new(HTTPMethodNotAllowed)}
|
||||
@ -49,6 +51,11 @@ impl<S> Default for Resource<S> {
|
||||
|
||||
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.
|
||||
pub fn handler<F, R>(&mut self, method: Method, handler: F)
|
||||
where F: Fn(HttpRequest, Payload, &S) -> R + 'static,
|
||||
|
@ -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<String, Box<Handler>>,
|
||||
resources: Recognizer<Resource>,
|
||||
resources: RouteRecognizer<Resource>,
|
||||
}
|
||||
|
||||
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::<MyRoute>());
|
||||
/// map.resource("/users/{userid}/{friend}", |r| r.get::<MyRoute>());
|
||||
/// ```
|
||||
pub struct RoutingMap {
|
||||
parts: Option<RoutingMapParts>,
|
||||
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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]
|
||||
|
Loading…
Reference in New Issue
Block a user