1
0
mirror of https://github.com/actix/actix-extras.git synced 2024-11-24 07:53:00 +01:00

add resource map, it allow to check if router has resource and it allows to generate urls for named resources

This commit is contained in:
Nikolay Kim 2019-03-09 07:39:34 -08:00
parent 2f6df11183
commit aadcdaa3d6
13 changed files with 361 additions and 18 deletions

View File

@ -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 }

View File

@ -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"]))
);
}
}

View File

@ -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<P> Future for AppRoutingFactoryResponse<P> {
.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<ServiceRequest, Response = ServiceRequest<P>>,
{
chain: C,
rmap: Rc<ResourceMap>,
state: Vec<Box<StateFactory>>,
extensions: Rc<RefCell<Rc<Extensions>>>,
}
@ -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<ServiceRequest, Response = ServiceRequest<P>, InitError = ()>,
{
chain: C::Future,
rmap: Rc<ResourceMap>,
state: Vec<Box<StateFactoryResult>>,
extensions: Rc<RefCell<Rc<Extensions>>>,
}
@ -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<ServiceRequest, Response = ServiceRequest<P>>,
{
chain: C,
rmap: Rc<ResourceMap>,
extensions: Rc<Extensions>,
}
@ -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)

View File

@ -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<Box<Guard>>;
@ -18,7 +19,12 @@ pub struct AppConfig<P> {
host: String,
root: bool,
default: Rc<HttpNewService<P>>,
services: Vec<(ResourceDef, HttpNewService<P>, Option<Guards>)>,
services: Vec<(
ResourceDef,
HttpNewService<P>,
Option<Guards>,
Option<Rc<ResourceMap>>,
)>,
}
impl<P: 'static> AppConfig<P> {
@ -46,7 +52,12 @@ impl<P: 'static> AppConfig<P> {
pub(crate) fn into_services(
self,
) -> Vec<(ResourceDef, HttpNewService<P>, Option<Guards>)> {
) -> Vec<(
ResourceDef,
HttpNewService<P>,
Option<Guards>,
Option<Rc<ResourceMap>>,
)> {
self.services
}
@ -85,6 +96,7 @@ impl<P: 'static> AppConfig<P> {
rdef: ResourceDef,
guards: Option<Vec<Box<Guard>>>,
service: F,
nested: Option<Rc<ResourceMap>>,
) where
F: IntoNewService<S, ServiceRequest<P>>,
S: NewService<
@ -98,6 +110,7 @@ impl<P: 'static> AppConfig<P> {
rdef,
boxed::new_service(service.into_new_service()),
guards,
nested,
));
}
}

20
src/error.rs Normal file
View File

@ -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 {}

View File

@ -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;

View File

@ -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<RequestHead>,
pub(crate) path: Path<Url>,
rmap: Rc<ResourceMap>,
extensions: Rc<Extensions>,
}
@ -23,11 +26,13 @@ impl HttpRequest {
pub(crate) fn new(
head: Message<RequestHead>,
path: Path<Url>,
rmap: Rc<ResourceMap>,
extensions: Rc<Extensions>,
) -> 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<U, I>(
&self,
name: &str,
elements: U,
) -> Result<url::Url, UrlGenerationError>
where
U: IntoIterator<Item = I>,
I: AsRef<str>,
{
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<url::Url, UrlGenerationError> {
const NO_PARAMS: [&str; 0] = [];
self.url_for(name, &NO_PARAMS)
}
// /// Get *ConnectionInfo* for the correct request.
// #[inline]
// pub fn connection_info(&self) -> Ref<ConnectionInfo> {

View File

@ -288,7 +288,7 @@ where
} else {
ResourceDef::new(&self.rdef)
};
config.register_service(rdef, guards, self)
config.register_service(rdef, guards, self, None)
}
}

188
src/rmap.rs Normal file
View File

@ -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<Option<Rc<ResourceMap>>>,
named: HashMap<String, ResourceDef>,
patterns: Vec<(ResourceDef, Option<Rc<ResourceMap>>)>,
}
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<Rc<ResourceMap>>) {
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<ResourceMap>) {
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<U, I>(
&self,
req: &HttpRequest,
name: &str,
elements: U,
) -> Result<Url, UrlGenerationError>
where
U: IntoIterator<Item = I>,
I: AsRef<str>,
{
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<U, I>(
&self,
name: &str,
path: &mut String,
elements: &mut U,
) -> Result<Option<()>, UrlGenerationError>
where
U: Iterator<Item = I>,
I: AsRef<str>,
{
if self.pattern_for(name, path, elements)?.is_some() {
Ok(Some(()))
} else {
self.parent_pattern_for(name, path, elements)
}
}
fn pattern_for<U, I>(
&self,
name: &str,
path: &mut String,
elements: &mut U,
) -> Result<Option<()>, UrlGenerationError>
where
U: Iterator<Item = I>,
I: AsRef<str>,
{
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<U, I>(
&self,
path: &mut String,
elements: &mut U,
) -> Result<(), UrlGenerationError>
where
U: Iterator<Item = I>,
I: AsRef<str>,
{
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<U, I>(
&self,
name: &str,
path: &mut String,
elements: &mut U,
) -> Result<Option<()>, UrlGenerationError>
where
U: Iterator<Item = I>,
I: AsRef<str>,
{
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)
}
}
}

View File

@ -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<P>) {
// 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<P> Future for ScopeFactoryResponse<P> {
.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!(),
}

View File

@ -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<Self> {
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)

View File

@ -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<P> {
fn register(self, config: &mut AppConfig<P>);
@ -58,12 +59,13 @@ impl<P> ServiceRequest<P> {
pub(crate) fn new(
path: Path<Url>,
request: Request<P>,
rmap: Rc<ResourceMap>,
extensions: Rc<Extensions>,
) -> Self {
let (head, payload) = request.into_parts();
ServiceRequest {
payload,
req: HttpRequest::new(head, path, extensions),
req: HttpRequest::new(head, path, rmap, extensions),
}
}

View File

@ -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)