diff --git a/Cargo.toml b/Cargo.toml index f9e2266e..5e2027c5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -72,11 +72,13 @@ actix-utils = { git = "https://github.com/actix/actix-net.git" } actix-http = { git = "https://github.com/actix/actix-http.git" } actix-router = { git = "https://github.com/actix/actix-net.git" } actix-server = { git = "https://github.com/actix/actix-net.git" } +#actix-router = { path="../actix-net/router" } bytes = "0.4" derive_more = "0.14" encoding = "0.2" futures = "0.1" +hashbrown = "0.1.8" log = "0.4" lazy_static = "1.2" mime = "0.3" @@ -89,6 +91,7 @@ serde_json = "1.0" serde_urlencoded = "^0.5.3" threadpool = "1.7" time = "0.1" +url = { version="1.7", features=["query_encoding"] } # middlewares # actix-session = { path="session", optional = true } diff --git a/actix-files/src/lib.rs b/actix-files/src/lib.rs index 5dd98dcc..17efdd81 100644 --- a/actix-files/src/lib.rs +++ b/actix-files/src/lib.rs @@ -1090,4 +1090,35 @@ mod tests { // assert_eq!(response.status(), StatusCode::OK); // } + #[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/app.rs b/src/app.rs index c1c019a3..b4f6e535 100644 --- a/src/app.rs +++ b/src/app.rs @@ -16,6 +16,7 @@ use futures::{Async, Future, IntoFuture, Poll}; use crate::config::AppConfig; use crate::guard::Guard; use crate::resource::Resource; +use crate::rmap::ResourceMap; use crate::route::Route; use crate::service::{ HttpServiceFactory, ServiceFactory, ServiceFactoryWrapper, ServiceRequest, @@ -449,19 +450,29 @@ where .into_iter() .for_each(|mut srv| srv.register(&mut config)); - // set factory + let mut rmap = ResourceMap::new(ResourceDef::new("")); + + // complete pipeline creation *self.factory_ref.borrow_mut() = Some(AppRoutingFactory { default, services: Rc::new( config .into_services() .into_iter() - .map(|(rdef, srv, guards)| (rdef, srv, RefCell::new(guards))) + .map(|(mut rdef, srv, guards, nested)| { + rmap.add(&mut rdef, nested); + (rdef, srv, RefCell::new(guards)) + }) .collect(), ), }); + // complete ResourceMap tree creation + let rmap = Rc::new(rmap); + rmap.finish(rmap.clone()); + AppInit { + rmap, chain: self.chain, state: self.state, extensions: Rc::new(RefCell::new(Rc::new(self.extensions))), @@ -561,8 +572,7 @@ impl

Future for AppRoutingFactoryResponse

{ .fold(Router::build(), |mut router, item| { match item { CreateAppRoutingItem::Service(path, guards, service) => { - router.rdef(path, service); - router.set_user_data(guards); + router.rdef(path, service).2 = guards; } CreateAppRoutingItem::Future(_, _, _) => unreachable!(), } @@ -683,6 +693,7 @@ where C: NewService>, { chain: C, + rmap: Rc, state: Vec>, extensions: Rc>>, } @@ -702,6 +713,7 @@ where chain: self.chain.new_service(&()), state: self.state.iter().map(|s| s.construct()).collect(), extensions: self.extensions.clone(), + rmap: self.rmap.clone(), } } } @@ -712,6 +724,7 @@ where C: NewService, InitError = ()>, { chain: C::Future, + rmap: Rc, state: Vec>, extensions: Rc>>, } @@ -744,6 +757,7 @@ where Ok(Async::Ready(AppInitService { chain, + rmap: self.rmap.clone(), extensions: self.extensions.borrow().clone(), })) } @@ -755,6 +769,7 @@ where C: Service>, { chain: C, + rmap: Rc, extensions: Rc, } @@ -774,6 +789,7 @@ where let req = ServiceRequest::new( Path::new(Url::new(req.uri().clone())), req, + self.rmap.clone(), self.extensions.clone(), ); self.chain.call(req) diff --git a/src/config.rs b/src/config.rs index 483b0a50..4afd213c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -5,6 +5,7 @@ use actix_router::ResourceDef; use actix_service::{boxed, IntoNewService, NewService}; use crate::guard::Guard; +use crate::rmap::ResourceMap; use crate::service::{ServiceRequest, ServiceResponse}; type Guards = Vec>; @@ -18,7 +19,12 @@ pub struct AppConfig

