diff --git a/http/examples/route.rs b/http/examples/route.rs new file mode 100644 index 000000000..05b05c08e --- /dev/null +++ b/http/examples/route.rs @@ -0,0 +1,131 @@ +#![feature(never_type)] + +use std::io; +use std::net::SocketAddr; +use std::time::Duration; + +use heph::actor::{self, Actor, NewActor}; +use heph::net::TcpStream; +use heph::rt::{self, Runtime, ThreadLocal}; +use heph::spawn::options::{ActorOptions, Priority}; +use heph::supervisor::{Supervisor, SupervisorStrategy}; +use heph::timer::Deadline; +use heph_http::body::OneshotBody; +use heph_http::{self as http, route, HttpServer, Request, Response}; +use log::{error, info, warn}; + +fn main() -> Result<(), rt::Error> { + std_logger::init(); + + let actor = http_actor as fn(_, _, _) -> _; + let address = "127.0.0.1:7890".parse().unwrap(); + let server = HttpServer::setup(address, conn_supervisor, actor, ActorOptions::default()) + .map_err(rt::Error::setup)?; + + let mut runtime = Runtime::setup().use_all_cores().build()?; + runtime.run_on_workers(move |mut runtime_ref| -> io::Result<()> { + let options = ActorOptions::default().with_priority(Priority::LOW); + let server_ref = runtime_ref.try_spawn_local(ServerSupervisor, server, (), options)?; + + runtime_ref.receive_signals(server_ref.try_map()); + Ok(()) + })?; + info!("listening on http://{}", address); + runtime.start() +} + +/// Our supervisor for the TCP server. +#[derive(Copy, Clone, Debug)] +struct ServerSupervisor; + +impl Supervisor for ServerSupervisor +where + NA: NewActor, + NA::Actor: Actor>, +{ + fn decide(&mut self, err: http::server::Error) -> SupervisorStrategy<()> { + use http::server::Error::*; + match err { + Accept(err) => { + error!("error accepting new connection: {}", err); + SupervisorStrategy::Restart(()) + } + NewActor(_) => unreachable!(), + } + } + + fn decide_on_restart_error(&mut self, err: io::Error) -> SupervisorStrategy<()> { + error!("error restarting the TCP server: {}", err); + SupervisorStrategy::Stop + } + + fn second_restart_error(&mut self, err: io::Error) { + error!("error restarting the actor a second time: {}", err); + } +} + +fn conn_supervisor(err: io::Error) -> SupervisorStrategy<(TcpStream, SocketAddr)> { + error!("error handling connection: {}", err); + SupervisorStrategy::Stop +} + +const READ_TIMEOUT: Duration = Duration::from_secs(10); +const ALIVE_TIMEOUT: Duration = Duration::from_secs(120); +const WRITE_TIMEOUT: Duration = Duration::from_secs(10); + +async fn http_actor( + mut ctx: actor::Context, + mut connection: http::Connection, + address: SocketAddr, +) -> io::Result<()> { + info!("accepted connection: source={}", address); + connection.set_nodelay(true)?; + + let mut read_timeout = READ_TIMEOUT; + loop { + let fut = Deadline::after(&mut ctx, read_timeout, connection.next_request()); + + let response = match fut.await? { + Ok(Some(request)) => { + info!("received request: {:?}: source={}", request, address); + route!(match request { + GET | HEAD "/" => index, + GET | HEAD "/other_page" => other_page, + POST "/post" => post, + _ => not_found, + }) + } + // No more requests. + Ok(None) => return Ok(()), + Err(err) => { + warn!("error reading request: {}: source={}", err, address); + err.response().with_body("Bad request".into()) + } + }; + + // TODO: improve this, add a `Connection::respond_with(Response)` method. + let (head, body) = response.split(); + let write_response = connection.respond(head.status(), &head.headers(), body); + Deadline::after(&mut ctx, WRITE_TIMEOUT, write_response).await?; + + // Now that we've read a single request we can wait a little for the + // next one so that we can reuse the resources for the next request. + read_timeout = ALIVE_TIMEOUT; + } +} + +async fn index(_req: Request) -> Response> { + Response::ok().with_body("Index".into()) +} + +async fn other_page(_req: Request) -> Response> { + Response::ok().with_body("Other page!".into()) +} + +async fn post(_req: Request) -> Response> { + Response::ok().with_body("POST".into()) +} + +async fn not_found(_req: Request) -> Response> { + Response::not_found().with_body("Page not found".into()) +} diff --git a/http/src/lib.rs b/http/src/lib.rs index f1c4ee176..1e519e258 100644 --- a/http/src/lib.rs +++ b/http/src/lib.rs @@ -30,6 +30,7 @@ pub mod client; pub mod head; mod request; mod response; +mod route; pub mod server; #[doc(no_inline)] diff --git a/http/src/route.rs b/http/src/route.rs new file mode 100644 index 000000000..fe70653af --- /dev/null +++ b/http/src/route.rs @@ -0,0 +1,167 @@ +//! Module with the [`route!`] macro. + +/// Macro to route a request to HTTP handlers. +/// +/// This macro follows a match statement-like syntax, the easiest way to +/// document that is an example: +/// +/// ``` +/// # #![allow(dead_code)] +/// # use heph_http::{route, Request}; +/// # +/// # async fn test(request: Request) { +/// route!(match request { +/// // Route GET and HEAD requests to / to `index`. +/// GET | HEAD "/" => index, +/// GET | HEAD "/some_route" => some_route_handler, +/// // Route POST requests to /pet to `create_pet`. +/// POST "/pet" => create_pet, +/// // Anything that doesn't match the routes above will be routed to the +/// // `not_found` handler. +/// _ => not_found, +/// }) +/// # } +/// # async fn index(_: Request) { } +/// # async fn some_route_handler(_: Request) { } +/// # async fn create_pet(_: Request) { } +/// # async fn not_found(_: Request) { } +/// ``` +/// +/// The example has 5 routes: +/// * Using GET or HEAD to the path `/` will route to the `index` handler (2 +/// routes), +/// * GET or HEAD to `/some_route` to `some_route_handler`, and +/// * POST to `/pet` to `create_pet`. +/// +/// The match statement expects two arguments in it's pattern: a method or +/// multiple methods and a path to match. The arm must be a async function that +/// accepts the `request` and returns a response. +/// +/// # Types +/// +/// The macro is untyped, but expects `request` to be a [`Request`] and will +/// call at least the [`method`] and [`path`] methods on it. The generic +/// parameter `B` of `Request` must be the same for all handlers, in most +/// cases this will be [`server::Body`], but it could be helpful to make the +/// handler generic of the body to make testing them simpler. +/// +/// As mentioned, all handlers must accept a `Request`, with the same body +/// `B` for all handlers. The handlers must be async functions, i.e. functions +/// that return a `Future>`. Here the response body `B` must +/// also be the same for all handlers. +/// +/// [`Request`]: crate::Request +/// [`method`]: crate::head::RequestHead::method +/// [`path`]: crate::head::RequestHead::path +/// [`server::Body`]: crate::server::Body +#[macro_export] +macro_rules! route { + (match $request: ident { + $( $method: ident $( | $method2: ident )* $path: literal => $handler: path, )+ + _ => $not_found: path $(,)* + }) => {{ + $( $crate::_route!( _check_method $method $(, $method2 )* ); )+ + let request = $request; + match request.method() { + $crate::Method::Get => $crate::_route!(_single Get, request, + $( $method $(, $method2 )* $path => $handler, )+ + _ => $not_found + ), + $crate::Method::Head => $crate::_route!(_single Head, request, + $( $method $(, $method2 )* $path => $handler, )+ + _ => $not_found + ), + $crate::Method::Post => $crate::_route!(_single Post, request, + $( $method $(, $method2 )* $path => $handler, )+ + _ => $not_found + ), + $crate::Method::Put => $crate::_route!(_single Put, request, + $( $method $(, $method2 )* $path => $handler, )+ + _ => $not_found + ), + $crate::Method::Delete => $crate::_route!(_single Delete, request, + $( $method $(, $method2 )* $path => $handler, )+ + _ => $not_found + ), + $crate::Method::Connect => $crate::_route!(_single Connect, request, + $( $method $(, $method2 )* $path => $handler, )+ + _ => $not_found + ), + $crate::Method::Options => $crate::_route!(_single Options, request, + $( $method $(, $method2 )* $path => $handler, )+ + _ => $not_found + ), + $crate::Method::Trace => $crate::_route!(_single Trace, request, + $( $method $(, $method2 )* $path => $handler, )+ + _ => $not_found + ), + $crate::Method::Patch => $crate::_route!(_single Patch, request, + $( $method $(, $method2 )* $path => $handler, )+ + _ => $not_found + ), + } + }}; +} + +/// Helper macro for the [`route!`] macro. +/// +/// This a private macro so that we don't clutter the public documentation with +/// all private variants of the macro. +#[doc(hidden)] +#[macro_export] +#[rustfmt::skip] +macro_rules! _route { + // Single arm in the match statement. + ( + _single + $match_method: ident, $request: ident, + $( $( $method: ident),+ $path: literal => $handler: path, )+ + _ => $not_found: path $(,)* + ) => {{ + let path = $request.path(); + // This branch is never taken and will be removed by the compiler. + if false { + unreachable!() + } + $( + // If any of the `$method`s match `$match_method` the `(false || + // $(...))` bit will return `true`, else to `false`. Depending on + // that value the compile can either remove the branch (as `false && + // ...` is always false) or removes the the entire `(false || ...)` + // bit and just check the path.. + else if (false $( || $crate::_route!(_method_filter $match_method $method) )+) && path == $path { + $handler($request).await + } + )+ + else { + $not_found($request).await + } + }}; + + // Check the methods. + ( _check_method GET $(, $method: ident )*) => {{ $crate::_route!(_check_method $( $method ),* ); }}; + ( _check_method HEAD $(, $method: ident )*) => {{ $crate::_route!(_check_method $( $method ),* ); }}; + ( _check_method POST $(, $method: ident )*) => {{ $crate::_route!(_check_method $( $method ),* ); }}; + ( _check_method PUT $(, $method: ident )*) => {{ $crate::_route!(_check_method $( $method ),* ); }}; + ( _check_method DELETE $(, $method: ident )*) => {{ $crate::_route!(_check_method $( $method ),* ); }}; + ( _check_method CONNECT $(, $method: ident )*) => {{ $crate::_route!(_check_method $( $method ),* ); }}; + ( _check_method OPTIONS $(, $method: ident )*) => {{ $crate::_route!(_check_method $( $method ),* ); }}; + ( _check_method TRACE $(, $method: ident )*) => {{ $crate::_route!(_check_method $( $method ),* ); }}; + ( _check_method PATCH $(, $method: ident )*) => {{ $crate::_route!(_check_method $( $method ),* ); }}; + ( _check_method ) => {{ /* All good. */ }}; + + // Check if the method of route (left) matches the method of the route + // (right). + // If `$expected` == `$got` then true, else false. + (_method_filter Get GET) => {{ true }}; + (_method_filter Head HEAD) => {{ true }}; + (_method_filter Post POST) => {{ true }}; + (_method_filter Put PUT) => {{ true }}; + (_method_filter Delete DELETE) => {{ true }}; + (_method_filter Connect CONNECT) => {{ true }}; + (_method_filter Options OPTIONS) => {{ true }}; + (_method_filter Trace TRACE) => {{ true }}; + (_method_filter Patch PATCH) => {{ true }}; + // No match. + (_method_filter $expected: ident $got: ident) => {{ false }}; +} diff --git a/http/tests/functional.rs b/http/tests/functional.rs index a7dce86f8..33d98c2cc 100644 --- a/http/tests/functional.rs +++ b/http/tests/functional.rs @@ -21,6 +21,7 @@ mod functional { mod header; mod message; mod method; + mod route; mod server; mod status_code; mod version; diff --git a/http/tests/functional/route.rs b/http/tests/functional/route.rs new file mode 100644 index 000000000..7a0d48bed --- /dev/null +++ b/http/tests/functional/route.rs @@ -0,0 +1,151 @@ +//! Tests for the [`route!`] macro. + +use heph::test::block_on; +use heph_http::body::{EmptyBody, OneshotBody}; +use heph_http::{route, Headers, Method, Request, Response, Version}; + +async fn route(request: Request) -> Response> { + route!(match request { + GET | HEAD "/" => index, + GET "/test1" => handlers::get, + HEAD "/test1" => handlers::head, + POST "/test1" => handlers::post, + PUT "/test1" => handlers::put, + DELETE "/test1" => handlers::delete, + CONNECT "/test1" => handlers::connect, + OPTIONS "/test1" => handlers::options, + TRACE "/test1" => handlers::trace, + PATCH "/test1" => handlers::patch, + + POST "/test2" => handlers::post, + _ => handlers::not_found, + }) +} + +async fn index(request: Request) -> Response> { + assert!(matches!(request.method(), Method::Get | Method::Head)); + assert_eq!(request.path(), "/"); + Response::ok().with_body("index".into()) +} + +mod handlers { + use heph_http::body::OneshotBody; + use heph_http::{Method, Request, Response}; + + pub async fn get(request: Request) -> Response> { + assert!(matches!(request.method(), Method::Get)); + assert_eq!(request.path(), "/test1"); + Response::ok().with_body("GET".into()) + } + + pub async fn head(request: Request) -> Response> { + assert!(matches!(request.method(), Method::Head)); + assert_eq!(request.path(), "/test1"); + Response::ok().with_body("HEAD".into()) + } + + pub async fn post(request: Request) -> Response> { + assert!(matches!(request.method(), Method::Post)); + assert_eq!(request.path(), "/test1"); + Response::ok().with_body("POST".into()) + } + + pub async fn put(request: Request) -> Response> { + assert!(matches!(request.method(), Method::Put)); + assert_eq!(request.path(), "/test1"); + Response::ok().with_body("PUT".into()) + } + + pub async fn delete(request: Request) -> Response> { + assert!(matches!(request.method(), Method::Delete)); + assert_eq!(request.path(), "/test1"); + Response::ok().with_body("DELETE".into()) + } + + pub async fn connect(request: Request) -> Response> { + assert!(matches!(request.method(), Method::Connect)); + assert_eq!(request.path(), "/test1"); + Response::ok().with_body("CONNECT".into()) + } + + pub async fn options(request: Request) -> Response> { + assert!(matches!(request.method(), Method::Options)); + assert_eq!(request.path(), "/test1"); + Response::ok().with_body("OPTIONS".into()) + } + + pub async fn trace(request: Request) -> Response> { + assert!(matches!(request.method(), Method::Trace)); + assert_eq!(request.path(), "/test1"); + Response::ok().with_body("TRACE".into()) + } + + pub async fn patch(request: Request) -> Response> { + assert!(matches!(request.method(), Method::Patch)); + assert_eq!(request.path(), "/test1"); + Response::ok().with_body("PATCH".into()) + } + + pub async fn not_found(_: Request) -> Response> { + Response::not_found().with_body("not found".into()) + } +} + +#[test] +fn multiple_methods_same_route() { + block_on(async move { + let tests = [Request::get("/".to_owned()), Request::head("/".to_owned())]; + for test_request in tests { + let response = route(test_request).await; + assert_eq!(response.body(), "index") + } + }); +} + +#[test] +fn correct_routing_based_on_method() { + block_on(async move { + let methods = [ + Method::Options, + Method::Get, + Method::Post, + Method::Put, + Method::Delete, + Method::Head, + Method::Trace, + Method::Connect, + Method::Patch, + ]; + for method in methods { + let request = Request::new( + method, + "/test1".to_string(), + Version::Http11, + Headers::EMPTY, + EmptyBody, + ); + let response = route(request).await; + assert_eq!(response.body(), method.as_str()) + } + }); +} + +#[test] +fn not_found_fallback() { + block_on(async move { + let tests = [ + // Unknown path. + Request::get("/unknown".to_owned()), + // Wrong method. + Request::get("/test2".to_owned()), + ]; + for test_request in tests { + let response = route(test_request).await; + assert_eq!(response.body(), "not found") + } + }); +} + +// TODO: test compile failure with the following errors: +// * Not a valid method. +// * Same method & path used twice (not implemented yet).