-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Macro to do simple routing of a request to the correct.
- Loading branch information
1 parent
3d5e56b
commit 8e6eda2
Showing
5 changed files
with
451 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.