{ host: String, root: bool, default: Rc>, - services: Vec<(ResourceDef, HttpNewService

, Option)>, + services: Vec<( + ResourceDef, + HttpNewService

, + Option, + Option>, + )>, } impl AppConfig

{ @@ -46,7 +52,12 @@ impl AppConfig

{ pub(crate) fn into_services( self, - ) -> Vec<(ResourceDef, HttpNewService

, Option)> { + ) -> Vec<( + ResourceDef, + HttpNewService

, + Option, + Option>, + )> { self.services } @@ -85,6 +96,7 @@ impl AppConfig

{ rdef: ResourceDef, guards: Option>>, service: F, + nested: Option>, ) where F: IntoNewService>, S: NewService< @@ -98,6 +110,7 @@ impl AppConfig

{ rdef, boxed::new_service(service.into_new_service()), guards, + nested, )); } } diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 00000000..d1c0d3ca --- /dev/null +++ b/src/error.rs @@ -0,0 +1,20 @@ +pub use actix_http::error::*; +use derive_more::{Display, From}; +use url::ParseError as UrlParseError; + +/// Errors which can occur when attempting to generate resource uri. +#[derive(Debug, PartialEq, Display, From)] +pub enum UrlGenerationError { + /// Resource not found + #[display(fmt = "Resource not found")] + ResourceNotFound, + /// Not all path pattern covered + #[display(fmt = "Not all path pattern covered")] + NotEnoughElements, + /// URL parse error + #[display(fmt = "{}", _0)] + ParseError(UrlParseError), +} + +/// `InternalServerError` for `UrlGeneratorError` +impl ResponseError for UrlGenerationError {} diff --git a/src/lib.rs b/src/lib.rs index a61387e8..94bf1ba7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,11 +6,13 @@ mod handler; // mod info; pub mod blocking; mod config; +pub mod error; pub mod guard; pub mod middleware; mod request; mod resource; mod responder; +mod rmap; mod route; mod scope; mod server; @@ -27,7 +29,7 @@ pub use actix_web_codegen::*; // re-export for convenience pub use actix_http::Response as HttpResponse; -pub use actix_http::{error, http, Error, HttpMessage, ResponseError, Result}; +pub use actix_http::{http, Error, HttpMessage, ResponseError, Result}; pub use crate::app::App; pub use crate::extract::FromRequest; diff --git a/src/request.rs b/src/request.rs index 1c86cac3..6655f1ba 100644 --- a/src/request.rs +++ b/src/request.rs @@ -7,7 +7,9 @@ use actix_http::http::{HeaderMap, Method, Uri, Version}; use actix_http::{Error, Extensions, HttpMessage, Message, Payload, RequestHead}; use actix_router::{Path, Url}; +use crate::error::UrlGenerationError; use crate::extract::FromRequest; +use crate::rmap::ResourceMap; use crate::service::ServiceFromRequest; #[derive(Clone)] @@ -15,6 +17,7 @@ use crate::service::ServiceFromRequest; pub struct HttpRequest { pub(crate) head: Message, pub(crate) path: Path, + rmap: Rc, extensions: Rc, } @@ -23,11 +26,13 @@ impl HttpRequest { pub(crate) fn new( head: Message, path: Path, + rmap: Rc, extensions: Rc, ) -> HttpRequest { HttpRequest { head, path, + rmap, extensions, } } @@ -93,6 +98,47 @@ impl HttpRequest { &self.extensions } + /// Generate url for named resource + /// + /// ```rust + /// # extern crate actix_web; + /// # use actix_web::{App, HttpRequest, HttpResponse, http}; + /// # + /// fn index(req: HttpRequest) -> HttpResponse { + /// let url = req.url_for("foo", &["1", "2", "3"]); // <- generate url for "foo" resource + /// HttpResponse::Ok().into() + /// } + /// + /// fn main() { + /// let app = App::new() + /// .resource("/test/{one}/{two}/{three}", |r| { + /// r.name("foo"); // <- set resource name, then it could be used in `url_for` + /// r.method(http::Method::GET).f(|_| HttpResponse::Ok()); + /// }) + /// .finish(); + /// } + /// ``` + pub fn url_for( + &self, + name: &str, + elements: U, + ) -> Result + where + U: IntoIterator, + I: AsRef, + { + self.rmap.url_for(&self, name, elements) + } + + /// Generate url for named resource + /// + /// This method is similar to `HttpRequest::url_for()` but it can be used + /// for urls that do not contain variable parts. + pub fn url_for_static(&self, name: &str) -> Result { + const NO_PARAMS: [&str; 0] = []; + self.url_for(name, &NO_PARAMS) + } + // /// Get *ConnectionInfo* for the correct request. // #[inline] // pub fn connection_info(&self) -> Ref { diff --git a/src/resource.rs b/src/resource.rs index 13afff70..a1177ca0 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -288,7 +288,7 @@ where } else { ResourceDef::new(&self.rdef) }; - config.register_service(rdef, guards, self) + config.register_service(rdef, guards, self, None) } } diff --git a/src/rmap.rs b/src/rmap.rs new file mode 100644 index 00000000..4922084b --- /dev/null +++ b/src/rmap.rs @@ -0,0 +1,188 @@ +use std::cell::RefCell; +use std::rc::Rc; + +use actix_router::ResourceDef; +use hashbrown::HashMap; +use url::Url; + +use crate::error::UrlGenerationError; +use crate::request::HttpRequest; + +#[derive(Clone, Debug)] +pub struct ResourceMap { + root: ResourceDef, + parent: RefCell>>, + named: HashMap, + patterns: Vec<(ResourceDef, Option>)>, +} + +impl ResourceMap { + pub fn new(root: ResourceDef) -> Self { + ResourceMap { + root, + parent: RefCell::new(None), + named: HashMap::new(), + patterns: Vec::new(), + } + } + + pub fn add(&mut self, pattern: &mut ResourceDef, nested: Option>) { + pattern.set_id(self.patterns.len() as u16); + self.patterns.push((pattern.clone(), nested)); + if !pattern.name().is_empty() { + self.named + .insert(pattern.name().to_string(), pattern.clone()); + } + } + + pub(crate) fn finish(&self, current: Rc) { + for (_, nested) in &self.patterns { + if let Some(ref nested) = nested { + *nested.parent.borrow_mut() = Some(current.clone()) + } + } + } +} + +impl ResourceMap { + /// Generate url for named resource + /// + /// Check [`HttpRequest::url_for()`](../struct.HttpRequest.html#method. + /// url_for) for detailed information. + pub fn url_for( + &self, + req: &HttpRequest, + name: &str, + elements: U, + ) -> Result + where + U: IntoIterator, + I: AsRef, + { + let mut path = String::new(); + let mut elements = elements.into_iter(); + + if self.patterns_for(name, &mut path, &mut elements)?.is_some() { + if path.starts_with('/') { + // let conn = req.connection_info(); + // Ok(Url::parse(&format!( + // "{}://{}{}", + // conn.scheme(), + // conn.host(), + // path + // ))?) + unimplemented!() + } else { + Ok(Url::parse(&path)?) + } + } else { + Err(UrlGenerationError::ResourceNotFound) + } + } + + pub fn has_resource(&self, path: &str) -> bool { + let path = if path.is_empty() { "/" } else { path }; + + for (pattern, rmap) in &self.patterns { + if let Some(ref rmap) = rmap { + if let Some(plen) = pattern.is_prefix_match(path) { + return rmap.has_resource(&path[plen..]); + } + } else if pattern.is_match(path) { + return true; + } + } + false + } + + fn patterns_for( + &self, + name: &str, + path: &mut String, + elements: &mut U, + ) -> Result, UrlGenerationError> + where + U: Iterator, + I: AsRef, + { + if self.pattern_for(name, path, elements)?.is_some() { + Ok(Some(())) + } else { + self.parent_pattern_for(name, path, elements) + } + } + + fn pattern_for( + &self, + name: &str, + path: &mut String, + elements: &mut U, + ) -> Result, UrlGenerationError> + where + U: Iterator, + I: AsRef, + { + if let Some(pattern) = self.named.get(name) { + self.fill_root(path, elements)?; + if pattern.resource_path(path, elements) { + Ok(Some(())) + } else { + Err(UrlGenerationError::NotEnoughElements) + } + } else { + for (_, rmap) in &self.patterns { + if let Some(ref rmap) = rmap { + if rmap.pattern_for(name, path, elements)?.is_some() { + return Ok(Some(())); + } + } + } + Ok(None) + } + } + + fn fill_root( + &self, + path: &mut String, + elements: &mut U, + ) -> Result<(), UrlGenerationError> + where + U: Iterator, + I: AsRef, + { + if let Some(ref parent) = *self.parent.borrow() { + parent.fill_root(path, elements)?; + } + if self.root.resource_path(path, elements) { + Ok(()) + } else { + Err(UrlGenerationError::NotEnoughElements) + } + } + + fn parent_pattern_for( + &self, + name: &str, + path: &mut String, + elements: &mut U, + ) -> Result, UrlGenerationError> + where + U: Iterator, + I: AsRef, + { + if let Some(ref parent) = *self.parent.borrow() { + if let Some(pattern) = parent.named.get(name) { + self.fill_root(path, elements)?; + if pattern.resource_path(path, elements) { + Ok(Some(())) + } else { + Err(UrlGenerationError::NotEnoughElements) + } + } else { + parent.parent_pattern_for(name, path, elements) + } + } else { + Ok(None) + } + } +} diff --git a/src/scope.rs b/src/scope.rs index 2c2e3c2b..15c652c8 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -13,6 +13,7 @@ use futures::{Async, Poll}; use crate::dev::{AppConfig, HttpServiceFactory}; use crate::guard::Guard; use crate::resource::Resource; +use crate::rmap::ResourceMap; use crate::route::Route; use crate::service::{ ServiceFactory, ServiceFactoryWrapper, ServiceRequest, ServiceResponse, @@ -237,35 +238,46 @@ where > + 'static, { fn register(self, config: &mut AppConfig

) { + // update default resource if needed if self.default.borrow().is_none() { *self.default.borrow_mut() = Some(config.default_service()); } - // register services + // register nested services let mut cfg = config.clone_config(); self.services .into_iter() .for_each(|mut srv| srv.register(&mut cfg)); + let mut rmap = ResourceMap::new(ResourceDef::root_prefix(&self.rdef)); + + // complete scope pipeline creation *self.factory_ref.borrow_mut() = Some(ScopeFactory { default: self.default.clone(), services: Rc::new( cfg.into_services() .into_iter() - .map(|(rdef, srv, guards)| (rdef, srv, RefCell::new(guards))) + .map(|(mut rdef, srv, guards, nested)| { + rmap.add(&mut rdef, nested); + (rdef, srv, RefCell::new(guards)) + }) .collect(), ), }); + // get guards let guards = if self.guards.is_empty() { None } else { Some(self.guards) }; + + // register final service config.register_service( ResourceDef::root_prefix(&self.rdef), guards, self.endpoint, + Some(Rc::new(rmap)), ) } } @@ -367,8 +379,7 @@ impl

Future for ScopeFactoryResponse

{ .fold(Router::build(), |mut router, item| { match item { CreateScopeServiceItem::Service(path, guards, service) => { - router.rdef(path, service); - router.set_user_data(guards); + router.rdef(path, service).2 = guards; } CreateScopeServiceItem::Future(_, _, _) => unreachable!(), } diff --git a/src/server.rs b/src/server.rs index d6d88d00..d3ae5b2b 100644 --- a/src/server.rs +++ b/src/server.rs @@ -230,7 +230,7 @@ where /// /// HttpServer does not change any configuration for TcpListener, /// it needs to be configured before passing it to listen() method. - pub fn listen(mut self, lst: net::TcpListener) -> Self { + pub fn listen(mut self, lst: net::TcpListener) -> io::Result { let cfg = self.config.clone(); let factory = self.factory.clone(); let addr = lst.local_addr().unwrap(); @@ -248,9 +248,9 @@ where ServiceConfig::new(c.keep_alive, c.client_timeout, 0); HttpService::with_config(service_config, factory()) }, - )); + )?); - self + Ok(self) } #[cfg(feature = "tls")] @@ -328,7 +328,7 @@ where let sockets = self.bind2(addr)?; for lst in sockets { - self = self.listen(lst); + self = self.listen(lst)?; } Ok(self) diff --git a/src/service.rs b/src/service.rs index e2213060..ba811458 100644 --- a/src/service.rs +++ b/src/service.rs @@ -15,6 +15,7 @@ use futures::future::{ok, FutureResult, IntoFuture}; use crate::config::AppConfig; use crate::request::HttpRequest; +use crate::rmap::ResourceMap; pub trait HttpServiceFactory

{ fn register(self, config: &mut AppConfig

); @@ -58,12 +59,13 @@ impl

ServiceRequest

{ pub(crate) fn new( path: Path, request: Request

, + rmap: Rc, extensions: Rc, ) -> Self { let (head, payload) = request.into_parts(); ServiceRequest { payload, - req: HttpRequest::new(head, path, extensions), + req: HttpRequest::new(head, path, rmap, extensions), } } diff --git a/src/test.rs b/src/test.rs index ccc4b38e..e4cdefbe 100644 --- a/src/test.rs +++ b/src/test.rs @@ -6,13 +6,14 @@ use actix_http::http::header::{Header, HeaderName, IntoHeaderValue}; use actix_http::http::{HttpTryFrom, Method, Version}; use actix_http::test::TestRequest as HttpTestRequest; use actix_http::{Extensions, PayloadStream, Request}; -use actix_router::{Path, Url}; +use actix_router::{Path, ResourceDef, Url}; use actix_rt::Runtime; use actix_service::{IntoNewService, NewService, Service}; use bytes::Bytes; use futures::Future; use crate::request::HttpRequest; +use crate::rmap::ResourceMap; use crate::service::{ServiceFromRequest, ServiceRequest, ServiceResponse}; thread_local! { @@ -135,6 +136,7 @@ where pub struct TestRequest { req: HttpTestRequest, extensions: Extensions, + rmap: ResourceMap, } impl Default for TestRequest { @@ -142,6 +144,7 @@ impl Default for TestRequest { TestRequest { req: HttpTestRequest::default(), extensions: Extensions::new(), + rmap: ResourceMap::new(ResourceDef::new("")), } } } @@ -152,6 +155,7 @@ impl TestRequest { TestRequest { req: HttpTestRequest::default().uri(path).take(), extensions: Extensions::new(), + rmap: ResourceMap::new(ResourceDef::new("")), } } @@ -160,6 +164,7 @@ impl TestRequest { TestRequest { req: HttpTestRequest::default().set(hdr).take(), extensions: Extensions::new(), + rmap: ResourceMap::new(ResourceDef::new("")), } } @@ -172,6 +177,7 @@ impl TestRequest { TestRequest { req: HttpTestRequest::default().header(key, value).take(), extensions: Extensions::new(), + rmap: ResourceMap::new(ResourceDef::new("")), } } @@ -180,6 +186,7 @@ impl TestRequest { TestRequest { req: HttpTestRequest::default().method(Method::GET).take(), extensions: Extensions::new(), + rmap: ResourceMap::new(ResourceDef::new("")), } } @@ -188,6 +195,7 @@ impl TestRequest { TestRequest { req: HttpTestRequest::default().method(Method::POST).take(), extensions: Extensions::new(), + rmap: ResourceMap::new(ResourceDef::new("")), } } @@ -244,6 +252,7 @@ impl TestRequest { ServiceRequest::new( Path::new(Url::new(req.uri().clone())), req, + Rc::new(self.rmap), Rc::new(self.extensions), ) } @@ -260,6 +269,7 @@ impl TestRequest { ServiceRequest::new( Path::new(Url::new(req.uri().clone())), req, + Rc::new(self.rmap), Rc::new(self.extensions), ) .into_request() @@ -272,6 +282,7 @@ impl TestRequest { let req = ServiceRequest::new( Path::new(Url::new(req.uri().clone())), req, + Rc::new(self.rmap), Rc::new(self.extensions), ); ServiceFromRequest::new(req, None)