diff --git a/README.md b/README.md index d9b7c672..a9c07b31 100644 --- a/README.md +++ b/README.md @@ -39,35 +39,21 @@ use std::str::FromStr; use actix::prelude::*; use actix_web::*; -// Route -struct MyRoute; - -impl Actor for MyRoute { - type Context = HttpContext; -} - -impl Route for MyRoute { - type State = (); - - fn request(req: HttpRequest, payload: Payload, ctx: &mut HttpContext) -> Reply - { - 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::(); - // 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(|| { diff --git a/src/application.rs b/src/application.rs index 130a548e..5ae120c2 100644 --- a/src/application.rs +++ b/src/application.rs @@ -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 Application 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 Application where S: 'static { + /// Create application builder + pub fn builder(state: S) -> ApplicationBuilder { + 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 Application where S: 'static { } /// Add resource by path. - pub fn add(&mut self, path: P) -> &mut Resource + pub fn resource(&mut self, path: P) -> &mut Resource { let path = path.to_string(); @@ -87,8 +101,31 @@ impl Application 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(&mut self, path: P, handler: F) -> &mut Self + where F: Fn(HttpRequest, Payload, &S) -> R + 'static, + R: Into + 'static, + P: ToString, + { + self.handlers.insert(path.to_string(), Box::new(FnHandler::new(handler))); + self + } + /// Add path handler - pub fn add_handler(&mut self, path: P, h: H) + pub fn route_handler(&mut self, path: P, h: H) where H: RouteHandler + 'static, P: ToString { let path = path.to_string(); @@ -107,6 +144,141 @@ impl Application where S: 'static { } } +struct ApplicationBuilderParts { + state: S, + default: Resource, + handlers: HashMap>>, + resources: HashMap>, +} + +impl From> for Application { + fn from(b: ApplicationBuilderParts) -> Self { + Application { + state: b.state, + default: b.default, + handlers: b.handlers, + resources: b.resources, + } + } +} + +/// Application builder +pub struct ApplicationBuilder { + parts: Option>, +} + +impl ApplicationBuilder 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; + /// } + /// + /// impl Route for MyRoute { + /// type State = (); + /// + /// fn request(req: HttpRequest, + /// payload: Payload, + /// ctx: &mut HttpContext) -> Reply { + /// Reply::reply(httpcodes::HTTPOk) + /// } + /// } + /// fn main() { + /// let app = Application::default() + /// .resource("/test", |r| { + /// r.get::(); + /// r.handler(Method::HEAD, |req, payload, state| { + /// httpcodes::HTTPMethodNotAllowed + /// }); + /// }) + /// .finish(); + /// } + /// ``` + pub fn resource(&mut self, path: P, f: F) -> &mut Self + where F: FnOnce(&mut Resource) + '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(&mut self, f: F) -> &mut Self + where F: FnOnce(&mut Resource) + '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(&mut self, path: P, handler: F) -> &mut Self + where F: Fn(HttpRequest, Payload, &S) -> R + 'static, + R: Into + '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(&mut self, path: P, h: H) -> &mut Self + where H: RouteHandler + '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 { + self.parts.take().expect("Use after finish").into() + } +} pub(crate) struct InnerApplication { diff --git a/src/dev.rs b/src/dev.rs index c4d36c83..1a822e72 100644 --- a/src/dev.rs +++ b/src/dev.rs @@ -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; diff --git a/src/httpcodes.rs b/src/httpcodes.rs index 82e5dc02..7ffa1a66 100644 --- a/src/httpcodes.rs +++ b/src/httpcodes.rs @@ -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 { diff --git a/src/httpresponse.rs b/src/httpresponse.rs index 07d93c25..9cf3e628 100644 --- a/src/httpresponse.rs +++ b/src/httpresponse.rs @@ -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, err: Option, } -impl Builder { +impl HttpResponseBuilder { /// Get the HTTP version of this response. #[inline] pub fn version(&mut self, version: Version) -> &mut Self { diff --git a/src/lib.rs b/src/lib.rs index 4f642726..d57e196c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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; diff --git a/src/main.rs b/src/main.rs index 0fd4e2bc..dcccb596 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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::() - .post::(); - - routes.add("/blah", app); - - routes.add_resource("/test") - .post::(); - - routes.add_resource("/ws/") - .get::(); - - 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::(); + r.post::(); + }) + .finish()) + .resource("/test", |r| r.post::()) + .resource("/ws/", |r| r.get::()) + .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(); diff --git a/src/resource.rs b/src/resource.rs index e79ca6e9..171b117d 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -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::(); +/// let router = RoutingMap::default() +/// .resource("/", |r| r.post::()) +/// .finish(); /// } pub struct Resource { state: PhantomData, @@ -50,48 +50,62 @@ impl Default for Resource { impl Resource where S: 'static { /// Register handler for specified method. - pub fn handler(&mut self, method: Method, handler: H) -> &mut Self + pub fn handler(&mut self, method: Method, handler: F) + where F: Fn(HttpRequest, Payload, &S) -> R + 'static, + R: Into + 'static, + { + self.routes.insert(method, Box::new(FnHandler::new(handler))); + } + + /// Register async handler for specified method. + pub fn async(&mut self, method: Method, handler: F) + where F: Fn(HttpRequest, Payload, &S) -> R + 'static, + R: Stream + 'static, + { + self.routes.insert(method, Box::new(StreamHandler::new(handler))); + } + + /// Register handler for specified method. + pub fn route_handler(&mut self, method: Method, handler: H) where H: RouteHandler { 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(&mut self, handler: H) -> &mut Self + pub fn default_handler(&mut self, handler: H) where H: RouteHandler { self.default = Box::new(handler); - self } /// Handler for `GET` method. - pub fn get(&mut self) -> &mut Self + pub fn get(&mut self) where A: Actor> + Route { - self.handler(Method::GET, A::factory()) + self.route_handler(Method::GET, A::factory()); } /// Handler for `POST` method. - pub fn post(&mut self) -> &mut Self + pub fn post(&mut self) where A: Actor> + Route { - self.handler(Method::POST, A::factory()) + self.route_handler(Method::POST, A::factory()); } /// Handler for `PUR` method. - pub fn put(&mut self) -> &mut Self + pub fn put(&mut self) where A: Actor> + Route { - self.handler(Method::PUT, A::factory()) + self.route_handler(Method::PUT, A::factory()); } /// Handler for `METHOD` method. - pub fn delete(&mut self) -> &mut Self + pub fn delete(&mut self) where A: Actor> + Route { - self.handler(Method::DELETE, A::factory()) + self.route_handler(Method::DELETE, A::factory()); } } diff --git a/src/route.rs b/src/route.rs index de2d5b1a..f549379b 100644 --- a/src/route.rs +++ b/src/route.rs @@ -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 RouteHandler for RouteFactory A::request(req, payload, &mut ctx).into(ctx) } } + +/// Simple route handler +pub(crate) +struct FnHandler + where F: Fn(HttpRequest, Payload, &S) -> R + 'static, + R: Into, + S: 'static, +{ + f: Box, + s: PhantomData, +} + +impl FnHandler + where F: Fn(HttpRequest, Payload, &S) -> R + 'static, + R: Into + 'static, + S: 'static, +{ + pub fn new(f: F) -> Self { + FnHandler{f: Box::new(f), s: PhantomData} + } +} + +impl RouteHandler for FnHandler + where F: Fn(HttpRequest, Payload, &S) -> R + 'static, + R: Into + 'static, + S: 'static, +{ + fn handle(&self, req: HttpRequest, payload: Payload, state: Rc) -> Task + { + Task::reply((self.f)(req, payload, &state).into()) + } +} + +/// Async route handler +pub(crate) +struct StreamHandler + where F: Fn(HttpRequest, Payload, &S) -> R + 'static, + R: Stream + 'static, + S: 'static, +{ + f: Box, + s: PhantomData, +} + +impl StreamHandler + where F: Fn(HttpRequest, Payload, &S) -> R + 'static, + R: Stream + 'static, + S: 'static, +{ + pub fn new(f: F) -> Self { + StreamHandler{f: Box::new(f), s: PhantomData} + } +} + +impl RouteHandler for StreamHandler + where F: Fn(HttpRequest, Payload, &S) -> R + 'static, + R: Stream + 'static, + S: 'static, +{ + fn handle(&self, req: HttpRequest, payload: Payload, state: Rc) -> Task + { + Task::with_stream( + (self.f)(req, payload, &state).map_err( + |_| io::Error::new(io::ErrorKind::Other, "")) + ) + } +} diff --git a/src/router.rs b/src/router.rs index 98965b92..94392a53 100644 --- a/src/router.rs +++ b/src/router.rs @@ -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>, + resources: Recognizer, +} + +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::(); +/// map.resource("/users/:userid/:friendid", |r| r.get::()); /// ``` pub struct RoutingMap { + parts: Option, +} + +struct RoutingMapParts { apps: HashMap>, resources: HashMap, } @@ -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::() - /// .post::(); - /// - /// let mut routes = RoutingMap::default(); - /// routes.add("/pre", app); + /// let mut router = + /// RoutingMap::default() + /// .app("/pre", Application::default() + /// .resource("/test", |r| { + /// r.get::(); + /// r.post::(); + /// }) + /// .finish() + /// ).finish(); /// } /// ``` /// In this example, `MyRoute` route is available as `http://.../pre/test` url. - pub fn add(&mut self, prefix: P, app: Application) + pub fn app(&mut self, prefix: P, app: Application) -> &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::(); + /// RoutingMap::default().resource("/test", |r| { + /// r.post::(); + /// }).finish(); /// } /// ``` /// In this example, `MyRoute` route is available as `http://.../test` url. - pub fn add_resource

(&mut self, path: P) -> &mut Resource - where P: ToString + pub fn resource(&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>, - resources: Recognizer, -} - -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()) - } - } -} diff --git a/src/server.rs b/src/server.rs index c0ec2f3e..76f1d1fb 100644 --- a/src/server.rs +++ b/src/server.rs @@ -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.