mirror of
https://github.com/actix/actix-extras.git
synced 2024-11-27 17:22:57 +01:00
added HttpRequest::url_for
This commit is contained in:
parent
8d52e2bbd9
commit
0dd27bd224
@ -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<S>(Rc<RouteRecognizer<Resource<S>>>);
|
||||
|
||||
impl<S: 'static> Router<S> {
|
||||
pub fn new(prefix: String, map: HashMap<String, Resource<S>>) -> Router<S>
|
||||
{
|
||||
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<String, UriGenerationError>
|
||||
where U: IntoIterator<Item=&'a str>
|
||||
{
|
||||
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<S> {
|
||||
state: Rc<S>,
|
||||
@ -66,12 +22,12 @@ pub struct HttpApplication<S> {
|
||||
impl<S: 'static> HttpApplication<S> {
|
||||
|
||||
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<S> Application<S> where S: 'static {
|
||||
/// Finish application configuration and create HttpHandler object
|
||||
pub fn finish(&mut self) -> HttpApplication<S> {
|
||||
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),
|
||||
|
@ -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};
|
||||
|
16
src/error.rs
16
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<UrlParseError> for UrlGenerationError {
|
||||
fn from(err: UrlParseError) -> Self {
|
||||
UrlGenerationError::ParseError(err)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -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<SocketAddr>,
|
||||
payload: Payload,
|
||||
info: Option<ConnectionInfo<'static>>,
|
||||
|
||||
}
|
||||
|
||||
impl Default for HttpMessage {
|
||||
@ -53,7 +55,7 @@ impl Default for HttpMessage {
|
||||
}
|
||||
|
||||
/// An HTTP Request
|
||||
pub struct HttpRequest<S=()>(Rc<HttpMessage>, Rc<S>);
|
||||
pub struct HttpRequest<S=()>(Rc<HttpMessage>, Rc<S>, Option<Router<S>>);
|
||||
|
||||
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<S>(self, state: Rc<S>) -> HttpRequest<S> {
|
||||
HttpRequest(self.0, state)
|
||||
pub fn with_state<S>(self, state: Rc<S>, router: Router<S>) -> HttpRequest<S> {
|
||||
HttpRequest(self.0, state, Some(router))
|
||||
}
|
||||
}
|
||||
|
||||
@ -90,10 +93,11 @@ impl<S> HttpRequest<S> {
|
||||
|
||||
/// 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<S> HttpRequest<S> {
|
||||
}
|
||||
|
||||
/// Shared application state
|
||||
#[inline]
|
||||
pub fn state(&self) -> &S {
|
||||
&self.1
|
||||
}
|
||||
@ -111,6 +116,7 @@ impl<S> HttpRequest<S> {
|
||||
&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<S> HttpRequest<S> {
|
||||
}
|
||||
|
||||
/// 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<S> HttpRequest<S> {
|
||||
self.0.info.as_ref().unwrap()
|
||||
}
|
||||
|
||||
pub fn url_for<U, I>(&mut self, name: &str, elements: U) -> Result<Url, UrlGenerationError>
|
||||
where U: IntoIterator<Item=I>,
|
||||
I: AsRef<str>,
|
||||
{
|
||||
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<S>> {
|
||||
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<SocketAddr>) {
|
||||
self.as_mut().addr = addr
|
||||
}
|
||||
|
||||
/// Return a new iterator that yields pairs of `Cow<str>` for query parameters
|
||||
#[inline]
|
||||
pub fn query(&self) -> HashMap<String, String> {
|
||||
let mut q: HashMap<String, String> = HashMap::new();
|
||||
if let Some(query) = self.0.uri.query().as_ref() {
|
||||
@ -206,6 +229,7 @@ impl<S> HttpRequest<S> {
|
||||
}
|
||||
|
||||
/// Return request cookies.
|
||||
#[inline]
|
||||
pub fn cookies(&self) -> &Vec<Cookie<'static>> {
|
||||
&self.0.cookies
|
||||
}
|
||||
@ -245,6 +269,7 @@ impl<S> HttpRequest<S> {
|
||||
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<S> HttpRequest<S> {
|
||||
}
|
||||
|
||||
/// 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<S> Clone for HttpRequest<S> {
|
||||
fn clone(&self) -> HttpRequest<S> {
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
@ -57,6 +57,7 @@ mod httpresponse;
|
||||
mod payload;
|
||||
mod info;
|
||||
mod route;
|
||||
mod router;
|
||||
mod resource;
|
||||
mod recognizer;
|
||||
mod handler;
|
||||
|
@ -204,16 +204,17 @@ FROM_STR!(std::net::SocketAddrV6);
|
||||
|
||||
pub struct RouteRecognizer<T> {
|
||||
re: RegexSet,
|
||||
prefix: usize,
|
||||
prefix: String,
|
||||
routes: Vec<(Pattern, T)>,
|
||||
patterns: HashMap<String, Pattern>,
|
||||
}
|
||||
|
||||
impl<T> RouteRecognizer<T> {
|
||||
|
||||
pub fn new<P: Into<String>, U, K>(prefix: P, routes: U) -> Self
|
||||
pub fn new<P, U, K>(prefix: P, routes: U) -> Self
|
||||
where U: IntoIterator<Item=(K, Option<String>, T)>,
|
||||
K: Into<String>,
|
||||
P: Into<String>,
|
||||
{
|
||||
let mut paths = Vec::new();
|
||||
let mut handlers = Vec::new();
|
||||
@ -231,7 +232,7 @@ impl<T> RouteRecognizer<T> {
|
||||
|
||||
RouteRecognizer {
|
||||
re: regset.unwrap(),
|
||||
prefix: prefix.into().len() - 1,
|
||||
prefix: prefix.into(),
|
||||
routes: handlers,
|
||||
patterns: patterns,
|
||||
}
|
||||
@ -242,29 +243,20 @@ impl<T> RouteRecognizer<T> {
|
||||
}
|
||||
|
||||
/// Length of the prefix
|
||||
pub fn prefix(&self) -> usize {
|
||||
self.prefix
|
||||
}
|
||||
|
||||
pub fn set_prefix<P: Into<String>>(&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<Params>, &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);
|
||||
|
@ -44,7 +44,7 @@ impl<S> Default for Resource<S> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> Resource<S> where S: 'static {
|
||||
impl<S> Resource<S> {
|
||||
|
||||
pub(crate) fn default_not_found() -> Self {
|
||||
Resource {
|
||||
@ -62,6 +62,9 @@ impl<S> Resource<S> where S: 'static {
|
||||
pub(crate) fn get_name(&self) -> Option<String> {
|
||||
if self.name.is_empty() { None } else { Some(self.name.clone()) }
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: 'static> Resource<S> {
|
||||
|
||||
/// Register a new route and return mutable reference to *Route* object.
|
||||
/// *Route* is used for route configuration, i.e. adding predicates, setting up handler.
|
||||
|
73
src/router.rs
Normal file
73
src/router.rs
Normal file
@ -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<S>(Rc<RouteRecognizer<Resource<S>>>);
|
||||
|
||||
impl<S> Router<S> {
|
||||
pub(crate) fn new(prefix: &str, map: HashMap<String, Resource<S>>) -> Router<S>
|
||||
{
|
||||
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<Params>, &Resource<S>)> {
|
||||
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<U, I>(&self, name: &str, elements: U)
|
||||
-> Result<String, UrlGenerationError>
|
||||
where U: IntoIterator<Item=I>,
|
||||
I: AsRef<str>,
|
||||
{
|
||||
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<S: 'static> Clone for Router<S> {
|
||||
fn clone(&self) -> Router<S> {
|
||||
Router(Rc::clone(&self.0))
|
||||
}
|
||||
}
|
@ -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();
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user