Skip to content

Commit

Permalink
Merge branch 'dev' into feat/cli-v1
Browse files Browse the repository at this point in the history
  • Loading branch information
elcharitas committed Sep 29, 2024
2 parents 86ece37 + 6e40dc3 commit 092f171
Show file tree
Hide file tree
Showing 11 changed files with 157 additions and 53 deletions.
2 changes: 1 addition & 1 deletion crates/core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ pub mod prelude {
pub use crate::macros::*;
pub use ngyn_hyper::HyperApplication;
pub use ngyn_shared::{
core::{handler, NgynEngine},
core::{handler::*, engine::NgynEngine},
server::{
Body, JsonResponse, JsonResult, NgynContext, NgynRequest, NgynResponse, Param, Query,
Transducer,
Expand Down
2 changes: 1 addition & 1 deletion crates/macros/src/common/controller.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ pub(crate) fn controller_macro(args: TokenStream, input: TokenStream) -> TokenSt
quote! {
let mut middleware = #m::default();
middleware.inject(cx);
middleware.handle(cx, res);
middleware.handle(cx, res).await;
}
})
.collect();
Expand Down
18 changes: 9 additions & 9 deletions crates/shared/src/core/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,16 @@ use http::Request;
use http_body_util::Full;
use std::sync::Arc;

use super::RouteHandler;
use super::handler::RouteHandler;
use crate::{
server::{context::AppState, Method, Middlewares, NgynContext, NgynResponse, Routes},
traits::{NgynController, NgynInterpreter, NgynMiddleware, NgynModule},
server::{context::AppState, Method, NgynContext, NgynResponse},
traits::{Middleware, NgynController, NgynInterpreter, NgynMiddleware, NgynModule},
};

#[derive(Default)]
pub struct PlatformData {
routes: Routes,
middlewares: Middlewares,
routes: Vec<(String, Option<Method>, Box<RouteHandler>)>,
middlewares: Vec<Box<dyn crate::traits::Middleware>>,
interpreters: Vec<Box<dyn NgynInterpreter>>,
state: Option<Arc<Box<dyn AppState>>>,
}
Expand Down Expand Up @@ -48,7 +48,7 @@ impl PlatformData {

// trigger global middlewares
for middleware in &self.middlewares {
middleware.handle(&mut cx, &mut res);
middleware.run(&mut cx, &mut res).await;
}

// execute controlled route if it is handled
Expand Down Expand Up @@ -96,7 +96,7 @@ impl PlatformData {
/// ### Arguments
///
/// * `middleware` - The middleware to add.
pub(self) fn add_middleware(&mut self, middleware: Box<dyn NgynMiddleware>) {
pub(self) fn add_middleware(&mut self, middleware: Box<dyn Middleware>) {
self.middlewares.push(middleware);
}

Expand Down Expand Up @@ -243,7 +243,7 @@ pub trait NgynEngine: NgynPlatform {

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

use super::*;
Expand Down Expand Up @@ -272,7 +272,7 @@ mod tests {
}

impl NgynMiddleware for MockMiddleware {
fn handle(&self, _cx: &mut NgynContext, _res: &mut NgynResponse) {}
async fn handle(&self, _cx: &mut NgynContext, _res: &mut NgynResponse) {}
}

struct MockInterpreter;
Expand Down
64 changes: 45 additions & 19 deletions crates/shared/src/core/handler.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
use std::{future::Future, pin::Pin};

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

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

/// 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;
pub(crate) type Handler = dyn Fn(&mut NgynContext, &mut NgynResponse) + Send + Sync + 'static;

pub type AsyncHandler = Box<
pub(crate) type AsyncHandler = Box<
dyn for<'a, 'b> Fn(
&'a mut NgynContext,
&'b mut NgynResponse,
Expand All @@ -30,7 +29,7 @@ impl<F: Fn(&mut NgynContext, &mut NgynResponse) + Send + Sync + 'static> From<F>

impl From<AsyncHandler> for RouteHandler {
fn from(f: AsyncHandler) -> Self {
RouteHandler::Async(Box::new(f))
RouteHandler::Async(f)
}
}

Expand Down Expand Up @@ -77,20 +76,47 @@ pub fn async_handler<S: ToBytes + 'static, Fut: Future<Output = S> + Send + 'sta
})
}

pub fn middleware<M: NgynMiddleware + 'static>(middleware: M) -> Box<Handler> {
Box::new(move |ctx: &mut NgynContext, res: &mut NgynResponse| {
middleware.handle(ctx, res);
/// Create a not-implemented handler that returns a `501 Not Implemented` status code.
///
/// This is very similar to unimplemented! macro in Rust.
pub fn not_implemented() -> Box<Handler> {
Box::new(|_ctx: &mut NgynContext, res: &mut NgynResponse| {
*res.status_mut() = StatusCode::NOT_IMPLEMENTED;
})
}

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);
/// Redirects to a specified location with a `303 See Other` status code.
pub fn redirect_to(location: &'static str) -> Box<Handler> {
Box::new(|_ctx: &mut NgynContext, res: &mut NgynResponse| {
res.headers_mut()
.insert("Location", HeaderValue::from_str(location).unwrap());
*res.status_mut() = StatusCode::SEE_OTHER;
})
}

/// Redirects to a specified location with a `307 Temporary Redirect` status code.
pub fn redirect_temporary(location: &'static str) -> Box<Handler> {
Box::new(|_ctx: &mut NgynContext, res: &mut NgynResponse| {
res.headers_mut()
.insert("Location", HeaderValue::from_str(location).unwrap());
*res.status_mut() = StatusCode::TEMPORARY_REDIRECT;
})
}

/// Redirects to a specified location with a `301 Moved Permanently` status code.
pub fn redirect_permanent(location: &'static str) -> Box<Handler> {
Box::new(|_ctx: &mut NgynContext, res: &mut NgynResponse| {
res.headers_mut()
.insert("Location", HeaderValue::from_str(location).unwrap());
*res.status_mut() = StatusCode::MOVED_PERMANENTLY;
})
}

/// Redirects to a specified location with a `302 Found` status code.
pub fn redirect_found(location: &'static str) -> Box<Handler> {
Box::new(|_ctx: &mut NgynContext, res: &mut NgynResponse| {
res.headers_mut()
.insert("Location", HeaderValue::from_str(location).unwrap());
*res.status_mut() = StatusCode::FOUND;
})
}
7 changes: 2 additions & 5 deletions crates/shared/src/core/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,2 @@
mod engine;
mod handler;

pub use engine::*;
pub use handler::*;
pub mod engine;
pub mod handler;
13 changes: 10 additions & 3 deletions crates/shared/src/server/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,19 @@ impl<T: AppState> AppState for Box<T> {
}
}

/// # Panics
/// Panics if the state has been dropped. This should never happen unless the state is dropped manually.
impl From<&Arc<Box<dyn AppState>>> for Box<dyn AppState> {
fn from(value: &Arc<Box<dyn AppState>>) -> Self {
// creating a clone is essential since this ref will be dropped after this function returns
let arc_clone = value.clone();
let state_ref: &dyn AppState = &**arc_clone;

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

let nn_ptr = std::ptr::NonNull::new(state_ptr as *mut dyn AppState).unwrap();
// SAFETY: state_ptr is not null, it is safe to convert it to a NonNull pointer, this way we can safely convert it back to a Box
let nn_ptr = std::ptr::NonNull::new(state_ptr as *mut dyn AppState)
.expect("State has been dropped, ensure it is being cloned correctly."); // This should never happen, if it does, it's a bug
let raw_ptr = nn_ptr.as_ptr();

unsafe { Box::from_raw(raw_ptr) }
Expand Down Expand Up @@ -448,12 +453,14 @@ impl NgynContext {
/// context.execute(&mut response).await;
/// ```
pub(crate) async fn execute(&mut self, res: &mut NgynResponse) {
// safely consume the route information, it will be set again if needed
let (handler, controller) = match self.route_info.take() {
Some((handler, ctrl)) => (handler, ctrl),
None => return,
};
let mut controller =
ManuallyDrop::<Box<dyn NgynController>>::new(controller.clone().into());
// allow the controller to live even after the request is handled, until the server is stopped or crashes or in weird cases, the controller is dropped.
// If the controller is dropped, the server will panic.
let mut controller = ManuallyDrop::<Box<dyn NgynController>>::new(controller.into()); // panics if the controller has been dropped
controller.handle(&handler, self, res).await;
}
}
Expand Down
3 changes: 0 additions & 3 deletions crates/shared/src/server/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,3 @@ 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::RouteHandler>)>;
pub(crate) type Middlewares = Vec<Box<dyn crate::traits::NgynMiddleware>>;
66 changes: 58 additions & 8 deletions crates/shared/src/traits/controller_trait.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,18 +31,68 @@ pub trait NgynController: NgynInjectable + Sync + Send {
}

/// In Ngyn, controllers are stored as `Arc<Box<dyn NgynController>>`.
/// This is because controllers are shared across threads and need to be cloned easily.
/// And we do this because controllers are shared across threads and an arc guarantees
/// that the controller is not dropped until all references to it are dropped.
///
/// Here's how we convert an `Arc<Box<dyn NgynController>>` to a `Box<dyn NgynController>`.
/// This conversion allows us to mutably borrow the controller and handle routing logic.
/// When working with controllers, you'd quickly notice that Ngyn allows you to define routes that require mutable access to the controller.
/// For instance, take this sample controller:
/// ```rust ignore
/// #[controller]
/// struct TestController;
///
/// #[routes]
/// impl TestController {
/// #[get("/")]
/// async fn index(&mut self) -> String {
/// "Hello, World!".to_string()
/// }
/// }
/// ```
///
/// In the above example, the `index` method requires mutable access to the controller. This pattern, though not encouraged (check app states), is allowed in Ngyn.
/// You could for instance create a localized state in the controller that is only accessible to the controller and its routes.
/// The way Ngyn allows this without performance overhead is through a specialized `Arc -> Box` conversion that only works so well becasue of how Ngyn is designed.
///
/// HOW DOES IT WORK?
///
/// ```text
/// +-----------------+ +-----------------+ +-----------------+
/// | Arc<Box<Ctrl>> | | Arc<Box<Ctrl>> | | Arc<Box<Ctrl>> |
/// +-----------------+ +-----------------+ +-----------------+
/// | | |
/// +-----------------+ +-----------------+ +-----------------+
/// | &Box<Ctrl> | | &Box<Ctrl> | | &Box<Ctrl> |
/// +-----------------+ +-----------------+ +-----------------+
/// | | |
/// +-----------------+ +-----------------+ +-----------------+
/// | &mut Ctrl | | &mut Ctrl | | &mut Ctrl |
/// +-----------------+ +-----------------+ +-----------------+
/// | | |
/// +-----------------+ +-----------------+ +-----------------+
/// | *mut Ctrl | | *mut Ctrl | | *mut Ctrl |
/// +-----------------+ +-----------------+ +-----------------+
/// | | |
/// +-----------------+ +-----------------+ +-----------------+
/// | Box<Ctrl> | | Box<Ctrl> | | Box<Ctrl> |
/// +-----------------+ +-----------------+ +-----------------+
///
/// ```
///
///
/// When a controller is created, we box it and then wrap it in an Arc. This way, the controller is converted to a trait object and can be shared across threads.
/// The trait object is what allows us to call the controller's methods from the server. But when we need mutable access to the controller, we convert it back to a Box.
/// Rather than making use of a mutex, what we do is get the raw pointer of the initial controller, ensure it's not null, and then convert it back to a Box.
///
/// # Panics
/// Panics if the controller has been dropped. This should never happen unless the controller is dropped manually.
impl From<Arc<Box<dyn NgynController>>> for Box<dyn NgynController> {
fn from(arc: Arc<Box<dyn NgynController>>) -> Self {
let arc_clone = arc.clone();
let controller_ref: &dyn NgynController = &**arc_clone;

fn from(controller_arc: Arc<Box<dyn NgynController>>) -> Self {
let controller_ref: &dyn NgynController = &**controller_arc;
let controller_ptr: *const dyn NgynController = controller_ref as *const dyn NgynController;

let nn_ptr = NonNull::new(controller_ptr as *mut dyn NgynController).unwrap();
// SAFETY: controller_ptr is not null, it is safe to convert it to a NonNull pointer, this way we can safely convert it back to a Box
let nn_ptr = NonNull::new(controller_ptr as *mut dyn NgynController)
.expect("Controller has been dropped, ensure it is being cloned correctly.");
let raw_ptr = nn_ptr.as_ptr();

unsafe { Box::from_raw(raw_ptr) }
Expand Down
31 changes: 29 additions & 2 deletions crates/shared/src/traits/middleware_trait.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::{future::Future, pin::Pin};

use crate::{
server::{NgynContext, NgynResponse},
traits::NgynInjectable,
Expand Down Expand Up @@ -29,12 +31,37 @@ use crate::{
/// }
///
/// impl NgynMiddleware for RequestReceivedLogger {
/// fn handle(&self, cx: &mut NgynContext, res: &mut NgynResponse) {
/// async fn handle(&self, cx: &mut NgynContext, res: &mut NgynResponse) {
/// println!("Request received: {:?}", cx.request());
/// }
/// }
/// ```
pub trait NgynMiddleware: NgynInjectable + Sync {
/// Handles the request.
fn handle(&self, cx: &mut NgynContext, res: &mut NgynResponse);
#[allow(async_fn_in_trait)]
fn handle(
&self,
cx: &mut NgynContext,
res: &mut NgynResponse,
) -> impl std::future::Future<Output = ()> + Send;
}

pub(crate) trait Middleware: NgynInjectable + Sync {
fn run<'a>(
&'a self,
_cx: &'a mut NgynContext,
_res: &'a mut NgynResponse,
) -> Pin<Box<dyn Future<Output = ()> + Send + 'a>> {
Box::pin(async move {})
}
}

impl<T: NgynMiddleware> Middleware for T {
fn run<'a>(
&'a self,
cx: &'a mut NgynContext,
res: &'a mut NgynResponse,
) -> Pin<Box<dyn Future<Output = ()> + Send + 'a>> {
Box::pin(self.handle(cx, res))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use serde_json::json;
pub struct NotFoundMiddleware;

impl NgynMiddleware for NotFoundMiddleware {
fn handle(&self, cx: &mut NgynContext, res: &mut NgynResponse) {
async fn handle(&self, cx: &mut NgynContext, res: &mut NgynResponse) {
if cx.is_valid_route() {
return;
}
Expand Down
2 changes: 1 addition & 1 deletion examples/weather_app/src/middlewares/test_middleware.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use ngyn::prelude::*;
pub struct TestMiddleware;

impl NgynMiddleware for TestMiddleware {
fn handle(&self, _cx: &mut NgynContext, _response: &mut NgynResponse) {
async fn handle(&self, _cx: &mut NgynContext, _response: &mut NgynResponse) {
println!("middleware works");
}
}

0 comments on commit 092f171

Please sign in to comment.