Skip to content

Commit

Permalink
feat: add support for async handlers (#192)
Browse files Browse the repository at this point in the history
* feat: add support for async handlers

* reduce state clone

* rename copied code to state

* add test for read bytes
  • Loading branch information
elcharitas authored Sep 14, 2024
1 parent e1b3751 commit 195b632
Show file tree
Hide file tree
Showing 5 changed files with 125 additions and 47 deletions.
72 changes: 43 additions & 29 deletions crates/shared/src/core/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use http::Request;
use http_body_util::Full;
use std::sync::Arc;

use super::{Handler, RouteHandle};
use super::RouteHandler;
use crate::{
server::{context::AppState, Method, Middlewares, NgynContext, NgynResponse, Routes},
traits::{NgynController, NgynInterpreter, NgynMiddleware, NgynModule},
Expand Down Expand Up @@ -33,7 +33,7 @@ impl PlatformData {
let mut res = NgynResponse::default();

if let Some(state) = &self.state {
cx.state = Some(state.clone().into());
cx.state = Some(state.into());
}

let route_handler = self
Expand All @@ -53,7 +53,12 @@ impl PlatformData {

// execute controlled route if it is handled
if let Some(route_handler) = route_handler {
route_handler(&mut cx, &mut res);
match route_handler.as_ref() {
RouteHandler::Sync(handler) => handler(&mut cx, &mut res),
RouteHandler::Async(async_handler) => {
async_handler(&mut cx, &mut res).await;
}
}
cx.execute(&mut res).await;
// if the request method is HEAD, we should not return a body
// even if the route handler has set a body
Expand All @@ -77,7 +82,12 @@ impl PlatformData {
/// * `path` - The path of the route.
/// * `method` - The HTTP method of the route.
/// * `handler` - The handler function for the route.
pub(self) fn add_route(&mut self, path: String, method: Option<Method>, handler: Box<Handler>) {
pub(self) fn add_route(
&mut self,
path: String,
method: Option<Method>,
handler: Box<RouteHandler>,
) {
self.routes.push((path, method, handler));
}

Expand Down Expand Up @@ -125,43 +135,43 @@ pub trait NgynEngine: NgynPlatform {
/// let mut engine = MyEngine::default();
/// engine.route('/', Method::GET, Box::new(|_, _| {}));
/// ```
fn route(&mut self, path: &str, method: Method, handler: Box<Handler>) {
fn route(&mut self, path: &str, method: Method, handler: impl Into<RouteHandler>) {
self.data_mut()
.add_route(path.to_string(), Some(method), handler);
.add_route(path.to_string(), Some(method), Box::new(handler.into()));
}

fn any(&mut self, path: &str, handler: impl RouteHandle) {
fn any(&mut self, path: &str, handler: impl Into<RouteHandler>) {
self.data_mut()
.add_route(path.to_string(), None, handler.into());
.add_route(path.to_string(), None, Box::new(handler.into()));
}

/// Adds a new route to the `NgynApplication` with the `Method::Get`.
fn get(&mut self, path: &str, handler: impl RouteHandle) {
fn get(&mut self, path: &str, handler: impl Into<RouteHandler>) {
self.route(path, Method::GET, handler.into())
}

/// Adds a new route to the `NgynApplication` with the `Method::Post`.
fn post(&mut self, path: &str, handler: impl RouteHandle) {
fn post(&mut self, path: &str, handler: impl Into<RouteHandler>) {
self.route(path, Method::POST, handler.into())
}

/// Adds a new route to the `NgynApplication` with the `Method::Put`.
fn put(&mut self, path: &str, handler: impl RouteHandle) {
fn put(&mut self, path: &str, handler: impl Into<RouteHandler>) {
self.route(path, Method::PUT, handler.into())
}

/// Adds a new route to the `NgynApplication` with the `Method::Delete`.
fn delete(&mut self, path: &str, handler: impl RouteHandle) {
fn delete(&mut self, path: &str, handler: impl Into<RouteHandler>) {
self.route(path, Method::DELETE, handler.into())
}

/// Adds a new route to the `NgynApplication` with the `Method::Patch`.
fn patch(&mut self, path: &str, handler: impl RouteHandle) {
fn patch(&mut self, path: &str, handler: impl Into<RouteHandler>) {
self.route(path, Method::PATCH, handler.into())
}

/// Adds a new route to the `NgynApplication` with the `Method::Head`.
fn head(&mut self, path: &str, handler: impl RouteHandle) {
fn head(&mut self, path: &str, handler: impl Into<RouteHandler>) {
self.route(path, Method::HEAD, handler.into())
}

Expand Down Expand Up @@ -210,15 +220,13 @@ pub trait NgynEngine: NgynPlatform {
/// * `controller` - The arc'd controller to load.
fn load_controller(&mut self, controller: Arc<Box<dyn NgynController + 'static>>) {
for (path, http_method, handler) in controller.routes() {
let controller = controller.clone();
self.route(
path.as_str(),
Method::from_bytes(http_method.as_bytes()).unwrap_or_default(),
Box::new({
Box::new(move |cx: &mut NgynContext, _res: &mut NgynResponse| {
let controller = controller.clone();
move |cx: &mut NgynContext, _res: &mut NgynResponse| {
let controller = controller.clone();
cx.prepare(controller, handler.clone());
}
cx.prepare(controller, handler.clone());
}),
);
}
Expand All @@ -235,7 +243,7 @@ pub trait NgynEngine: NgynPlatform {

#[cfg(test)]
mod tests {
use crate::traits::NgynInjectable;
use crate::{core::Handler, traits::NgynInjectable};
use std::any::Any;

use super::*;
Expand Down Expand Up @@ -378,9 +386,11 @@ mod tests {
async fn test_respond_with_route_handler() {
let mut engine = MockEngine::default();
let handler: Box<Handler> = Box::new(|_, _| {});
engine
.data_mut()
.add_route("/test".to_string(), Some(Method::GET), handler);
engine.data_mut().add_route(
"/test".to_string(),
Some(Method::GET),
Box::new(RouteHandler::Sync(handler)),
);

let req = Request::builder()
.method(Method::GET)
Expand Down Expand Up @@ -430,9 +440,11 @@ mod tests {
async fn test_respond_with_head_method() {
let mut engine = MockEngine::default();
let handler: Box<Handler> = Box::new(|_, _| {});
engine
.data_mut()
.add_route("/test".to_string(), Some(Method::GET), handler);
engine.data_mut().add_route(
"/test".to_string(),
Some(Method::GET),
Box::new(RouteHandler::Sync(handler)),
);

let req = Request::builder()
.method(Method::HEAD)
Expand All @@ -449,9 +461,11 @@ mod tests {
async fn test_add_route() {
let mut engine = MockEngine::default();
let handler: Box<Handler> = Box::new(|_, _| {});
engine
.data_mut()
.add_route("/test".to_string(), Some(Method::GET), handler);
engine.data_mut().add_route(
"/test".to_string(),
Some(Method::GET),
Box::new(RouteHandler::Sync(handler)),
);

assert_eq!(engine.data.routes.len(), 1);
}
Expand Down
79 changes: 67 additions & 12 deletions crates/shared/src/core/handler.rs
Original file line number Diff line number Diff line change
@@ -1,21 +1,36 @@
use crate::server::{NgynContext, NgynResponse, ToBytes};
use std::{future::Future, pin::Pin};

use crate::{
server::{NgynContext, NgynResponse, ToBytes},
traits::NgynMiddleware,
};

/// Represents a handler function that takes in a mutable reference to `NgynContext` and `NgynResponse`.
pub type Handler = dyn Fn(&mut NgynContext, &mut NgynResponse) + Send + Sync + 'static;

/// Represents a trait for converting a type into a `Handler` trait object.
pub trait RouteHandle: Send + Sync {
/// Converts the implementing type into a `Handler` trait object.
fn into(self) -> Box<Handler>;
pub type AsyncHandler = Box<
dyn for<'a, 'b> Fn(
&'a mut NgynContext,
&'b mut NgynResponse,
) -> Pin<Box<dyn Future<Output = ()> + Send + 'b>>
+ Send
+ Sync,
>;

pub enum RouteHandler {
Sync(Box<Handler>),
Async(AsyncHandler),
}

impl<F: Fn(&mut NgynContext, &mut NgynResponse) + Send + Sync + 'static> From<F> for RouteHandler {
fn from(f: F) -> Self {
RouteHandler::Sync(Box::new(f))
}
}

impl<F> RouteHandle for F
where
F: Fn(&mut NgynContext, &mut NgynResponse) + Send + Sync + 'static,
{
/// Converts the implementing function into a `Handler` trait object.
fn into(self) -> Box<Handler> {
Box::new(self)
impl From<AsyncHandler> for RouteHandler {
fn from(f: AsyncHandler) -> Self {
RouteHandler::Async(Box::new(f))
}
}

Expand All @@ -39,3 +54,43 @@ pub fn handler<S: ToBytes + 'static>(
*res.body_mut() = body.into();
})
}

/// Creates a `AsyncHandler` trait object from an async function that takes in a mutable reference to `NgynContext` and returns a future with output that implements `ToBytes`.
///
/// ### Example
/// ```rust ignore
/// use ngyn::server::{async_handler, NgynContext, ToBytes};
///
/// app.get("/hello", async_handler(async |ctx: &mut NgynContext| {
/// "Hello, World!"
/// }));
/// ```
pub fn async_handler<S: ToBytes + 'static, Fut: Future<Output = S> + Send + 'static>(
f: impl Fn(&mut NgynContext) -> Fut + Send + Sync + 'static,
) -> AsyncHandler {
Box::new(move |ctx: &mut NgynContext, res: &mut NgynResponse| {
let fut = f(ctx);
Box::pin(async move {
let body = fut.await.to_bytes();
*res.body_mut() = body.into();
})
})
}

pub fn middleware<M: NgynMiddleware + 'static>(middleware: M) -> Box<Handler> {
Box::new(move |ctx: &mut NgynContext, res: &mut NgynResponse| {
middleware.handle(ctx, res);
})
}

pub fn with_middlewares<M: NgynMiddleware + 'static>(
middleware: Vec<M>,
handler: Box<Handler>,
) -> impl Into<RouteHandler> {
Box::new(move |ctx: &mut NgynContext, res: &mut NgynResponse| {
for m in &middleware {
m.handle(ctx, res);
}
handler(ctx, res);
})
}
10 changes: 5 additions & 5 deletions crates/shared/src/server/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,14 @@ impl<T: AppState> AppState for Box<T> {
}
}

impl From<Arc<Box<dyn AppState>>> for Box<dyn AppState> {
fn from(value: Arc<Box<dyn AppState>>) -> Self {
impl From<&Arc<Box<dyn AppState>>> for Box<dyn AppState> {
fn from(value: &Arc<Box<dyn AppState>>) -> Self {
let arc_clone = value.clone();
let controller_ref: &dyn AppState = &**arc_clone;
let state_ref: &dyn AppState = &**arc_clone;

let controller_ptr: *const dyn AppState = controller_ref as *const dyn AppState;
let state_ptr: *const dyn AppState = state_ref as *const dyn AppState;

let nn_ptr = std::ptr::NonNull::new(controller_ptr as *mut dyn AppState).unwrap();
let nn_ptr = std::ptr::NonNull::new(state_ptr as *mut dyn AppState).unwrap();
let raw_ptr = nn_ptr.as_ptr();

unsafe { Box::from_raw(raw_ptr) }
Expand Down
2 changes: 1 addition & 1 deletion crates/shared/src/server/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,5 @@ pub use transformer::{Body, Param, Query, Transducer, Transformer};
pub type NgynRequest = http::Request<Vec<u8>>;
pub type NgynResponse = http::Response<Full<Bytes>>;

pub(crate) type Routes = Vec<(String, Option<Method>, Box<crate::core::Handler>)>;
pub(crate) type Routes = Vec<(String, Option<Method>, Box<crate::core::RouteHandler>)>;
pub(crate) type Middlewares = Vec<Box<dyn crate::traits::NgynMiddleware>>;
9 changes: 9 additions & 0 deletions crates/shared/src/server/response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,16 @@ mod tests {
};

response.peek_bytes(peek_fn).await;
assert_eq!(bytes, body);
}

#[tokio::test]
async fn test_read_bytes() {
let mut response = NgynResponse::default();
let body = Bytes::from("Hello, world!");
*response.body_mut() = body.clone().into();

let bytes = response.read_bytes().await.unwrap();
assert_eq!(bytes, body);
}
}

0 comments on commit 195b632

Please sign in to comment.