From 968f5d39d6cf9759b94854c523bc5f2a56596301 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Thu, 7 Dec 2017 16:22:26 -0800 Subject: [PATCH] added external resources; refactor route recognizer --- .travis.yml | 2 +- Cargo.toml | 1 + src/application.rs | 75 ++++++-- src/fs.rs | 2 +- src/httprequest.rs | 43 ++++- src/lib.rs | 9 +- src/param.rs | 203 ++++++++++++++++++++ src/recognizer.rs | 324 ++------------------------------ src/resource.rs | 4 +- src/router.rs | 381 +++++++++++++++++++++++++++++++++++--- tests/test_httprequest.rs | 11 +- 11 files changed, 686 insertions(+), 369 deletions(-) create mode 100644 src/param.rs diff --git a/.travis.yml b/.travis.yml index 5449c1ab5..883a9d40c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -50,7 +50,7 @@ after_success: - | if [[ "$TRAVIS_OS_NAME" == "linux" && "$TRAVIS_RUST_VERSION" == "stable" ]]; then bash <(curl https://raw.githubusercontent.com/xd009642/tarpaulin/master/travis-install.sh) - cargo tarpaulin --ignore-tests --out Xml + cargo tarpaulin --out Xml bash <(curl -s https://codecov.io/bash) echo "Uploaded code coverage" fi diff --git a/Cargo.toml b/Cargo.toml index bb4b7b932..2ff98b27f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,6 +52,7 @@ serde_json = "1.0" flate2 = "0.2" brotli2 = "^0.3.2" percent-encoding = "1.0" +smallvec = "0.6" # redis-async = { git="https://github.com/benashford/redis-async-rs" } diff --git a/src/application.rs b/src/application.rs index ae5e5c007..0c2b376c3 100644 --- a/src/application.rs +++ b/src/application.rs @@ -2,9 +2,8 @@ use std::rc::Rc; use std::collections::HashMap; use handler::{Reply, RouteHandler}; -use router::Router; +use router::{Router, Pattern}; use resource::Resource; -use recognizer::check_pattern; use httprequest::HttpRequest; use channel::{HttpHandler, IntoHttpHandler}; use pipeline::Pipeline; @@ -24,11 +23,7 @@ impl HttpApplication { fn run(&self, req: HttpRequest) -> Reply { let mut req = req.with_state(Rc::clone(&self.state), self.router.clone()); - if let Some((params, h)) = self.router.query(req.path()) { - if let Some(params) = params { - req.set_match_info(params); - req.set_prefix(self.router.prefix().len()); - } + if let Some(h) = self.router.recognize(&mut req) { h.handle(req) } else { self.default.handle(req) @@ -52,7 +47,8 @@ struct ApplicationParts { state: S, prefix: String, default: Resource, - resources: HashMap>, + resources: HashMap>>, + external: HashMap, middlewares: Vec>, } @@ -74,6 +70,7 @@ impl Application<()> { prefix: prefix.into(), default: Resource::default_not_found(), resources: HashMap::new(), + external: HashMap::new(), middlewares: Vec::new(), }) } @@ -94,6 +91,7 @@ impl Application where S: 'static { prefix: prefix.into(), default: Resource::default_not_found(), resources: HashMap::new(), + external: HashMap::new(), middlewares: Vec::new(), }) } @@ -130,19 +128,22 @@ impl Application where S: 'static { /// .finish(); /// } /// ``` - pub fn resource>(&mut self, path: P, f: F) -> &mut Self + pub fn resource(&mut self, path: &str, f: F) -> &mut Self where F: FnOnce(&mut Resource) + 'static { { let parts = self.parts.as_mut().expect("Use after finish"); // add resource - let path = path.into(); - if !parts.resources.contains_key(&path) { - check_pattern(&path); - parts.resources.insert(path.clone(), Resource::default()); + let mut resource = Resource::default(); + f(&mut resource); + + let pattern = Pattern::new(resource.get_name(), path); + if parts.resources.contains_key(&pattern) { + panic!("Resource {:?} is registered.", path); } - f(parts.resources.get_mut(&path).unwrap()); + + parts.resources.insert(pattern, Some(resource)); } self } @@ -158,6 +159,44 @@ impl Application where S: 'static { self } + /// Register external resource. + /// + /// External resources are useful for URL generation purposes only and + /// are never considered for matching at request time. + /// Call to `HttpRequest::url_for()` will work as expected. + /// + /// ```rust + /// # extern crate actix_web; + /// use actix_web::*; + /// + /// fn index(mut req: HttpRequest) -> Result { + /// let url = req.url_for("youtube", &["oHg5SJYRHA0"])?; + /// assert_eq!(url.as_str(), "https://youtube.com/watch/oHg5SJYRHA0"); + /// Ok(httpcodes::HTTPOk.into()) + /// } + /// + /// fn main() { + /// let app = Application::new("/") + /// .resource("/index.html", |r| r.f(index)) + /// .external_resource("youtube", "https://youtube.com/watch/{video_id}") + /// .finish(); + /// } + /// ``` + pub fn external_resource(&mut self, name: T, url: U) -> &mut Self + where T: AsRef, U: AsRef + { + { + let parts = self.parts.as_mut().expect("Use after finish"); + + if parts.external.contains_key(name.as_ref()) { + panic!("External resource {:?} is registered.", name.as_ref()); + } + parts.external.insert( + String::from(name.as_ref()), Pattern::new(name.as_ref(), url.as_ref())); + } + self + } + /// Register a middleware pub fn middleware(&mut self, mw: T) -> &mut Self where T: Middleware + 'static @@ -171,11 +210,17 @@ impl Application where S: 'static { pub fn finish(&mut self) -> HttpApplication { let parts = self.parts.take().expect("Use after finish"); let prefix = parts.prefix.trim().trim_right_matches('/'); + + let mut resources = parts.resources; + for (_, pattern) in parts.external { + resources.insert(pattern, None); + } + HttpApplication { state: Rc::new(parts.state), prefix: prefix.to_owned(), default: parts.default, - router: Router::new(prefix, parts.resources), + router: Router::new(prefix, resources), middlewares: Rc::new(parts.middlewares), } } diff --git a/src/fs.rs b/src/fs.rs index 2c000ffed..a5a015d1a 100644 --- a/src/fs.rs +++ b/src/fs.rs @@ -9,8 +9,8 @@ use std::path::{Path, PathBuf}; use std::ops::{Deref, DerefMut}; use mime_guess::get_mime_type; +use param::FromParam; use handler::{Handler, FromRequest}; -use recognizer::FromParam; use httprequest::HttpRequest; use httpresponse::HttpResponse; use httpcodes::HTTPOk; diff --git a/src/httprequest.rs b/src/httprequest.rs index 11acf1d7e..3747aabbb 100644 --- a/src/httprequest.rs +++ b/src/httprequest.rs @@ -11,8 +11,8 @@ use http::{header, Uri, Method, Version, HeaderMap, Extensions}; use Cookie; use info::ConnectionInfo; +use param::Params; use router::Router; -use recognizer::Params; use payload::Payload; use multipart::Multipart; use error::{ParseError, PayloadError, UrlGenerationError, @@ -26,7 +26,7 @@ struct HttpMessage { prefix: usize, headers: HeaderMap, extensions: Extensions, - params: Params, + params: Params<'static>, cookies: Vec>, cookies_loaded: bool, addr: Option, @@ -186,8 +186,12 @@ impl HttpRequest { Err(UrlGenerationError::RouterNotAvailable) } else { let path = self.router().unwrap().resource_path(name, elements)?; - let conn = self.load_connection_info(); - Ok(Url::parse(&format!("{}://{}{}", conn.scheme(), conn.host(), path))?) + if path.starts_with('/') { + let conn = self.load_connection_info(); + Ok(Url::parse(&format!("{}://{}{}", conn.scheme(), conn.host(), path))?) + } else { + Ok(Url::parse(&path)?) + } } } @@ -267,12 +271,14 @@ impl HttpRequest { /// Route supports glob patterns: * for a single wildcard segment and :param /// for matching storing that segment of the request url in the Params object. #[inline] - pub fn match_info(&self) -> &Params { &self.0.params } + pub fn match_info(&self) -> &Params { + unsafe{ mem::transmute(&self.0.params) } + } /// Set request Params. #[inline] - pub fn set_match_info(&mut self, params: Params) { - self.as_mut().params = params; + pub(crate) fn match_info_mut(&mut self) -> &mut Params { + unsafe{ mem::transmute(&mut self.as_mut().params) } } /// Checks if a connection should be kept alive. @@ -431,7 +437,7 @@ impl fmt::Debug for HttpRequest { if !self.query_string().is_empty() { let _ = write!(f, " query: ?{:?}\n", self.query_string()); } - if !self.0.params.is_empty() { + if !self.match_info().is_empty() { let _ = write!(f, " params: {:?}\n", self.0.params); } let _ = write!(f, " headers:\n"); @@ -483,6 +489,7 @@ mod tests { use super::*; use http::Uri; use std::str::FromStr; + use router::Pattern; use payload::Payload; use resource::Resource; @@ -543,7 +550,7 @@ mod tests { let mut resource = Resource::default(); resource.name("index"); let mut map = HashMap::new(); - map.insert("/user/{name}.{ext}".to_owned(), resource); + map.insert(Pattern::new("index", "/user/{name}.{ext}"), Some(resource)); let router = Router::new("", map); assert!(router.has_route("/user/test.html")); assert!(!router.has_route("/test/unknown")); @@ -560,4 +567,22 @@ mod tests { let url = req.url_for("index", &["test", "html"]); assert_eq!(url.ok().unwrap().as_str(), "http://www.rust-lang.org/user/test.html"); } + + #[test] + fn test_url_for_external() { + let req = HttpRequest::new( + Method::GET, Uri::from_str("/").unwrap(), + Version::HTTP_11, HeaderMap::new(), Payload::empty()); + + let mut resource = Resource::<()>::default(); + resource.name("index"); + let mut map = HashMap::new(); + map.insert(Pattern::new("youtube", "https://youtube.com/watch/{video_id}"), None); + let router = Router::new("", map); + assert!(!router.has_route("https://youtube.com/watch/unknown")); + + let mut req = req.with_state(Rc::new(()), router); + let url = req.url_for("youtube", &["oHg5SJYRHA0"]); + assert_eq!(url.ok().unwrap().as_str(), "https://youtube.com/watch/oHg5SJYRHA0"); + } } diff --git a/src/lib.rs b/src/lib.rs index 8eaec276f..f5633a240 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -31,6 +31,7 @@ extern crate serde_json; extern crate flate2; extern crate brotli2; extern crate percent_encoding; +extern crate smallvec; extern crate actix; extern crate h2 as http2; @@ -58,8 +59,9 @@ mod payload; mod info; mod route; mod router; +mod param; mod resource; -mod recognizer; +// mod recognizer; mod handler; mod pipeline; mod server; @@ -117,10 +119,11 @@ pub mod dev { // dev specific pub use info::ConnectionInfo; pub use handler::Handler; - pub use router::Router; + pub use router::{Router, Pattern}; pub use pipeline::Pipeline; pub use channel::{HttpChannel, HttpHandler, IntoHttpHandler}; - pub use recognizer::{FromParam, RouteRecognizer, Params, Pattern, PatternElement}; + // pub use recognizer::RouteRecognizer; + pub use param::{FromParam, Params}; pub use cookie::CookieBuilder; pub use http_range::HttpRange; diff --git a/src/param.rs b/src/param.rs new file mode 100644 index 000000000..3981a81bf --- /dev/null +++ b/src/param.rs @@ -0,0 +1,203 @@ +use std; +use std::ops::Index; +use std::path::PathBuf; +use std::str::FromStr; + +use failure::Fail; +use http::{StatusCode}; +use smallvec::SmallVec; + +use body::Body; +use httpresponse::HttpResponse; +use error::{ResponseError, UriSegmentError}; + + +/// A trait to abstract the idea of creating a new instance of a type from a path parameter. +pub trait FromParam: Sized { + /// The associated error which can be returned from parsing. + type Err: ResponseError; + + /// Parses a string `s` to return a value of this type. + fn from_param(s: &str) -> Result; +} + +/// Route match information +/// +/// If resource path contains variable patterns, `Params` stores this variables. +#[derive(Debug)] +pub struct Params<'a>(SmallVec<[(&'a str, &'a str); 4]>); + +impl<'a> Default for Params<'a> { + fn default() -> Params<'a> { + Params(SmallVec::new()) + } +} + +impl<'a> Params<'a> { + + pub(crate) fn add(&mut self, name: &'a str, value: &'a str) { + self.0.push((name, value)); + } + + /// Check if there are any matched patterns + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + /// Get matched parameter by name without type conversion + pub fn get(&self, key: &str) -> Option<&'a str> { + for item in &self.0 { + if key == item.0 { + return Some(item.1) + } + } + None + } + + /// Get matched `FromParam` compatible parameter by name. + /// + /// If keyed parameter is not available empty string is used as default value. + /// + /// ```rust + /// # extern crate actix_web; + /// # use actix_web::*; + /// fn index(req: HttpRequest) -> Result { + /// let ivalue: isize = req.match_info().query("val")?; + /// Ok(format!("isuze value: {:?}", ivalue)) + /// } + /// # fn main() {} + /// ``` + pub fn query(&self, key: &str) -> Result::Err> + { + if let Some(s) = self.get(key) { + T::from_param(s) + } else { + T::from_param("") + } + } +} + +impl<'a, 'b> Index<&'b str> for Params<'a> { + type Output = str; + + fn index(&self, name: &'b str) -> &str { + self.get(name).expect("Value for parameter is not available") + } +} + +/// Creates a `PathBuf` from a path parameter. The returned `PathBuf` is +/// percent-decoded. If a segment is equal to "..", the previous segment (if +/// any) is skipped. +/// +/// For security purposes, if a segment meets any of the following conditions, +/// an `Err` is returned indicating the condition met: +/// +/// * Decoded segment starts with any of: `.` (except `..`), `*` +/// * Decoded segment ends with any of: `:`, `>`, `<` +/// * Decoded segment contains any of: `/` +/// * On Windows, decoded segment contains any of: '\' +/// * Percent-encoding results in invalid UTF8. +/// +/// As a result of these conditions, a `PathBuf` parsed from request path parameter is +/// safe to interpolate within, or use as a suffix of, a path without additional +/// checks. +impl FromParam for PathBuf { + type Err = UriSegmentError; + + fn from_param(val: &str) -> Result { + let mut buf = PathBuf::new(); + for segment in val.split('/') { + if segment == ".." { + buf.pop(); + } else if segment.starts_with('.') { + return Err(UriSegmentError::BadStart('.')) + } else if segment.starts_with('*') { + return Err(UriSegmentError::BadStart('*')) + } else if segment.ends_with(':') { + return Err(UriSegmentError::BadEnd(':')) + } else if segment.ends_with('>') { + return Err(UriSegmentError::BadEnd('>')) + } else if segment.ends_with('<') { + return Err(UriSegmentError::BadEnd('<')) + } else if segment.is_empty() { + continue + } else if cfg!(windows) && segment.contains('\\') { + return Err(UriSegmentError::BadChar('\\')) + } else { + buf.push(segment) + } + } + + Ok(buf) + } +} + +#[derive(Fail, Debug)] +#[fail(display="Error")] +pub struct BadRequest(T); + +impl BadRequest { + pub fn cause(&self) -> &T { + &self.0 + } +} + +impl ResponseError for BadRequest + where T: Send + Sync + std::fmt::Debug +std::fmt::Display + 'static, +BadRequest: Fail +{ + fn error_response(&self) -> HttpResponse { + HttpResponse::new(StatusCode::BAD_REQUEST, Body::Empty) + } +} + +macro_rules! FROM_STR { + ($type:ty) => { + impl FromParam for $type { + type Err = BadRequest<<$type as FromStr>::Err>; + + fn from_param(val: &str) -> Result { + <$type as FromStr>::from_str(val).map_err(BadRequest) + } + } + } +} + +FROM_STR!(u8); +FROM_STR!(u16); +FROM_STR!(u32); +FROM_STR!(u64); +FROM_STR!(usize); +FROM_STR!(i8); +FROM_STR!(i16); +FROM_STR!(i32); +FROM_STR!(i64); +FROM_STR!(isize); +FROM_STR!(f32); +FROM_STR!(f64); +FROM_STR!(String); +FROM_STR!(std::net::IpAddr); +FROM_STR!(std::net::Ipv4Addr); +FROM_STR!(std::net::Ipv6Addr); +FROM_STR!(std::net::SocketAddr); +FROM_STR!(std::net::SocketAddrV4); +FROM_STR!(std::net::SocketAddrV6); + +#[cfg(test)] +mod tests { + use super::*; + use std::iter::FromIterator; + + #[test] + fn test_path_buf() { + assert_eq!(PathBuf::from_param("/test/.tt"), Err(UriSegmentError::BadStart('.'))); + assert_eq!(PathBuf::from_param("/test/*tt"), Err(UriSegmentError::BadStart('*'))); + assert_eq!(PathBuf::from_param("/test/tt:"), Err(UriSegmentError::BadEnd(':'))); + assert_eq!(PathBuf::from_param("/test/tt<"), Err(UriSegmentError::BadEnd('<'))); + assert_eq!(PathBuf::from_param("/test/tt>"), Err(UriSegmentError::BadEnd('>'))); + assert_eq!(PathBuf::from_param("/seg1/seg2/"), + Ok(PathBuf::from_iter(vec!["seg1", "seg2"]))); + assert_eq!(PathBuf::from_param("/seg1/../seg2/"), + Ok(PathBuf::from_iter(vec!["seg2"]))); + } +} diff --git a/src/recognizer.rs b/src/recognizer.rs index 5f2a9b00a..79be1cb1b 100644 --- a/src/recognizer.rs +++ b/src/recognizer.rs @@ -1,325 +1,50 @@ -use std; -use std::rc::Rc; -use std::path::PathBuf; -use std::ops::Index; -use std::str::FromStr; -use std::collections::HashMap; - -use failure::Fail; -use http::{StatusCode}; -use regex::{Regex, RegexSet, Captures}; - -use body::Body; -use httpresponse::HttpResponse; -use error::{ResponseError, UriSegmentError}; - -/// A trait to abstract the idea of creating a new instance of a type from a path parameter. -pub trait FromParam: Sized { - /// The associated error which can be returned from parsing. - type Err: ResponseError; - - /// Parses a string `s` to return a value of this type. - fn from_param(s: &str) -> Result; -} - -/// Route match information -/// -/// If resource path contains variable patterns, `Params` stores this variables. -#[derive(Debug)] -pub struct Params { - text: String, - matches: Vec>, - names: Rc>, -} - -impl Default for Params { - fn default() -> Params { - Params { - text: String::new(), - names: Rc::new(HashMap::new()), - matches: Vec::new(), - } - } -} - -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(), - } - } - - /// Check if there are any matched patterns - pub fn is_empty(&self) -> bool { - self.names.is_empty() - } - - fn by_idx(&self, index: usize) -> Option<&str> { - self.matches - .get(index + 1) - .and_then(|m| m.map(|(start, end)| &self.text[start..end])) - } - - /// Get matched parameter by name without type conversion - pub fn get(&self, key: &str) -> Option<&str> { - self.names.get(key).and_then(|&i| self.by_idx(i - 1)) - } - - /// Get matched `FromParam` compatible parameter by name. - /// - /// If keyed parameter is not available empty string is used as default value. - /// - /// ```rust - /// # extern crate actix_web; - /// # use actix_web::*; - /// fn index(req: HttpRequest) -> Result { - /// let ivalue: isize = req.match_info().query("val")?; - /// Ok(format!("isuze value: {:?}", ivalue)) - /// } - /// # fn main() {} - /// ``` - pub fn query(&self, key: &str) -> Result::Err> - { - if let Some(s) = self.get(key) { - T::from_param(s) - } else { - T::from_param("") - } - } -} - -impl<'a> Index<&'a str> for Params { - type Output = str; - - fn index(&self, name: &'a str) -> &str { - self.get(name).expect("Value for parameter is not available") - } -} - -/// Creates a `PathBuf` from a path parameter. The returned `PathBuf` is -/// percent-decoded. If a segment is equal to "..", the previous segment (if -/// any) is skipped. -/// -/// For security purposes, if a segment meets any of the following conditions, -/// an `Err` is returned indicating the condition met: -/// -/// * Decoded segment starts with any of: `.` (except `..`), `*` -/// * Decoded segment ends with any of: `:`, `>`, `<` -/// * Decoded segment contains any of: `/` -/// * On Windows, decoded segment contains any of: '\' -/// * Percent-encoding results in invalid UTF8. -/// -/// As a result of these conditions, a `PathBuf` parsed from request path parameter is -/// safe to interpolate within, or use as a suffix of, a path without additional -/// checks. -impl FromParam for PathBuf { - type Err = UriSegmentError; - - fn from_param(val: &str) -> Result { - let mut buf = PathBuf::new(); - for segment in val.split('/') { - if segment == ".." { - buf.pop(); - } else if segment.starts_with('.') { - return Err(UriSegmentError::BadStart('.')) - } else if segment.starts_with('*') { - return Err(UriSegmentError::BadStart('*')) - } else if segment.ends_with(':') { - return Err(UriSegmentError::BadEnd(':')) - } else if segment.ends_with('>') { - return Err(UriSegmentError::BadEnd('>')) - } else if segment.ends_with('<') { - return Err(UriSegmentError::BadEnd('<')) - } else if segment.is_empty() { - continue - } else if cfg!(windows) && segment.contains('\\') { - return Err(UriSegmentError::BadChar('\\')) - } else { - buf.push(segment) - } - } - - Ok(buf) - } -} - -#[derive(Fail, Debug)] -#[fail(display="Error")] -pub struct BadRequest(T); - -impl BadRequest { - pub fn cause(&self) -> &T { - &self.0 - } -} - -impl ResponseError for BadRequest - where T: Send + Sync + std::fmt::Debug +std::fmt::Display + 'static, - BadRequest: Fail -{ - fn error_response(&self) -> HttpResponse { - HttpResponse::new(StatusCode::BAD_REQUEST, Body::Empty) - } -} - -macro_rules! FROM_STR { - ($type:ty) => { - impl FromParam for $type { - type Err = BadRequest<<$type as FromStr>::Err>; - - fn from_param(val: &str) -> Result { - <$type as FromStr>::from_str(val).map_err(BadRequest) - } - } - } -} - -FROM_STR!(u8); -FROM_STR!(u16); -FROM_STR!(u32); -FROM_STR!(u64); -FROM_STR!(usize); -FROM_STR!(i8); -FROM_STR!(i16); -FROM_STR!(i32); -FROM_STR!(i64); -FROM_STR!(isize); -FROM_STR!(f32); -FROM_STR!(f64); -FROM_STR!(String); -FROM_STR!(std::net::IpAddr); -FROM_STR!(std::net::Ipv4Addr); -FROM_STR!(std::net::Ipv6Addr); -FROM_STR!(std::net::SocketAddr); -FROM_STR!(std::net::SocketAddrV4); -FROM_STR!(std::net::SocketAddrV6); +use regex::RegexSet; pub struct RouteRecognizer { re: RegexSet, - prefix: String, - routes: Vec<(Pattern, T)>, - patterns: HashMap, + routes: Vec, } impl RouteRecognizer { - pub fn new(prefix: P, routes: U) -> Self - where U: IntoIterator, T)>, - K: Into, - P: Into, + pub fn new(routes: U) -> Self + where U: IntoIterator, K: Into, { let mut paths = Vec::new(); - let mut handlers = Vec::new(); - let mut patterns = HashMap::new(); + let mut routes = Vec::new(); for item in routes { - let (pat, elements) = parse(&item.0.into()); - let pattern = Pattern::new(&pat, elements); - if let Some(ref name) = item.1 { - let _ = patterns.insert(name.clone(), pattern.clone()); - } - handlers.push((pattern, item.2)); - paths.push(pat); + let pattern = parse(&item.0.into()); + paths.push(pattern); + routes.push(item.1); }; let regset = RegexSet::new(&paths); RouteRecognizer { re: regset.unwrap(), - prefix: prefix.into(), - routes: handlers, - patterns: patterns, + routes: routes, } } - pub fn get_pattern(&self, name: &str) -> Option<&Pattern> { - self.patterns.get(name) - } - - /// Length of the prefix - pub fn prefix(&self) -> &str { - &self.prefix - } - - pub fn recognize(&self, path: &str) -> Option<(Option, &T)> { - let p = &path[self.prefix.len()..]; - if p.is_empty() { + pub fn recognize(&self, path: &str) -> Option<&T> { + if path.is_empty() { if let Some(idx) = self.re.matches("/").into_iter().next() { - let (ref pattern, ref route) = self.routes[idx]; - return Some((pattern.match_info(&path[self.prefix.len()..]), route)) + return Some(&self.routes[idx]) } - } else if let Some(idx) = self.re.matches(p).into_iter().next() { - let (ref pattern, ref route) = self.routes[idx]; - return Some((pattern.match_info(&path[self.prefix.len()..]), route)) + } else if let Some(idx) = self.re.matches(path).into_iter().next() { + return Some(&self.routes[idx]) } None } } -#[derive(Debug, Clone, PartialEq)] -pub enum PatternElement { - Str(String), - Var(String), -} - -#[derive(Clone)] -pub struct Pattern { - re: Regex, - names: Rc>, - elements: Vec, -} - -impl Pattern { - fn new(pattern: &str, elements: Vec) -> 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), - elements: elements, - } - } - - 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 fn elements(&self) -> &Vec { - &self.elements - } -} - -pub(crate) fn check_pattern(path: &str) { - if let Err(err) = Regex::new(&parse(path).0) { - panic!("Wrong path pattern: \"{}\" {}", path, err); - } -} - -fn parse(pattern: &str) -> (String, Vec) { +fn parse(pattern: &str) -> String { const DEFAULT_PATTERN: &str = "[^/]+"; let mut re = String::from("^/"); - let mut el = String::new(); 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); - let mut elems = Vec::new(); for (index, ch) in pattern.chars().enumerate() { // All routes must have a leading slash so its optional to have one @@ -330,7 +55,6 @@ fn parse(pattern: &str) -> (String, Vec) { if in_param { // In parameter segment: `{....}` if ch == '}' { - elems.push(PatternElement::Var(param_name.clone())); re.push_str(&format!(r"(?P<{}>{})", ¶m_name, ¶m_pattern)); param_name.clear(); @@ -352,16 +76,13 @@ fn parse(pattern: &str) -> (String, Vec) { } } else if ch == '{' { in_param = true; - elems.push(PatternElement::Str(el.clone())); - el.clear(); } else { re.push(ch); - el.push(ch); } } re.push('$'); - (re, elems) + re } #[cfg(test)] @@ -370,19 +91,6 @@ mod tests { use super::*; use std::iter::FromIterator; - #[test] - fn test_path_buf() { - assert_eq!(PathBuf::from_param("/test/.tt"), Err(UriSegmentError::BadStart('.'))); - assert_eq!(PathBuf::from_param("/test/*tt"), Err(UriSegmentError::BadStart('*'))); - assert_eq!(PathBuf::from_param("/test/tt:"), Err(UriSegmentError::BadEnd(':'))); - assert_eq!(PathBuf::from_param("/test/tt<"), Err(UriSegmentError::BadEnd('<'))); - assert_eq!(PathBuf::from_param("/test/tt>"), Err(UriSegmentError::BadEnd('>'))); - assert_eq!(PathBuf::from_param("/seg1/seg2/"), - Ok(PathBuf::from_iter(vec!["seg1", "seg2"]))); - assert_eq!(PathBuf::from_param("/seg1/../seg2/"), - Ok(PathBuf::from_iter(vec!["seg2"]))); - } - #[test] fn test_recognizer() { let routes = vec![ diff --git a/src/resource.rs b/src/resource.rs index 4652a32ff..f4677ad08 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -59,8 +59,8 @@ impl Resource { self.name = name.into(); } - pub(crate) fn get_name(&self) -> Option { - if self.name.is_empty() { None } else { Some(self.name.clone()) } + pub(crate) fn get_name(&self) -> &str { + &self.name } } diff --git a/src/router.rs b/src/router.rs index ec07d3c58..b90f79c56 100644 --- a/src/router.rs +++ b/src/router.rs @@ -1,40 +1,98 @@ +use std::mem; use std::rc::Rc; +use std::hash::{Hash, Hasher}; use std::collections::HashMap; +use regex::{Regex, RegexSet}; + use error::UrlGenerationError; use resource::Resource; -use recognizer::{Params, RouteRecognizer, PatternElement}; +use httprequest::HttpRequest; /// Interface for application router. -pub struct Router(Rc>>); +pub struct Router(Rc>); + +struct Inner { + prefix: String, + regset: RegexSet, + named: HashMap, + patterns: Vec, + resources: Vec>, +} impl Router { - pub(crate) fn new(prefix: &str, map: HashMap>) -> Router + /// Create new router + pub fn new(prefix: &str, map: HashMap>>) -> Router { let prefix = prefix.trim().trim_right_matches('/').to_owned(); + let mut named = HashMap::new(); + let mut patterns = Vec::new(); let mut resources = Vec::new(); - for (path, resource) in map { - resources.push((path, resource.get_name(), resource)) + let mut paths = Vec::new(); + + for (pattern, resource) in map { + if !pattern.name().is_empty() { + let name = pattern.name().into(); + named.insert(name, (pattern.clone(), resource.is_none())); + } + + if let Some(resource) = resource { + paths.push(pattern.pattern().to_owned()); + patterns.push(pattern); + resources.push(resource); + } } - Router(Rc::new(RouteRecognizer::new(prefix, resources))) + Router(Rc::new( + Inner{ prefix: prefix, + regset: RegexSet::new(&paths).unwrap(), + named: named, + patterns: patterns, + resources: resources })) } /// Router prefix #[inline] pub(crate) fn prefix(&self) -> &str { - self.0.prefix() + &self.0.prefix } /// Query for matched resource - pub fn query(&self, path: &str) -> Option<(Option, &Resource)> { - self.0.recognize(path) + pub fn recognize(&self, req: &mut HttpRequest) -> Option<&Resource> { + let mut idx = None; + { + let path = &req.path()[self.0.prefix.len()..]; + if path.is_empty() { + if let Some(i) = self.0.regset.matches("/").into_iter().next() { + idx = Some(i); + } + } else if let Some(i) = self.0.regset.matches(path).into_iter().next() { + idx = Some(i); + } + } + + if let Some(idx) = idx { + let path: &str = unsafe{ mem::transmute(&req.path()[self.0.prefix.len()..]) }; + req.set_prefix(self.prefix().len()); + self.0.patterns[idx].update_match_info(path, req); + return Some(&self.0.resources[idx]) + } else { + None + } } /// Check if application contains matching route. pub fn has_route(&self, path: &str) -> bool { - self.0.recognize(path).is_some() + let p = &path[self.0.prefix.len()..]; + if p.is_empty() { + if self.0.regset.matches("/").into_iter().next().is_some() { + return true + } + } else if self.0.regset.matches(p).into_iter().next().is_some() { + return true + } + false } /// Build named resource path. @@ -46,23 +104,12 @@ impl Router { where U: IntoIterator, I: AsRef, { - if let Some(pattern) = self.0.get_pattern(name) { - let mut path = String::from(self.prefix()); - path.push('/'); - let mut iter = elements.into_iter(); - for el in pattern.elements() { - match *el { - PatternElement::Str(ref s) => path.push_str(s), - PatternElement::Var(_) => { - if let Some(val) = iter.next() { - path.push_str(val.as_ref()) - } else { - return Err(UrlGenerationError::NotEnoughElements) - } - } - } + if let Some(pattern) = self.0.named.get(name) { + if pattern.1 { + pattern.0.path(None, elements) + } else { + pattern.0.path(Some(&self.0.prefix), elements) } - Ok(path) } else { Err(UrlGenerationError::ResourceNotFound) } @@ -74,3 +121,285 @@ impl Clone for Router { Router(Rc::clone(&self.0)) } } + +#[derive(Debug, Clone, PartialEq)] +enum PatternElement { + Str(String), + Var(String), +} + +#[derive(Clone)] +pub struct Pattern { + re: Regex, + name: String, + pattern: String, + names: Vec, + elements: Vec, +} + +impl Pattern { + /// Parse path pattern and create new `Pattern` instance. + /// + /// Panics if path pattern is wrong. + pub fn new(name: &str, path: &str) -> Self { + let (pattern, elements) = Pattern::parse(path); + + let re = match Regex::new(&pattern) { + Ok(re) => re, + Err(err) => panic!("Wrong path pattern: \"{}\" {}", path, err) + }; + let names = re.capture_names() + .filter_map(|name| name.map(|name| name.to_owned())) + .collect(); + + Pattern { + re: re, + name: name.into(), + pattern: pattern, + names: names, + elements: elements, + } + } + + /// Returns name of the pattern + pub fn name(&self) -> &str { + &self.name + } + + /// Returns path of the pattern + pub fn pattern(&self) -> &str { + &self.pattern + } + + /// Extract pattern parameters from the text + pub(crate) fn update_match_info(&self, text: &str, req: &mut HttpRequest) { + if !self.names.is_empty() { + if let Some(captures) = self.re.captures(text) { + let mut idx = 0; + for capture in captures.iter() { + if let Some(ref m) = capture { + if idx != 0 { + req.match_info_mut().add(&self.names[idx-1], m.as_str()); + } + idx += 1; + } + } + }; + } + } + + /// Build pattern path. + pub fn path(&self, prefix: Option<&str>, elements: U) + -> Result + where U: IntoIterator, + I: AsRef, + { + let mut iter = elements.into_iter(); + let mut path = if let Some(prefix) = prefix { + let mut path = String::from(prefix); + path.push('/'); + path + } else { + String::new() + }; + for el in &self.elements { + match *el { + PatternElement::Str(ref s) => path.push_str(s), + PatternElement::Var(_) => { + if let Some(val) = iter.next() { + path.push_str(val.as_ref()) + } else { + return Err(UrlGenerationError::NotEnoughElements) + } + } + } + } + Ok(path) + } + + fn parse(pattern: &str) -> (String, Vec) { + const DEFAULT_PATTERN: &str = "[^/]+"; + + let mut re = String::from("^/"); + let mut el = String::new(); + 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); + let mut elems = Vec::new(); + + 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 == '}' { + elems.push(PatternElement::Var(param_name.clone())); + 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; + elems.push(PatternElement::Str(el.clone())); + el.clear(); + } else { + re.push(ch); + el.push(ch); + } + } + + re.push('$'); + (re, elems) + } +} + +impl PartialEq for Pattern { + fn eq(&self, other: &Pattern) -> bool { + self.pattern == other.pattern + } +} + +impl Eq for Pattern {} + +impl Hash for Pattern { + fn hash(&self, state: &mut H) { + self.pattern.hash(state); + } +} + +#[cfg(test)] +mod tests { + use regex::Regex; + use super::*; + use http::{Uri, Version, Method}; + use http::header::HeaderMap; + use std::str::FromStr; + use payload::Payload; + + #[test] + fn test_recognizer() { + let mut routes = HashMap::new(); + routes.insert(Pattern::new("", "/name"), Some(Resource::default())); + routes.insert(Pattern::new("", "/name/{val}"), Some(Resource::default())); + routes.insert(Pattern::new("", "/name/{val}/index.html"), Some(Resource::default())); + routes.insert(Pattern::new("", "/v{val}/{val2}/index.html"), Some(Resource::default())); + routes.insert(Pattern::new("", "/v/{tail:.*}"), Some(Resource::default())); + routes.insert(Pattern::new("", "{test}/index.html"), Some(Resource::default())); + let rec = Router::new("", routes); + + let mut req = HttpRequest::new( + Method::GET, Uri::from_str("/name").unwrap(), + Version::HTTP_11, HeaderMap::new(), Payload::empty()); + assert!(rec.recognize(&mut req).is_some()); + assert!(req.match_info().is_empty()); + + let mut req = HttpRequest::new( + Method::GET, Uri::from_str("/name/value").unwrap(), + Version::HTTP_11, HeaderMap::new(), Payload::empty()); + assert!(rec.recognize(&mut req).is_some()); + assert_eq!(req.match_info().get("val").unwrap(), "value"); + assert_eq!(&req.match_info()["val"], "value"); + + let mut req = HttpRequest::new( + Method::GET, Uri::from_str("/name/value2/index.html").unwrap(), + Version::HTTP_11, HeaderMap::new(), Payload::empty()); + assert!(rec.recognize(&mut req).is_some()); + assert_eq!(req.match_info().get("val").unwrap(), "value2"); + + let mut req = HttpRequest::new( + Method::GET, Uri::from_str("/vtest/ttt/index.html").unwrap(), + Version::HTTP_11, HeaderMap::new(), Payload::empty()); + assert!(rec.recognize(&mut req).is_some()); + assert_eq!(req.match_info().get("val").unwrap(), "test"); + assert_eq!(req.match_info().get("val2").unwrap(), "ttt"); + + let mut req = HttpRequest::new( + Method::GET, Uri::from_str("/v/blah-blah/index.html").unwrap(), + Version::HTTP_11, HeaderMap::new(), Payload::empty()); + assert!(rec.recognize(&mut req).is_some()); + assert_eq!(req.match_info().get("tail").unwrap(), "blah-blah/index.html"); + + let mut req = HttpRequest::new( + Method::GET, Uri::from_str("/bbb/index.html").unwrap(), + Version::HTTP_11, HeaderMap::new(), Payload::empty()); + assert!(rec.recognize(&mut req).is_some()); + assert_eq!(req.match_info().get("test").unwrap(), "bbb"); + } + + fn assert_parse(pattern: &str, expected_re: &str) -> Regex { + let (re_str, _) = Pattern::parse(pattern); + assert_eq!(&*re_str, expected_re); + Regex::new(&re_str).unwrap() + } + + #[test] + fn test_parse_static() { + let re = assert_parse("/", r"^/$"); + assert!(re.is_match("/")); + assert!(!re.is_match("/a")); + + let re = assert_parse("/name", r"^/name$"); + assert!(re.is_match("/name")); + assert!(!re.is_match("/name1")); + assert!(!re.is_match("/name/")); + assert!(!re.is_match("/name~")); + + let re = assert_parse("/name/", r"^/name/$"); + assert!(re.is_match("/name/")); + assert!(!re.is_match("/name")); + assert!(!re.is_match("/name/gs")); + + let re = assert_parse("/user/profile", r"^/user/profile$"); + assert!(re.is_match("/user/profile")); + assert!(!re.is_match("/user/profile/profile")); + } + + #[test] + fn test_parse_param() { + let re = assert_parse("/user/{id}", r"^/user/(?P[^/]+)$"); + assert!(re.is_match("/user/profile")); + assert!(re.is_match("/user/2345")); + assert!(!re.is_match("/user/2345/")); + assert!(!re.is_match("/user/2345/sdg")); + + let captures = re.captures("/user/profile").unwrap(); + assert_eq!(captures.get(1).unwrap().as_str(), "profile"); + assert_eq!(captures.name("id").unwrap().as_str(), "profile"); + + let captures = re.captures("/user/1245125").unwrap(); + assert_eq!(captures.get(1).unwrap().as_str(), "1245125"); + assert_eq!(captures.name("id").unwrap().as_str(), "1245125"); + + let re = assert_parse( + "/v{version}/resource/{id}", + r"^/v(?P[^/]+)/resource/(?P[^/]+)$", + ); + assert!(re.is_match("/v1/resource/320120")); + assert!(!re.is_match("/v/resource/1")); + assert!(!re.is_match("/resource")); + + let captures = re.captures("/v151/resource/adahg32").unwrap(); + assert_eq!(captures.get(1).unwrap().as_str(), "151"); + assert_eq!(captures.name("version").unwrap().as_str(), "151"); + assert_eq!(captures.name("id").unwrap().as_str(), "adahg32"); + } +} diff --git a/tests/test_httprequest.rs b/tests/test_httprequest.rs index 263ba094d..f3519dbe6 100644 --- a/tests/test_httprequest.rs +++ b/tests/test_httprequest.rs @@ -4,6 +4,7 @@ extern crate time; use std::str; use std::str::FromStr; +use std::collections::HashMap; use actix_web::*; use actix_web::dev::*; use http::{header, Method, Version, HeaderMap, Uri}; @@ -92,11 +93,13 @@ fn test_request_match_info() { let mut req = HttpRequest::new(Method::GET, Uri::from_str("/value/?id=test").unwrap(), Version::HTTP_11, HeaderMap::new(), Payload::empty()); - let rec = RouteRecognizer::new("", vec![("/{key}/".to_owned(), None, 1)]); - let (params, _) = rec.recognize(req.path()).unwrap(); - let params = params.unwrap(); + let mut resource = Resource::default(); + resource.name("index"); + let mut map = HashMap::new(); + map.insert(Pattern::new("index", "/{key}/"), Some(resource)); + let router = Router::new("", map); + assert!(router.recognize(&mut req).is_some()); - req.set_match_info(params); assert_eq!(req.match_info().get("key"), Some("value")); }