From 0dd27bd22437ff9a12129a7be36e2efb7b930fb1 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Wed, 6 Dec 2017 16:26:27 -0800 Subject: [PATCH] added HttpRequest::url_for --- src/application.rs | 62 ++++-------------------------- src/dev.rs | 1 + src/error.rs | 16 +++++++- src/httprequest.rs | 79 +++++++++++++++++++++++++++++++++------ src/lib.rs | 1 + src/recognizer.rs | 28 +++++--------- src/resource.rs | 5 ++- src/router.rs | 73 ++++++++++++++++++++++++++++++++++++ tests/test_httprequest.rs | 2 +- 9 files changed, 179 insertions(+), 88 deletions(-) create mode 100644 src/router.rs diff --git a/src/application.rs b/src/application.rs index f0833e416..ff3374619 100644 --- a/src/application.rs +++ b/src/application.rs @@ -1,59 +1,15 @@ use std::rc::Rc; use std::collections::HashMap; -use error::UriGenerationError; use handler::{Reply, RouteHandler}; +use router::Router; use resource::Resource; -use recognizer::{RouteRecognizer, check_pattern, PatternElement}; +use recognizer::check_pattern; use httprequest::HttpRequest; use channel::{HttpHandler, IntoHttpHandler}; use pipeline::Pipeline; use middlewares::Middleware; -pub struct Router(Rc>>); - -impl Router { - pub fn new(prefix: String, map: HashMap>) -> Router - { - let mut resources = Vec::new(); - for (path, resource) in map { - resources.push((path, resource.get_name(), resource)) - } - - Router(Rc::new(RouteRecognizer::new(prefix, resources))) - } - - pub fn has_route(&self, path: &str) -> bool { - self.0.recognize(path).is_some() - } - - pub fn resource_path<'a, U>(&self, prefix: &str, name: &str, elements: U) - -> Result - where U: IntoIterator - { - if let Some(pattern) = self.0.get_pattern(name) { - let mut iter = elements.into_iter(); - let mut vec = vec![prefix]; - for el in pattern.elements() { - match *el { - PatternElement::Str(ref s) => vec.push(s), - PatternElement::Var(_) => { - if let Some(val) = iter.next() { - vec.push(val) - } else { - return Err(UriGenerationError::NotEnoughElements) - } - } - } - } - let s = vec.join("/").to_owned(); - Ok(s) - } else { - Err(UriGenerationError::ResourceNotFound) - } - } -} - /// Application pub struct HttpApplication { state: Rc, @@ -66,12 +22,12 @@ pub struct HttpApplication { impl HttpApplication { fn run(&self, req: HttpRequest) -> Reply { - let mut req = req.with_state(Rc::clone(&self.state)); + let mut req = req.with_state(Rc::clone(&self.state), self.router.clone()); - if let Some((params, h)) = self.router.0.recognize(req.path()) { + 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.0.prefix()); + req.set_prefix(self.router.prefix().len()); } h.handle(req) } else { @@ -214,14 +170,10 @@ impl Application where S: 'static { /// Finish application configuration and create HttpHandler object pub fn finish(&mut self) -> HttpApplication { let parts = self.parts.take().expect("Use after finish"); - let prefix = if parts.prefix.ends_with('/') { - parts.prefix - } else { - parts.prefix + "/" - }; + let prefix = parts.prefix.trim().trim_right_matches('/'); HttpApplication { state: Rc::new(parts.state), - prefix: prefix.clone(), + prefix: prefix.to_owned(), default: parts.default, router: Router::new(prefix, parts.resources), middlewares: Rc::new(parts.middlewares), diff --git a/src/dev.rs b/src/dev.rs index fefcfc71f..833fecf03 100644 --- a/src/dev.rs +++ b/src/dev.rs @@ -11,6 +11,7 @@ // dev specific pub use info::ConnectionInfo; pub use handler::Handler; +pub use router::Router; pub use pipeline::Pipeline; pub use channel::{HttpChannel, HttpHandler, IntoHttpHandler}; pub use recognizer::{FromParam, RouteRecognizer, Pattern, PatternElement}; diff --git a/src/error.rs b/src/error.rs index 61aa07df8..d2434b6db 100644 --- a/src/error.rs +++ b/src/error.rs @@ -15,6 +15,7 @@ use http::{header, StatusCode, Error as HttpError}; use http::uri::InvalidUriBytes; use http_range::HttpRangeParseError; use serde_json::error::Error as JsonError; +use url::ParseError as UrlParseError; // re-exports pub use cookie::{ParseError as CookieParseError}; @@ -405,11 +406,24 @@ impl ResponseError for UriSegmentError { /// Errors which can occur when attempting to generate resource uri. #[derive(Fail, Debug, PartialEq)] -pub enum UriGenerationError { +pub enum UrlGenerationError { #[fail(display="Resource not found")] ResourceNotFound, #[fail(display="Not all path pattern covered")] NotEnoughElements, + #[fail(display="Router is not available")] + RouterNotAvailable, + #[fail(display="{}", _0)] + ParseError(#[cause] UrlParseError), +} + +/// `InternalServerError` for `UrlGeneratorError` +impl ResponseError for UrlGenerationError {} + +impl From for UrlGenerationError { + fn from(err: UrlParseError) -> Self { + UrlGenerationError::ParseError(err) + } } #[cfg(test)] diff --git a/src/httprequest.rs b/src/httprequest.rs index 3c30037eb..07ae8b4e1 100644 --- a/src/httprequest.rs +++ b/src/httprequest.rs @@ -5,15 +5,16 @@ use std::net::SocketAddr; use std::collections::HashMap; use bytes::BytesMut; use futures::{Async, Future, Stream, Poll}; -use url::form_urlencoded; +use url::{Url, form_urlencoded}; use http::{header, Uri, Method, Version, HeaderMap, Extensions}; use {Cookie, HttpRange}; use info::ConnectionInfo; +use router::Router; use recognizer::Params; use payload::Payload; use multipart::Multipart; -use error::{ParseError, PayloadError, +use error::{ParseError, PayloadError, UrlGenerationError, MultipartError, CookieParseError, HttpRangeError, UrlencodedError}; @@ -30,6 +31,7 @@ struct HttpMessage { addr: Option, payload: Payload, info: Option>, + } impl Default for HttpMessage { @@ -53,7 +55,7 @@ impl Default for HttpMessage { } /// An HTTP Request -pub struct HttpRequest(Rc, Rc); +pub struct HttpRequest(Rc, Rc, Option>); impl HttpRequest<()> { /// Construct a new Request. @@ -76,13 +78,14 @@ impl HttpRequest<()> { extensions: Extensions::new(), info: None, }), - Rc::new(()) + Rc::new(()), + None, ) } /// Construct new http request with state. - pub fn with_state(self, state: Rc) -> HttpRequest { - HttpRequest(self.0, state) + pub fn with_state(self, state: Rc, router: Router) -> HttpRequest { + HttpRequest(self.0, state, Some(router)) } } @@ -90,10 +93,11 @@ impl HttpRequest { /// Construct new http request without state. pub fn clone_without_state(&self) -> HttpRequest { - HttpRequest(Rc::clone(&self.0), Rc::new(())) + HttpRequest(Rc::clone(&self.0), Rc::new(()), None) } /// get mutable reference for inner message + #[inline] fn as_mut(&mut self) -> &mut HttpMessage { let r: &HttpMessage = self.0.as_ref(); #[allow(mutable_transmutes)] @@ -101,6 +105,7 @@ impl HttpRequest { } /// Shared application state + #[inline] pub fn state(&self) -> &S { &self.1 } @@ -111,6 +116,7 @@ impl HttpRequest { &mut self.as_mut().extensions } + #[inline] pub(crate) fn set_prefix(&mut self, idx: usize) { self.as_mut().prefix = idx; } @@ -162,7 +168,6 @@ impl HttpRequest { } /// Load *ConnectionInfo* for currect request. - #[inline] pub fn load_connection_info(&mut self) -> &ConnectionInfo { if self.0.info.is_none() { let info: ConnectionInfo<'static> = unsafe{ @@ -172,17 +177,35 @@ impl HttpRequest { self.0.info.as_ref().unwrap() } + pub fn url_for(&mut self, name: &str, elements: U) -> Result + where U: IntoIterator, + I: AsRef, + { + if self.router().is_none() { + 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))?) + } + } + + #[inline] + pub fn router(&self) -> Option<&Router> { + self.2.as_ref() + } + #[inline] pub fn peer_addr(&self) -> Option<&SocketAddr> { self.0.addr.as_ref() } + #[inline] pub(crate) fn set_peer_addr(&mut self, addr: Option) { self.as_mut().addr = addr } /// Return a new iterator that yields pairs of `Cow` for query parameters - #[inline] pub fn query(&self) -> HashMap { let mut q: HashMap = HashMap::new(); if let Some(query) = self.0.uri.query().as_ref() { @@ -206,6 +229,7 @@ impl HttpRequest { } /// Return request cookies. + #[inline] pub fn cookies(&self) -> &Vec> { &self.0.cookies } @@ -245,6 +269,7 @@ impl HttpRequest { pub fn match_info(&self) -> &Params { &self.0.params } /// Set request Params. + #[inline] pub fn set_match_info(&mut self, params: Params) { self.as_mut().params = params; } @@ -324,6 +349,7 @@ impl HttpRequest { } /// Return payload + #[inline] pub fn take_payload(&mut self) -> Payload { mem::replace(&mut self.as_mut().payload, Payload::empty()) } @@ -387,13 +413,13 @@ impl Default for HttpRequest<()> { /// Construct default request fn default() -> HttpRequest { - HttpRequest(Rc::new(HttpMessage::default()), Rc::new(())) + HttpRequest(Rc::new(HttpMessage::default()), Rc::new(()), None) } } impl Clone for HttpRequest { fn clone(&self) -> HttpRequest { - HttpRequest(Rc::clone(&self.0), Rc::clone(&self.1)) + HttpRequest(Rc::clone(&self.0), Rc::clone(&self.1), None) } } @@ -454,9 +480,10 @@ impl Future for UrlEncoded { #[cfg(test)] mod tests { use super::*; + use http::Uri; use std::str::FromStr; use payload::Payload; - use http::Uri; + use resource::Resource; #[test] fn test_urlencoded_error() { @@ -502,4 +529,32 @@ mod tests { assert_eq!(req.urlencoded().err().unwrap(), UrlencodedError::ContentType); } + + #[test] + fn test_url_for() { + let mut headers = HeaderMap::new(); + headers.insert(header::HOST, + header::HeaderValue::from_static("www.rust-lang.org")); + let mut req = HttpRequest::new( + Method::GET, Uri::from_str("/").unwrap(), + Version::HTTP_11, headers, Payload::empty()); + + let mut resource = Resource::default(); + resource.name("index"); + let mut map = HashMap::new(); + map.insert("/user/{name}.{ext}".to_owned(), resource); + let router = Router::new("", map); + + assert_eq!(req.url_for("unknown", &["test"]), + Err(UrlGenerationError::RouterNotAvailable)); + + let mut req = req.with_state(Rc::new(()), router); + + assert_eq!(req.url_for("unknown", &["test"]), + Err(UrlGenerationError::ResourceNotFound)); + assert_eq!(req.url_for("index", &["test"]), + Err(UrlGenerationError::NotEnoughElements)); + let url = req.url_for("index", &["test", "html"]); + assert_eq!(url.ok().unwrap().as_str(), "http://www.rust-lang.org/user/test.html"); + } } diff --git a/src/lib.rs b/src/lib.rs index 8c9218775..f490ec44b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -57,6 +57,7 @@ mod httpresponse; mod payload; mod info; mod route; +mod router; mod resource; mod recognizer; mod handler; diff --git a/src/recognizer.rs b/src/recognizer.rs index 459086550..5f2a9b00a 100644 --- a/src/recognizer.rs +++ b/src/recognizer.rs @@ -204,16 +204,17 @@ FROM_STR!(std::net::SocketAddrV6); pub struct RouteRecognizer { re: RegexSet, - prefix: usize, + prefix: String, routes: Vec<(Pattern, T)>, patterns: HashMap, } impl RouteRecognizer { - pub fn new, U, K>(prefix: P, routes: U) -> Self + pub fn new(prefix: P, routes: U) -> Self where U: IntoIterator, T)>, K: Into, + P: Into, { let mut paths = Vec::new(); let mut handlers = Vec::new(); @@ -231,7 +232,7 @@ impl RouteRecognizer { RouteRecognizer { re: regset.unwrap(), - prefix: prefix.into().len() - 1, + prefix: prefix.into(), routes: handlers, patterns: patterns, } @@ -242,29 +243,20 @@ impl RouteRecognizer { } /// Length of the prefix - pub fn prefix(&self) -> usize { - self.prefix - } - - pub fn set_prefix>(&mut self, prefix: P) { - let p = prefix.into(); - if p.ends_with('/') { - self.prefix = p.len() - 1; - } else { - self.prefix = p.len(); - } + pub fn prefix(&self) -> &str { + &self.prefix } pub fn recognize(&self, path: &str) -> Option<(Option, &T)> { - let p = &path[self.prefix..]; + let p = &path[self.prefix.len()..]; if p.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..]), route)) + return Some((pattern.match_info(&path[self.prefix.len()..]), route)) } } 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..]), route)) + return Some((pattern.match_info(&path[self.prefix.len()..]), route)) } None } @@ -400,7 +392,7 @@ mod tests { ("/v{val}/{val2}/index.html", None, 4), ("/v/{tail:.*}", None, 5), ]; - let rec = RouteRecognizer::new("/", routes); + let rec = RouteRecognizer::new("", routes); let (params, val) = rec.recognize("/name").unwrap(); assert_eq!(*val, 1); diff --git a/src/resource.rs b/src/resource.rs index 3dc50c959..4652a32ff 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -44,7 +44,7 @@ impl Default for Resource { } } -impl Resource where S: 'static { +impl Resource { pub(crate) fn default_not_found() -> Self { Resource { @@ -62,6 +62,9 @@ impl Resource where S: 'static { pub(crate) fn get_name(&self) -> Option { if self.name.is_empty() { None } else { Some(self.name.clone()) } } +} + +impl Resource { /// Register a new route and return mutable reference to *Route* object. /// *Route* is used for route configuration, i.e. adding predicates, setting up handler. diff --git a/src/router.rs b/src/router.rs new file mode 100644 index 000000000..2009d9510 --- /dev/null +++ b/src/router.rs @@ -0,0 +1,73 @@ +use std::rc::Rc; +use std::collections::HashMap; + +use error::UrlGenerationError; +use resource::Resource; +use recognizer::{Params, RouteRecognizer, PatternElement}; + + +/// Interface for application router. +pub struct Router(Rc>>); + +impl Router { + pub(crate) fn new(prefix: &str, map: HashMap>) -> Router + { + let prefix = prefix.trim().trim_right_matches('/').to_owned(); + let mut resources = Vec::new(); + for (path, resource) in map { + resources.push((path, resource.get_name(), resource)) + } + + Router(Rc::new(RouteRecognizer::new(prefix, resources))) + } + + /// Router prefix + #[inline] + pub(crate) fn prefix(&self) -> &str { + self.0.prefix() + } + + /// Query for matched resource + pub fn query(&self, path: &str) -> Option<(Option, &Resource)> { + self.0.recognize(path) + } + + /// Check if application contains matching route. + pub fn has_route(&self, path: &str) -> bool { + self.0.recognize(path).is_some() + } + + /// Build named resource path + pub fn resource_path(&self, name: &str, elements: U) + -> Result + 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) + } + } + } + } + Ok(path) + } else { + Err(UrlGenerationError::ResourceNotFound) + } + } +} + +impl Clone for Router { + fn clone(&self) -> Router { + Router(Rc::clone(&self.0)) + } +} diff --git a/tests/test_httprequest.rs b/tests/test_httprequest.rs index 794864cd0..263ba094d 100644 --- a/tests/test_httprequest.rs +++ b/tests/test_httprequest.rs @@ -92,7 +92,7 @@ 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("/".to_owned(), vec![("/{key}/".to_owned(), None, 1)]); + let rec = RouteRecognizer::new("", vec![("/{key}/".to_owned(), None, 1)]); let (params, _) = rec.recognize(req.path()).unwrap(); let params = params.unwrap();