1
0
mirror of https://github.com/actix/actix-extras.git synced 2024-11-23 15:51:06 +01:00

Application, router, resource builders

This commit is contained in:
Nikolay Kim 2017-10-15 14:17:41 -07:00
parent 5480cb5d49
commit 5901f0f9f5
11 changed files with 406 additions and 146 deletions

View File

@ -39,35 +39,21 @@ use std::str::FromStr;
use actix::prelude::*;
use actix_web::*;
// Route
struct MyRoute;
impl Actor for MyRoute {
type Context = HttpContext<Self>;
}
impl Route for MyRoute {
type State = ();
fn request(req: HttpRequest, payload: Payload, ctx: &mut HttpContext<Self>) -> Reply<Self>
{
Reply::reply(httpcodes::HTTPOk)
}
}
fn main() {
let system = System::new("test");
// create routing map with `MyRoute` route
let mut routes = RoutingMap::default();
routes
.add_resource("/")
.post::<MyRoute>();
// start http server
let http = HttpServer::new(routes);
http.serve::<()>(
&net::SocketAddr::from_str("127.0.0.1:8880").unwrap()).unwrap();
HttpServer::new(
// create routing map with `MyRoute` route
RoutingMap::default()
.resource("/", |r|
r.handler(Method::GET, |req, payload, state| {
httpcodes::HTTPOk
})
)
.finish())
.serve::<()>(
&net::SocketAddr::from_str("127.0.0.1:8880").unwrap()).unwrap();
// stop system
Arbiter::handle().spawn_fn(|| {

View File

@ -5,11 +5,12 @@ use std::collections::HashMap;
use route_recognizer::Router;
use task::Task;
use route::RouteHandler;
use route::{RouteHandler, FnHandler};
use router::Handler;
use resource::Resource;
use payload::Payload;
use httprequest::HttpRequest;
use httpresponse::HttpResponse;
/// Application
@ -47,21 +48,34 @@ impl<S> Application<S> where S: 'static
}
}
impl Default for Application<()> {
/// Create default `Application` with no state
fn default() -> Self {
Application {
state: (),
default: Resource::default(),
handlers: HashMap::new(),
resources: HashMap::new(),
impl Application<()> {
/// Create default `ApplicationBuilder` with no state
pub fn default() -> ApplicationBuilder<()> {
ApplicationBuilder {
parts: Some(ApplicationBuilderParts {
state: (),
default: Resource::default(),
handlers: HashMap::new(),
resources: HashMap::new()})
}
}
}
impl<S> Application<S> where S: 'static {
/// Create application builder
pub fn builder(state: S) -> ApplicationBuilder<S> {
ApplicationBuilder {
parts: Some(ApplicationBuilderParts {
state: state,
default: Resource::default(),
handlers: HashMap::new(),
resources: HashMap::new()})
}
}
/// Create http application with specific state. State is shared with all
/// routes within same application and could be
/// accessed with `HttpContext::state()` method.
@ -75,7 +89,7 @@ impl<S> Application<S> where S: 'static {
}
/// Add resource by path.
pub fn add<P: ToString>(&mut self, path: P) -> &mut Resource<S>
pub fn resource<P: ToString>(&mut self, path: P) -> &mut Resource<S>
{
let path = path.to_string();
@ -87,8 +101,31 @@ impl<S> Application<S> where S: 'static {
self.resources.get_mut(&path).unwrap()
}
/// This method register handler for specified path.
///
/// ```rust
/// extern crate actix_web;
/// use actix_web::*;
///
/// fn main() {
/// let mut app = Application::new(());
///
/// app.handler("/test", |req, payload, state| {
/// httpcodes::HTTPOk
/// });
/// }
/// ```
pub fn handler<P, F, R>(&mut self, path: P, handler: F) -> &mut Self
where F: Fn(HttpRequest, Payload, &S) -> R + 'static,
R: Into<HttpResponse> + 'static,
P: ToString,
{
self.handlers.insert(path.to_string(), Box::new(FnHandler::new(handler)));
self
}
/// Add path handler
pub fn add_handler<H, P>(&mut self, path: P, h: H)
pub fn route_handler<H, P>(&mut self, path: P, h: H)
where H: RouteHandler<S> + 'static, P: ToString
{
let path = path.to_string();
@ -107,6 +144,141 @@ impl<S> Application<S> where S: 'static {
}
}
struct ApplicationBuilderParts<S> {
state: S,
default: Resource<S>,
handlers: HashMap<String, Box<RouteHandler<S>>>,
resources: HashMap<String, Resource<S>>,
}
impl<S> From<ApplicationBuilderParts<S>> for Application<S> {
fn from(b: ApplicationBuilderParts<S>) -> Self {
Application {
state: b.state,
default: b.default,
handlers: b.handlers,
resources: b.resources,
}
}
}
/// Application builder
pub struct ApplicationBuilder<S=()> {
parts: Option<ApplicationBuilderParts<S>>,
}
impl<S> ApplicationBuilder<S> where S: 'static {
/// Configure resource for specific path.
///
/// ```rust
/// extern crate actix;
/// extern crate actix_web;
/// use actix_web::*;
/// use actix::prelude::*;
///
/// struct MyRoute;
///
/// impl Actor for MyRoute {
/// type Context = HttpContext<Self>;
/// }
///
/// impl Route for MyRoute {
/// type State = ();
///
/// fn request(req: HttpRequest,
/// payload: Payload,
/// ctx: &mut HttpContext<Self>) -> Reply<Self> {
/// Reply::reply(httpcodes::HTTPOk)
/// }
/// }
/// fn main() {
/// let app = Application::default()
/// .resource("/test", |r| {
/// r.get::<MyRoute>();
/// r.handler(Method::HEAD, |req, payload, state| {
/// httpcodes::HTTPMethodNotAllowed
/// });
/// })
/// .finish();
/// }
/// ```
pub fn resource<F, P: ToString>(&mut self, path: P, f: F) -> &mut Self
where F: FnOnce(&mut Resource<S>) + 'static
{
{
let parts = self.parts.as_mut().expect("Use after finish");
// add resource
let path = path.to_string();
if !parts.resources.contains_key(&path) {
parts.resources.insert(path.clone(), Resource::default());
}
f(parts.resources.get_mut(&path).unwrap());
}
self
}
/// Default resource is used if no matches route could be found.
pub fn default_resource<F>(&mut self, f: F) -> &mut Self
where F: FnOnce(&mut Resource<S>) + 'static
{
{
let parts = self.parts.as_mut().expect("Use after finish");
f(&mut parts.default);
}
self
}
/// This method register handler for specified path.
///
/// ```rust
/// extern crate actix_web;
/// use actix_web::*;
///
/// fn main() {
/// let app = Application::default()
/// .handler("/test", |req, payload, state| {
/// match *req.method() {
/// Method::GET => httpcodes::HTTPOk,
/// Method::POST => httpcodes::HTTPMethodNotAllowed,
/// _ => httpcodes::HTTPNotFound,
/// }
/// })
/// .finish();
/// }
/// ```
pub fn handler<P, F, R>(&mut self, path: P, handler: F) -> &mut Self
where F: Fn(HttpRequest, Payload, &S) -> R + 'static,
R: Into<HttpResponse> + 'static,
P: ToString,
{
self.parts.as_mut().expect("Use after finish")
.handlers.insert(path.to_string(), Box::new(FnHandler::new(handler)));
self
}
/// Add path handler
pub fn route_handler<H, P>(&mut self, path: P, h: H) -> &mut Self
where H: RouteHandler<S> + 'static, P: ToString
{
{
// add resource
let parts = self.parts.as_mut().expect("Use after finish");
let path = path.to_string();
if parts.handlers.contains_key(&path) {
panic!("Handler already registered: {:?}", path);
}
parts.handlers.insert(path, Box::new(h));
}
self
}
/// Construct application
pub fn finish(&mut self) -> Application<S> {
self.parts.take().expect("Use after finish").into()
}
}
pub(crate)
struct InnerApplication<S> {

View File

@ -10,9 +10,9 @@
pub use ws;
pub use httpcodes;
pub use error::ParseError;
pub use application::Application;
pub use application::{Application, ApplicationBuilder};
pub use httprequest::HttpRequest;
pub use httpresponse::{Body, Builder, HttpResponse};
pub use httpresponse::{Body, HttpResponse, HttpResponseBuilder};
pub use payload::{Payload, PayloadItem, PayloadError};
pub use router::RoutingMap;
pub use resource::{Reply, Resource};
@ -22,6 +22,7 @@ pub use context::HttpContext;
pub use staticfiles::StaticFiles;
// re-exports
pub use http::{Method, StatusCode};
pub use cookie::{Cookie, CookieBuilder};
pub use cookie::{ParseError as CookieParseError};
pub use route_recognizer::Params;

View File

@ -7,7 +7,7 @@ use task::Task;
use route::RouteHandler;
use payload::Payload;
use httprequest::HttpRequest;
use httpresponse::{Body, Builder, HttpResponse};
use httpresponse::{Body, HttpResponse, HttpResponseBuilder};
pub const HTTPOk: StaticResponse = StaticResponse(StatusCode::OK);
pub const HTTPCreated: StaticResponse = StaticResponse(StatusCode::CREATED);
@ -23,7 +23,7 @@ pub const HTTPInternalServerError: StaticResponse =
pub struct StaticResponse(StatusCode);
impl StaticResponse {
pub fn builder(&self) -> Builder {
pub fn builder(&self) -> HttpResponseBuilder {
HttpResponse::builder(self.0)
}
pub fn response(&self) -> HttpResponse {

View File

@ -62,8 +62,8 @@ pub struct HttpResponse {
impl HttpResponse {
#[inline]
pub fn builder(status: StatusCode) -> Builder {
Builder {
pub fn builder(status: StatusCode) -> HttpResponseBuilder {
HttpResponseBuilder {
parts: Some(Parts::new(status)),
err: None,
}
@ -224,12 +224,12 @@ impl Parts {
/// This type can be used to construct an instance of `HttpResponse` through a
/// builder-like pattern.
#[derive(Debug)]
pub struct Builder {
pub struct HttpResponseBuilder {
parts: Option<Parts>,
err: Option<Error>,
}
impl Builder {
impl HttpResponseBuilder {
/// Get the HTTP version of this response.
#[inline]
pub fn version(&mut self, version: Version) -> &mut Self {

View File

@ -48,11 +48,11 @@ pub mod ws;
pub mod dev;
pub mod httpcodes;
pub use error::ParseError;
pub use application::Application;
pub use application::{Application, ApplicationBuilder};
pub use httprequest::HttpRequest;
pub use httpresponse::{Body, Builder, HttpResponse};
pub use httpresponse::{Body, HttpResponse, HttpResponseBuilder};
pub use payload::{Payload, PayloadItem, PayloadError};
pub use router::RoutingMap;
pub use router::{Router, RoutingMap};
pub use resource::{Reply, Resource};
pub use route::{Route, RouteFactory, RouteHandler};
pub use server::HttpServer;
@ -60,6 +60,7 @@ pub use context::HttpContext;
pub use staticfiles::StaticFiles;
// re-exports
pub use http::{Method, StatusCode};
pub use cookie::{Cookie, CookieBuilder};
pub use cookie::{ParseError as CookieParseError};
pub use route_recognizer::Params;

View File

@ -105,24 +105,24 @@ fn main() {
let sys = actix::System::new("http-example");
let mut routes = RoutingMap::default();
let mut app = Application::default();
app.add("/test")
.get::<MyRoute>()
.post::<MyRoute>();
routes.add("/blah", app);
routes.add_resource("/test")
.post::<MyRoute>();
routes.add_resource("/ws/")
.get::<MyWS>();
let http = HttpServer::new(routes);
http.serve::<()>(
&net::SocketAddr::from_str("127.0.0.1:9080").unwrap()).unwrap();
HttpServer::new(
RoutingMap::default()
.app(
"/blah", Application::default()
.resource("/test", |r| {
r.get::<MyRoute>();
r.post::<MyRoute>();
})
.finish())
.resource("/test", |r| r.post::<MyRoute>())
.resource("/ws/", |r| r.get::<MyWS>())
.resource("/simple/", |r|
r.handler(Method::GET, |req, payload, state| {
httpcodes::HTTPOk
}))
.finish())
.serve::<()>(
&net::SocketAddr::from_str("127.0.0.1:9080").unwrap()).unwrap();
println!("starting");
let _ = sys.run();

View File

@ -5,9 +5,10 @@ use std::collections::HashMap;
use actix::Actor;
use http::Method;
use futures::Stream;
use task::Task;
use route::{Route, RouteHandler};
use route::{Route, RouteHandler, Frame, FnHandler, StreamHandler};
use payload::Payload;
use context::HttpContext;
use httprequest::HttpRequest;
@ -26,10 +27,9 @@ use httpcodes::HTTPMethodNotAllowed;
/// struct MyRoute;
///
/// fn main() {
/// let mut routes = RoutingMap::default();
///
/// routes.add_resource("/")
/// .post::<MyRoute>();
/// let router = RoutingMap::default()
/// .resource("/", |r| r.post::<MyRoute>())
/// .finish();
/// }
pub struct Resource<S=()> {
state: PhantomData<S>,
@ -50,48 +50,62 @@ impl<S> Default for Resource<S> {
impl<S> Resource<S> where S: 'static {
/// Register handler for specified method.
pub fn handler<H>(&mut self, method: Method, handler: H) -> &mut Self
pub fn handler<F, R>(&mut self, method: Method, handler: F)
where F: Fn(HttpRequest, Payload, &S) -> R + 'static,
R: Into<HttpResponse> + 'static,
{
self.routes.insert(method, Box::new(FnHandler::new(handler)));
}
/// Register async handler for specified method.
pub fn async<F, R>(&mut self, method: Method, handler: F)
where F: Fn(HttpRequest, Payload, &S) -> R + 'static,
R: Stream<Item=Frame, Error=()> + 'static,
{
self.routes.insert(method, Box::new(StreamHandler::new(handler)));
}
/// Register handler for specified method.
pub fn route_handler<H>(&mut self, method: Method, handler: H)
where H: RouteHandler<S>
{
self.routes.insert(method, Box::new(handler));
self
}
/// Default handler is used if no matched route found.
/// By default `HTTPMethodNotAllowed` is used.
pub fn default_handler<H>(&mut self, handler: H) -> &mut Self
pub fn default_handler<H>(&mut self, handler: H)
where H: RouteHandler<S>
{
self.default = Box::new(handler);
self
}
/// Handler for `GET` method.
pub fn get<A>(&mut self) -> &mut Self
pub fn get<A>(&mut self)
where A: Actor<Context=HttpContext<A>> + Route<State=S>
{
self.handler(Method::GET, A::factory())
self.route_handler(Method::GET, A::factory());
}
/// Handler for `POST` method.
pub fn post<A>(&mut self) -> &mut Self
pub fn post<A>(&mut self)
where A: Actor<Context=HttpContext<A>> + Route<State=S>
{
self.handler(Method::POST, A::factory())
self.route_handler(Method::POST, A::factory());
}
/// Handler for `PUR` method.
pub fn put<A>(&mut self) -> &mut Self
pub fn put<A>(&mut self)
where A: Actor<Context=HttpContext<A>> + Route<State=S>
{
self.handler(Method::PUT, A::factory())
self.route_handler(Method::PUT, A::factory());
}
/// Handler for `METHOD` method.
pub fn delete<A>(&mut self) -> &mut Self
pub fn delete<A>(&mut self)
where A: Actor<Context=HttpContext<A>> + Route<State=S>
{
self.handler(Method::DELETE, A::factory())
self.route_handler(Method::DELETE, A::factory());
}
}

View File

@ -1,8 +1,10 @@
use std::io;
use std::rc::Rc;
use std::marker::PhantomData;
use actix::Actor;
use bytes::Bytes;
use futures::Stream;
use task::Task;
use context::HttpContext;
@ -59,3 +61,70 @@ impl<A, S> RouteHandler<S> for RouteFactory<A, S>
A::request(req, payload, &mut ctx).into(ctx)
}
}
/// Simple route handler
pub(crate)
struct FnHandler<S, R, F>
where F: Fn(HttpRequest, Payload, &S) -> R + 'static,
R: Into<HttpResponse>,
S: 'static,
{
f: Box<F>,
s: PhantomData<S>,
}
impl<S, R, F> FnHandler<S, R, F>
where F: Fn(HttpRequest, Payload, &S) -> R + 'static,
R: Into<HttpResponse> + 'static,
S: 'static,
{
pub fn new(f: F) -> Self {
FnHandler{f: Box::new(f), s: PhantomData}
}
}
impl<S, R, F> RouteHandler<S> for FnHandler<S, R, F>
where F: Fn(HttpRequest, Payload, &S) -> R + 'static,
R: Into<HttpResponse> + 'static,
S: 'static,
{
fn handle(&self, req: HttpRequest, payload: Payload, state: Rc<S>) -> Task
{
Task::reply((self.f)(req, payload, &state).into())
}
}
/// Async route handler
pub(crate)
struct StreamHandler<S, R, F>
where F: Fn(HttpRequest, Payload, &S) -> R + 'static,
R: Stream<Item=Frame, Error=()> + 'static,
S: 'static,
{
f: Box<F>,
s: PhantomData<S>,
}
impl<S, R, F> StreamHandler<S, R, F>
where F: Fn(HttpRequest, Payload, &S) -> R + 'static,
R: Stream<Item=Frame, Error=()> + 'static,
S: 'static,
{
pub fn new(f: F) -> Self {
StreamHandler{f: Box::new(f), s: PhantomData}
}
}
impl<S, R, F> RouteHandler<S> for StreamHandler<S, R, F>
where F: Fn(HttpRequest, Payload, &S) -> R + 'static,
R: Stream<Item=Frame, Error=()> + 'static,
S: 'static,
{
fn handle(&self, req: HttpRequest, payload: Payload, state: Rc<S>) -> Task
{
Task::with_stream(
(self.f)(req, payload, &state).map_err(
|_| io::Error::new(io::ErrorKind::Other, ""))
)
}
}

View File

@ -15,7 +15,30 @@ pub(crate) trait Handler: 'static {
fn handle(&self, req: HttpRequest, payload: Payload) -> Task;
}
/// Request routing map
/// Server routing map
pub struct Router {
apps: HashMap<String, Box<Handler>>,
resources: Recognizer<Resource>,
}
impl Router {
pub(crate) fn call(&self, req: HttpRequest, payload: Payload) -> Task
{
if let Ok(h) = self.resources.recognize(req.path()) {
h.handler.handle(req.with_params(h.params), payload, Rc::new(()))
} else {
for (prefix, app) in &self.apps {
if req.path().starts_with(prefix) {
return app.handle(req, payload)
}
}
Task::reply(HTTPNotFound.response())
}
}
}
/// Request routing map builder
///
/// Route supports glob patterns: * for a single wildcard segment and :param
/// for matching storing that segment of the request url in the Params object,
@ -25,11 +48,15 @@ pub(crate) trait Handler: 'static {
/// store userid and friend in the exposed Params object:
///
/// ```rust,ignore
/// let mut router = RoutingMap::default();
/// let mut map = RoutingMap::default();
///
/// router.add_resource("/users/:userid/:friendid").get::<MyRoute>();
/// map.resource("/users/:userid/:friendid", |r| r.get::<MyRoute>());
/// ```
pub struct RoutingMap {
parts: Option<RoutingMapParts>,
}
struct RoutingMapParts {
apps: HashMap<String, Box<Handler>>,
resources: HashMap<String, Resource>,
}
@ -37,8 +64,9 @@ pub struct RoutingMap {
impl Default for RoutingMap {
fn default() -> Self {
RoutingMap {
apps: HashMap::new(),
resources: HashMap::new()
parts: Some(RoutingMapParts {
apps: HashMap::new(),
resources: HashMap::new()}),
}
}
}
@ -53,92 +81,81 @@ impl RoutingMap {
/// struct MyRoute;
///
/// fn main() {
/// let mut app = Application::default();
/// app.add("/test")
/// .get::<MyRoute>()
/// .post::<MyRoute>();
///
/// let mut routes = RoutingMap::default();
/// routes.add("/pre", app);
/// let mut router =
/// RoutingMap::default()
/// .app("/pre", Application::default()
/// .resource("/test", |r| {
/// r.get::<MyRoute>();
/// r.post::<MyRoute>();
/// })
/// .finish()
/// ).finish();
/// }
/// ```
/// In this example, `MyRoute` route is available as `http://.../pre/test` url.
pub fn add<P, S: 'static>(&mut self, prefix: P, app: Application<S>)
pub fn app<P, S: 'static>(&mut self, prefix: P, app: Application<S>) -> &mut Self
where P: ToString
{
let prefix = prefix.to_string();
{
let parts = self.parts.as_mut().expect("Use after finish");
// we can not override registered resource
if self.apps.contains_key(&prefix) {
panic!("Resource is registered: {}", prefix);
// we can not override registered resource
let prefix = prefix.to_string();
if parts.apps.contains_key(&prefix) {
panic!("Resource is registered: {}", prefix);
}
// add application
parts.apps.insert(prefix.clone(), app.prepare(prefix));
}
// add application
self.apps.insert(prefix.clone(), app.prepare(prefix));
self
}
/// This method creates `Resource` for specified path
/// or returns mutable reference to resource object.
/// Configure resource for specific path.
///
/// ```rust,ignore
///
/// struct MyRoute;
///
/// fn main() {
/// let mut routes = RoutingMap::default();
///
/// routes.add_resource("/test")
/// .post::<MyRoute>();
/// RoutingMap::default().resource("/test", |r| {
/// r.post::<MyRoute>();
/// }).finish();
/// }
/// ```
/// In this example, `MyRoute` route is available as `http://.../test` url.
pub fn add_resource<P>(&mut self, path: P) -> &mut Resource
where P: ToString
pub fn resource<P, F>(&mut self, path: P, f: F) -> &mut Self
where F: FnOnce(&mut Resource<()>) + 'static,
P: ToString,
{
let path = path.to_string();
{
let parts = self.parts.as_mut().expect("Use after finish");
// add resource
if !self.resources.contains_key(&path) {
self.resources.insert(path.clone(), Resource::default());
// add resource
let path = path.to_string();
if !parts.resources.contains_key(&path) {
parts.resources.insert(path.clone(), Resource::default());
}
// configure resource
f(parts.resources.get_mut(&path).unwrap());
}
self.resources.get_mut(&path).unwrap()
self
}
pub(crate) fn into_router(self) -> Router {
/// Finish configuration and create `Router` instance
pub fn finish(&mut self) -> Router
{
let parts = self.parts.take().expect("Use after finish");
let mut router = Recognizer::new();
for (path, resource) in self.resources {
for (path, resource) in parts.resources {
router.add(path.as_str(), resource);
}
Router {
apps: self.apps,
apps: parts.apps,
resources: router,
}
}
}
pub(crate)
struct Router {
apps: HashMap<String, Box<Handler>>,
resources: Recognizer<Resource>,
}
impl Router {
pub fn call(&self, req: HttpRequest, payload: Payload) -> Task
{
if let Ok(h) = self.resources.recognize(req.path()) {
h.handler.handle(req.with_params(h.params), payload, Rc::new(()))
} else {
for (prefix, app) in &self.apps {
if req.path().starts_with(prefix) {
return app.handle(req, payload)
}
}
Task::reply(HTTPNotFound.response())
}
}
}

View File

@ -9,8 +9,8 @@ use tokio_core::reactor::Timeout;
use tokio_core::net::{TcpListener, TcpStream};
use task::{Task, RequestInfo};
use router::Router;
use reader::{Reader, ReaderError};
use router::{Router, RoutingMap};
/// An HTTP Server
pub struct HttpServer {
@ -23,8 +23,8 @@ impl Actor for HttpServer {
impl HttpServer {
/// Create new http server with specified `RoutingMap`
pub fn new(routes: RoutingMap) -> Self {
HttpServer {router: Rc::new(routes.into_router())}
pub fn new(router: Router) -> Self {
HttpServer {router: Rc::new(router)}
}
/// Start listening for incomming connections.