Skip to content

Commit

Permalink
Add route! macro
Browse files Browse the repository at this point in the history
Macro to do simple routing of a request to the correct.
  • Loading branch information
Thomasdezeeuw committed Oct 3, 2021
1 parent 3d5e56b commit 8e6eda2
Show file tree
Hide file tree
Showing 5 changed files with 451 additions and 0 deletions.
131 changes: 131 additions & 0 deletions http/examples/route.rs
Original file line number Diff line number Diff line change
@@ -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<NA> Supervisor<NA> for ServerSupervisor
where
NA: NewActor<Argument = (), Error = io::Error>,
NA::Actor: Actor<Error = http::server::Error<!>>,
{
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<!, ThreadLocal>,
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<B>(_req: Request<B>) -> Response<OneshotBody<'static>> {
Response::ok().with_body("Index".into())
}

async fn other_page<B>(_req: Request<B>) -> Response<OneshotBody<'static>> {
Response::ok().with_body("Other page!".into())
}

async fn post<B>(_req: Request<B>) -> Response<OneshotBody<'static>> {
Response::ok().with_body("POST".into())
}

async fn not_found<B>(_req: Request<B>) -> Response<OneshotBody<'static>> {
Response::not_found().with_body("Page not found".into())
}
1 change: 1 addition & 0 deletions http/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ pub mod client;
pub mod head;
mod request;
mod response;
mod route;
pub mod server;

#[doc(no_inline)]
Expand Down
167 changes: 167 additions & 0 deletions http/src/route.rs
Original file line number Diff line number Diff line change
@@ -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<B>(request: Request<B>) {
/// 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<B>(_: Request<B>) { }
/// # async fn some_route_handler<B>(_: Request<B>) { }
/// # async fn create_pet<B>(_: Request<B>) { }
/// # async fn not_found<B>(_: Request<B>) { }
/// ```
///
/// 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<B>` 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<B>`, with the same body
/// `B` for all handlers. The handlers must be async functions, i.e. functions
/// that return a `Future<Output=Response<B>>`. 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 }};
}
1 change: 1 addition & 0 deletions http/tests/functional.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ mod functional {
mod header;
mod message;
mod method;
mod route;
mod server;
mod status_code;
mod version;
Expand Down
Loading

0 comments on commit 8e6eda2

Please sign in to comment.