diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 455d0d32..6b9e5b2c 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -16,8 +16,13 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Install task command - run: sh -c "$(curl --location https://taskfile.dev/install.sh)" -- -d -b /usr/local/bin + - name: Setup mold + run: | + sudo apt install mold clang + echo '[target.x86_64-unknown-linux-gnu]' >> $HOME/.cargo/config.toml + echo 'linker = "clang"' >> $HOME/.cargo/config.toml + echo 'rustflags = ["-C", "link-arg=-fuse-ld=/usr/bin/mold"]' >> $HOME/.cargo/config.toml + cat $HOME/.cargo/config.toml - uses: actions-rs/toolchain@v1 with: @@ -25,15 +30,25 @@ jobs: profile: minimal override: true - - name: Cache cargo tools - id: cache_cargo_tools + - name: Cache cargo subcommands + id: cache_cargo_subcommands uses: actions/cache@v3 with: - key: ${{ runner.os }}-cargo-install + key: ${{ runner.os }}-cargo-install--sqlx-sccache path: ~/.cargo/bin - - name: Install cargo tools - if: ${{ steps.cache_cargo_tools.outputs.cache-hit != 'true' }} - run: cargo install sqlx-cli --no-default-features --features native-tls,postgres - + - name: Install cargo subcommands + if: ${{ steps.cache_cargo_subcommands.outputs.cache-hit != 'true' }} + run: | + cargo install sqlx-cli --no-default-features --features native-tls,postgres + cargo install sccache --locked + + - name: Setup sccache + run: | + echo '[build]' >> $HOME/.cargo/config.toml + echo "rustc-wrapper = \"$HOME/.cargo/bin/sccache\"" >> $HOME/.cargo/config.toml + cat $HOME/.cargo/config.toml + - name: Run tasks - run: task test check ${{ matrix.toolchain == 'nightly' && 'bench' || '' }} + run: | + sh -c "$(curl --location https://taskfile.dev/install.sh)" -- -d -b /usr/local/bin + task test check ${{ matrix.toolchain == 'nightly' && 'bench' || '' }} diff --git a/README.md b/README.md index dc262149..95e12863 100644 --- a/README.md +++ b/README.md @@ -32,14 +32,14 @@ tokio = { version = "1", features = ["full"] } ```rust,no_run use ohkami::prelude::*; -use ohkami::typed::status::{OK, NoContent}; +use ohkami::typed::status::{NoContent}; async fn health_check() -> NoContent { NoContent } -async fn hello(name: &str) -> OK { - OK(format!("Hello, {name}!")) +async fn hello(name: &str) -> String { + format!("Hello, {name}!") } #[tokio::main] @@ -233,7 +233,7 @@ async fn test_my_ohkami() { - [ ] WebSocket ## MSRV (Minimum Supported rustc Version) -Latest stable +Latest stable at the time of publication. ## License ohkami is licensed under MIT LICENSE ([LICENSE](https://github.com/kana-rus/ohkami/blob/main/LICENSE) or [https://opensource.org/licenses/MIT](https://opensource.org/licenses/MIT)). diff --git a/examples/quick_start/src/main.rs b/examples/quick_start/src/main.rs index 76209ae7..2dc505db 100644 --- a/examples/quick_start/src/main.rs +++ b/examples/quick_start/src/main.rs @@ -1,12 +1,12 @@ use ohkami::prelude::*; -use ohkami::typed::status::{OK, NoContent}; +use ohkami::typed::status::NoContent; async fn health_check() -> NoContent { NoContent } -async fn hello(name: &str) -> OK { - OK(format!("Hello, {name}!")) +async fn hello(name: &str) -> String { + format!("Hello, {name}!") } #[tokio::main] diff --git a/ohkami/src/fang/builtin/jwt.rs b/ohkami/src/fang/builtin/jwt.rs index 36597d70..dc8ff5e6 100644 --- a/ohkami/src/fang/builtin/jwt.rs +++ b/ohkami/src/fang/builtin/jwt.rs @@ -409,11 +409,14 @@ impl JWT { first_name: &'s str, familly_name: &'s str, } impl<'req> crate::FromRequest<'req> for SigninRequest<'req> { - type Error = crate::FromRequestError; + type Error = Response; fn from_request(req: &'req Request) -> Result { serde_json::from_slice( - req.payload().ok_or_else(|| crate::FromRequestError::Static("No payload found"))? - ).map_err(|e| crate::FromRequestError::Owned(e.to_string())) + req.payload().ok_or_else(|| Response::BadRequest().text("No payload found"))? + ).map_err(|e| { + eprintln!("Failed to deserialize: {}", e.to_string()); + Response::InternalServerError() + }) } } diff --git a/ohkami/src/fang/fangs.rs b/ohkami/src/fang/fangs.rs index 78775e93..29113afb 100644 --- a/ohkami/src/fang/fangs.rs +++ b/ohkami/src/fang/fangs.rs @@ -32,7 +32,7 @@ pub(crate) trait FrontFangCaller: Send + Sync { where Self: Sync + 'c; } impl FrontFangCaller for FF { - fn call<'c>(&'c self, req: &'c mut Request) -> Pin> + Send + 'c>> + #[inline(always)] fn call<'c>(&'c self, req: &'c mut Request) -> Pin> + Send + 'c>> where Self: Sync + 'c { Box::pin(self.bite(req)) @@ -70,7 +70,7 @@ pub(crate) trait BackFangCaller: Send + Sync { where Self: Sync + 'c; } impl BackFangCaller for BF { - fn call<'c>(&'c self, res: &'c mut Response, _req: &'c Request) -> Pin> + Send + 'c>> + #[inline(always)] fn call<'c>(&'c self, res: &'c mut Response, _req: &'c Request) -> Pin> + Send + 'c>> where Self: Sync + 'c { Box::pin(self.bite(res, _req)) diff --git a/ohkami/src/fang/mod.rs b/ohkami/src/fang/mod.rs index 423e9ab2..4ec93d01 100644 --- a/ohkami/src/fang/mod.rs +++ b/ohkami/src/fang/mod.rs @@ -79,7 +79,7 @@ pub(crate) mod proc { #[derive(Clone)] pub struct FrontFang(pub(super) Arc); impl FrontFang { - pub fn call<'c>(&'c self, req: &'c mut Request) -> Pin> + Send + 'c>> { + #[inline(always)] pub fn call<'c>(&'c self, req: &'c mut Request) -> Pin> + Send + 'c>> { self.0.call(req) } } @@ -87,7 +87,7 @@ pub(crate) mod proc { #[derive(Clone)] pub struct BackFang(pub(super) Arc); impl BackFang { - pub fn call<'c>(&'c self, res: &'c mut Response, req: &'c Request) -> Pin> + Send + 'c>> { + #[inline(always)] pub fn call<'c>(&'c self, res: &'c mut Response, req: &'c Request) -> Pin> + Send + 'c>> { self.0.call(res, req) } } diff --git a/ohkami/src/handler/handlers.rs b/ohkami/src/handler/handlers.rs index 3bd1df8f..d11f3ca0 100644 --- a/ohkami/src/handler/handlers.rs +++ b/ohkami/src/handler/handlers.rs @@ -29,7 +29,7 @@ macro_rules! Handlers { ($( $method:ident ),*) => { impl Handlers { $( - pub fn $method(mut self, handler: impl IntoHandler) -> Self { + pub fn $method(mut self, handler: impl IntoHandler) -> Self { self.$method.replace(handler.into_handler()); self } @@ -80,13 +80,13 @@ macro_rules! Route { /// ``` pub trait Route { $( - fn $method(self, handler: impl IntoHandler) -> Handlers; + fn $method(self, handler: impl IntoHandler) -> Handlers; )* fn By(self, another: Ohkami) -> ByAnother; } impl Route for &'static str { $( - fn $method(self, handler: impl IntoHandler) -> Handlers { + fn $method(self, handler: impl IntoHandler) -> Handlers { let mut handlers = Handlers::new(self); handlers.$method.replace(handler.into_handler()); handlers diff --git a/ohkami/src/handler/into_handler.rs b/ohkami/src/handler/into_handler.rs index 5c3c2b24..e61c5590 100644 --- a/ohkami/src/handler/into_handler.rs +++ b/ohkami/src/handler/into_handler.rs @@ -1,46 +1,41 @@ -use std::{future::Future, borrow::Cow}; +use std::{future::Future, pin::Pin}; use ohkami_lib::percent_decode_utf8; use super::Handler; -use crate::{ - Response, Status, - FromRequest, FromParam, Request, IntoResponse, -}; -#[cfg(feature="websocket")] -use crate::websocket::WebSocketContext; +use crate::{Response, FromRequest, FromParam, Request, IntoResponse}; -pub trait IntoHandler { +pub trait IntoHandler { fn into_handler(self) -> Handler; } -#[cold] #[inline(never)] fn __bad_request( - e: impl std::fmt::Display, -) -> std::pin::Pin>> { - Box::pin({ - let res = Response::with(Status::BadRequest).text(e.to_string()); - async {res.into()} - }) + +#[inline(never)] #[cold] fn __error__(e: Response) -> Pin + Send>> { + Box::pin(async {e}) } + #[inline(always)] fn from_param_bytes<'p, P: FromParam<'p>>( param_bytes_maybe_percent_encoded: &'p [u8] -) -> Result> { +) -> Result { let param = percent_decode_utf8(param_bytes_maybe_percent_encoded) - .map_err(|e| Cow::Owned(e.to_string()))?; + .map_err(|_e| { + #[cfg(debug_assertions)] eprintln!("Failed to decode percent encoding: {_e}"); + Response::InternalServerError() + })?;

::from_param(param) - .map_err(|e| Cow::Owned(e.to_string())) + .map_err(IntoResponse::into_response) } #[inline(always)] fn from_request<'fr, 'req, R: FromRequest<'fr>>( req: &'req Request -) -> Result>::Error> { +) -> Result { ::from_request(unsafe { - std::mem::transmute::<&'req _, &'fr _>(req) // - }) + std::mem::transmute::<&'req _, &'fr _>(req) + }).map_err(IntoResponse::into_response) } -const _: () = { - impl IntoHandlerBody> for F +const _: (/* no args */) = { + impl<'req, F, Body, Fut> IntoHandlerBody> for F where F: Fn() -> Fut + Send + Sync + 'static, Body: IntoResponse, @@ -58,33 +53,9 @@ const _: () = { }; const _: (/* FromParam */) = { - macro_rules! with_single_path_param { - ($( $param_type:ty ),*) => {$( - impl<'req, F, Body, Fut> IntoHandlerBody> for F - where - F: Fn($param_type) -> Fut + Send + Sync + 'static, - Body: IntoResponse, - Fut: Future + Send + 'static, - { - fn into_handler(self) -> Handler { - Handler::new(move |req| - match from_param_bytes(unsafe {req.path.assume_one_param()}) { - Ok(p1) => { - let res = self(p1); - Box::pin(async move {res.await.into_response()}) - } - Err(e) => __bad_request(e) - } - ) - } - } - )*}; - } with_single_path_param! { - String, u8, u16, u32, u64, u128, usize - } - impl<'req, F, Body, Fut> IntoHandlerBody> for F + impl<'req, F, Fut, Body, P1:FromParam<'req>> IntoHandlerBody> for F where - F: Fn(&'req str) -> Fut + Send + Sync + 'static, + F: Fn(P1) -> Fut + Send + Sync + 'static, Body: IntoResponse, Fut: Future + Send + 'static, { @@ -94,21 +65,14 @@ const _: (/* FromParam */) = { Ok(p1) => { let res = self(p1); Box::pin(async move {res.await.into_response()}) - }, - Err(e) => __bad_request(e) + } + Err(e) => __error__(e) } ) } } - #[cfg(test)] fn __() { - async fn h1(_param: String) -> Response {todo!()} - async fn h2(_param: &str) -> Response {todo!()} - - let _ = h1.into_handler(); - let _ = h2.into_handler(); - } - impl<'req, F, Body, Fut, P1:FromParam<'req>> IntoHandlerBody> for F + impl<'req, F, Body, Fut, P1:FromParam<'req>> IntoHandlerBody> for F where F: Fn((P1,)) -> Fut + Send + Sync + 'static, Body: IntoResponse, @@ -123,13 +87,13 @@ const _: (/* FromParam */) = { let res = self((p1,)); Box::pin(async move {res.await.into_response()}) } - Err(e) => __bad_request(e) + Err(e) => __error__(e) } ) } } - impl<'req, F, Fut, Body:IntoResponse, P1:FromParam<'req>, P2:FromParam<'req>> IntoHandlerBody> for F + impl<'req, F, Fut, Body:IntoResponse, P1:FromParam<'req>, P2:FromParam<'req>> IntoHandlerBody> for F where F: Fn((P1, P2)) -> Fut + Send + Sync + 'static, Fut: Future + Send + 'static, @@ -142,7 +106,7 @@ const _: (/* FromParam */) = { let res = self((p1, p2)); Box::pin(async move {res.await.into_response()}) } - (Err(e), _) | (_, Err(e)) => __bad_request(e), + (Err(e), _) | (_, Err(e)) => __error__(e), } }) } @@ -162,7 +126,7 @@ const _: (/* FromRequest items */) = { let res = self(item1); Box::pin(async move {res.await.into_response()}) } - Err(e) => __bad_request(e) + Err(e) => __error__(e) } ) } @@ -180,8 +144,8 @@ const _: (/* FromRequest items */) = { let res = self(item1, item2); Box::pin(async move {res.await.into_response()}) } - (Err(e), _) => __bad_request(e), - (_, Err(e)) => __bad_request(e), + (Err(e), _) | + (_, Err(e)) => __error__(e), } ) } @@ -199,92 +163,86 @@ const _: (/* FromRequest items */) = { let res = self(item1, item2, item3); Box::pin(async move {res.await.into_response()}) } - (Err(e), _, _) => __bad_request(e), - (_, Err(e), _) => __bad_request(e), - (_, _, Err(e)) => __bad_request(e), + (Err(e), _, _) | + (_, Err(e), _) | + (_, _, Err(e)) => __error__(e), } ) } } }; -const _: (/* single FromParam and FromRequest items */) = { - macro_rules! with_single_path_param_and_from_request_items { - ($( $param_type:ty ),*) => {$( - impl<'req, F, Fut, Body:IntoResponse, Item1:FromRequest<'req>> IntoHandlerBody> for F - where - F: Fn($param_type, Item1) -> Fut + Send + Sync + 'static, - Fut: Future + Send + 'static, - { - fn into_handler(self) -> Handler { - Handler::new(move |req| { - // SAFETY: Due to the architecture of `Router`, - // `params` has already `append`ed once before this code - let p1 = unsafe {req.path.assume_one_param()}; - - match (from_param_bytes(p1), from_request(req)) { - (Ok(p1), Ok(item1)) => { - let res = self(p1, item1); - Box::pin(async move {res.await.into_response()}) - }, - (Err(e), _) => __bad_request(e), - (_, Err(e)) => __bad_request(e), - } - }) +const _: (/* one FromParam without tuple and FromRequest items */) = { + impl<'req, F, Fut, Body:IntoResponse, P1:FromParam<'req>, Item1:FromRequest<'req>> IntoHandlerBody> for F + where + F: Fn(P1, Item1) -> Fut + Send + Sync + 'static, + Fut: Future + Send + 'static, + { + fn into_handler(self) -> Handler { + Handler::new(move |req| { + // SAFETY: Due to the architecture of `Router`, + // `params` has already `append`ed once before this code + let p1 = unsafe {req.path.assume_one_param()}; + + match (from_param_bytes(p1), from_request(req)) { + (Ok(p1), Ok(item1)) => { + let res = self(p1, item1); + Box::pin(async move {res.await.into_response()}) + }, + (Err(e), _) | + (_, Err(e)) => __error__(e), } - } - - impl<'req, F, Fut, Body:IntoResponse, Item1:FromRequest<'req>, Item2:FromRequest<'req>> IntoHandlerBody> for F - where - F: Fn($param_type, Item1, Item2) -> Fut + Send + Sync + 'static, - Fut: Future + Send + 'static, - { - fn into_handler(self) -> Handler { - Handler::new(move |req| { - // SAFETY: Due to the architecture of `Router`, - // `params` has already `append`ed once before this code - let p1 = unsafe {req.path.assume_one_param()}; - - match (from_param_bytes(p1), from_request::(req), from_request::(req)) { - (Ok(p1), Ok(item1), Ok(item2)) => { - let res = self(p1, item1, item2); - Box::pin(async move {res.await.into_response()}) - } - (Err(e),_,_) => __bad_request(e), - (_,Err(e),_) => __bad_request(e), - (_,_,Err(e)) => __bad_request(e), - } - }) + }) + } + } + + impl<'req, F, Fut, Body:IntoResponse, P1:FromParam<'req>, Item1:FromRequest<'req>, Item2:FromRequest<'req>> IntoHandlerBody> for F + where + F: Fn(P1, Item1, Item2) -> Fut + Send + Sync + 'static, + Fut: Future + Send + 'static, + { + fn into_handler(self) -> Handler { + Handler::new(move |req| { + // SAFETY: Due to the architecture of `Router`, + // `params` has already `append`ed once before this code + let p1 = unsafe {req.path.assume_one_param()}; + + match (from_param_bytes(p1), from_request::(req), from_request::(req)) { + (Ok(p1), Ok(item1), Ok(item2)) => { + let res = self(p1, item1, item2); + Box::pin(async move {res.await.into_response()}) + } + (Err(e),_,_) | + (_,Err(e),_) | + (_,_,Err(e)) => __error__(e), } - } - - impl<'req, F, Fut, Body:IntoResponse, Item1:FromRequest<'req>, Item2:FromRequest<'req>, Item3:FromRequest<'req>> IntoHandlerBody> for F - where - F: Fn($param_type, Item1, Item2, Item3) -> Fut + Send + Sync + 'static, - Fut: Future + Send + 'static, - { - fn into_handler(self) -> Handler { - Handler::new(move |req| { - // SAFETY: Due to the architecture of `Router`, - // `params` has already `append`ed once before this code - let p1 = unsafe {req.path.assume_one_param()}; - - match (from_param_bytes(p1), from_request::(req), from_request::(req), from_request::(req)) { - (Ok(p1), Ok(item1), Ok(item2), Ok(item3)) => { - let res = self(p1, item1, item2, item3); - Box::pin(async move {res.await.into_response()}) - } - (Err(e),_,_,_) => __bad_request(e), - (_,Err(e),_,_) => __bad_request(e), - (_,_,Err(e),_) => __bad_request(e), - (_,_,_,Err(e)) => __bad_request(e), - } - }) + }) + } + } + + impl<'req, F, Fut, Body:IntoResponse, P1:FromParam<'req>, Item1:FromRequest<'req>, Item2:FromRequest<'req>, Item3:FromRequest<'req>> IntoHandlerBody> for F + where + F: Fn(P1, Item1, Item2, Item3) -> Fut + Send + Sync + 'static, + Fut: Future + Send + 'static, + { + fn into_handler(self) -> Handler { + Handler::new(move |req| { + // SAFETY: Due to the architecture of `Router`, + // `params` has already `append`ed once before this code + let p1 = unsafe {req.path.assume_one_param()}; + + match (from_param_bytes(p1), from_request::(req), from_request::(req), from_request::(req)) { + (Ok(p1), Ok(item1), Ok(item2), Ok(item3)) => { + let res = self(p1, item1, item2, item3); + Box::pin(async move {res.await.into_response()}) + } + (Err(e),_,_,_) | + (_,Err(e),_,_) | + (_,_,Err(e),_) | + (_,_,_,Err(e)) => __error__(e), } - } - )*}; - } with_single_path_param_and_from_request_items! { - String, &'req str, u8, u16, u32, u64, u128, usize + }) + } } }; @@ -305,8 +263,8 @@ const _: (/* one FromParam and FromRequest items */) = { let res = self((p1,), item1); Box::pin(async move {res.await.into_response()}) } - (Err(e),_) => __bad_request(e), - (_,Err(e)) => __bad_request(e), + (Err(e),_) | + (_,Err(e)) => __error__(e), } }) } @@ -328,9 +286,9 @@ const _: (/* one FromParam and FromRequest items */) = { let res = self((p1,), item1, item2); Box::pin(async move {res.await.into_response()}) } - (Err(e),_,_) => __bad_request(e), - (_,Err(e),_) => __bad_request(e), - (_,_,Err(e)) => __bad_request(e), + (Err(e),_,_) | + (_,Err(e),_) | + (_,_,Err(e)) => __error__(e), } }) } @@ -352,10 +310,10 @@ const _: (/* one FromParam and FromRequest items */) = { let res = self((p1,), item1, item2, item3); Box::pin(async move {res.await.into_response()}) } - (Err(e),_,_,_) => __bad_request(e), - (_,Err(e),_,_) => __bad_request(e), - (_,_,Err(e),_) => __bad_request(e), - (_,_,_,Err(e)) => __bad_request(e), + (Err(e),_,_,_) | + (_,Err(e),_,_) | + (_,_,Err(e),_) | + (_,_,_,Err(e)) => __error__(e), } }) } @@ -379,9 +337,9 @@ const _: (/* two PathParams and FromRequest items */) = { let res = self((p1, p2), item1); Box::pin(async move {res.await.into_response()}) } - (Err(e),_,_) => __bad_request(e), - (_,Err(e),_) => __bad_request(e), - (_,_,Err(e)) => __bad_request(e), + (Err(e),_,_) | + (_,Err(e),_) | + (_,_,Err(e)) => __error__(e), } }) } @@ -403,10 +361,10 @@ const _: (/* two PathParams and FromRequest items */) = { let res = self((p1, p2), item1, item2); Box::pin(async move {res.await.into_response()}) } - (Err(e),_,_,_) => __bad_request(e), - (_,Err(e),_,_) => __bad_request(e), - (_,_,Err(e),_) => __bad_request(e), - (_,_,_,Err(e)) => __bad_request(e), + (Err(e),_,_,_) | + (_,Err(e),_,_) | + (_,_,Err(e),_) | + (_,_,_,Err(e)) => __error__(e), } }) } @@ -428,119 +386,35 @@ const _: (/* two PathParams and FromRequest items */) = { let res = self((p1, p2), item1, item2, item3); Box::pin(async move {res.await.into_response()}) } - (Err(e),_,_,_,_) => __bad_request(e), - (_,Err(e),_,_,_) => __bad_request(e), - (_,_,Err(e),_,_) => __bad_request(e), - (_,_,_,Err(e),_) => __bad_request(e), - (_,_,_,_,Err(e)) => __bad_request(e), + (Err(e),_,_,_,_) | + (_,Err(e),_,_,_) | + (_,_,Err(e),_,_) | + (_,_,_,Err(e),_) | + (_,_,_,_,Err(e)) => __error__(e), } }) } } }; -#[cfg(feature="websocket")] -const _: (/* requires upgrade to websocket */) = { - impl<'req, F, Fut, Body:IntoResponse> IntoHandlerBody> for F - where - F: Fn(WebSocketContext) -> Fut + Send + Sync + 'static, - Fut: Future + Send + 'static, - { - fn into_handler(self) -> Handler { - Handler::new(move |req| { - match WebSocketContext::new(req) { - Ok(wsc) => { - let res = self(wsc); - Box::pin(async move {res.await.into_response()}) - } - Err(res) => (|| Box::pin(async {res}))(), - } - }).requires_upgrade() - } - } - impl<'req, F, Fut, Body:IntoResponse, P1:FromParam<'req>> IntoHandlerBody> for F - where - F: Fn(WebSocketContext, P1) -> Fut + Send + Sync + 'static, - Fut: Future + Send + 'static, - { - fn into_handler(self) -> Handler { - Handler::new(move |req| { - let p1 = unsafe {req.path.assume_one_param()}; - match from_param_bytes(p1) { - Ok(p1) => match WebSocketContext::new(req) { - Ok(wsc) => { - let res = self(wsc, p1); - Box::pin(async move {res.await.into_response()}) - } - Err(res) => (|| Box::pin(async {res}))(), - } - Err(e) => __bad_request(e), - } - }).requires_upgrade() - } - } - impl<'req, F, Fut, Body:IntoResponse, P1:FromParam<'req>, P2:FromParam<'req>> IntoHandlerBody> for F - where - F: Fn(WebSocketContext, P1, P2) -> Fut + Send + Sync + 'static, - Fut: Future + Send + 'static, - { - fn into_handler(self) -> Handler { - Handler::new(move |req| { - let (p1, p2) = unsafe {req.path.assume_two_params()}; - match (from_param_bytes(p1), from_param_bytes(p2)) { - (Ok(p1), Ok(p2)) => match WebSocketContext::new(req) { - Ok(wsc) => { - let res = self(wsc, p1, p2); - Box::pin(async move {res.await.into_response()}) - } - Err(res) => (|| Box::pin(async {res}))(), - } - (Err(e),_)|(_,Err(e)) => __bad_request(e), - } - }).requires_upgrade() - } - } - impl<'req, F, Fut, Body:IntoResponse, P1:FromParam<'req>> IntoHandlerBody> for F - where - F: Fn(WebSocketContext, (P1,)) -> Fut + Send + Sync + 'static, - Fut: Future + Send + 'static, - { - fn into_handler(self) -> Handler { - Handler::new(move |req| { - let p1 = unsafe {req.path.assume_one_param()}; - match from_param_bytes(p1) { - Ok(p1) => match WebSocketContext::new(req) { - Ok(wsc) => { - let res = self(wsc, (p1,)); - Box::pin(async move {res.await.into_response()}) - } - Err(res) => (|| Box::pin(async {res}))(), - } - Err(e) => __bad_request(e), - } - }).requires_upgrade() - } - } - impl<'req, F, Fut, Body:IntoResponse, P1:FromParam<'req>, P2:FromParam<'req>> IntoHandlerBody> for F - where - F: Fn(WebSocketContext, (P1, P2)) -> Fut + Send + Sync + 'static, - Fut: Future + Send + 'static, - { - fn into_handler(self) -> Handler { - Handler::new(move |req| { - let (p1, p2) = unsafe {req.path.assume_two_params()}; - match (from_param_bytes(p1), from_param_bytes(p2)) { - (Ok(p1), Ok(p2)) => match WebSocketContext::new(req) { - Ok(wsc) => { - let res = self(wsc, (p1, p2)); - Box::pin(async move {res.await.into_response()}) - } - Err(res) => (|| Box::pin(async {res}))(), - } - (Err(e),_)|(_,Err(e)) => __bad_request(e), - } - }).requires_upgrade() + +#[cfg(test)] #[test] fn handler_args() { + async fn h1(_param: String) -> Response {todo!()} + async fn h2(_param: &str) -> Response {todo!()} + + struct P; + impl<'p> FromParam<'p> for P { + type Error = std::convert::Infallible; + fn from_param(_param: std::borrow::Cow<'p, str>) -> Result { + Ok(Self) } } -}; + async fn h3(_param: P) -> String {format!("")} + + macro_rules! assert_handlers { + ( $($function:ident)* ) => { + $( let _ = $function.into_handler(); )* + }; + } assert_handlers! { h1 h2 h3 } +} diff --git a/ohkami/src/handler/mod.rs b/ohkami/src/handler/mod.rs index cb3a5181..7030ac0f 100644 --- a/ohkami/src/handler/mod.rs +++ b/ohkami/src/handler/mod.rs @@ -1,5 +1,9 @@ -mod handlers; pub use handlers::{Handlers, ByAnother, Route}; -mod into_handler; pub use into_handler::{IntoHandler}; +mod handlers; +pub use handlers::{Handlers, ByAnother, Route}; + +mod into_handler; +pub use into_handler::{IntoHandler}; + use std::{ pin::Pin, @@ -10,30 +14,32 @@ use crate::{Request, Response}; #[derive(Clone)] -pub struct Handler { - pub(crate) proc: Arc Pin< - Box Pin + + Send + '_ + >> + + Send + Sync + + 'static +>); +const _: () = { + impl Handler { + fn new(proc: impl + Fn(&Request) -> Pin - + Send + 'static - > - > + Send + Sync + 'static - > -} + + Send + '_ + >> + + Send + Sync + + 'static + ) -> Self { + Self(Arc::new(proc)) + } -impl Handler { - fn new( - proc: ( - impl Fn(&mut Request) -> Pin< - Box - + Send + 'static - > - > + Send + Sync + 'static - ) - ) -> Self { - Self { - proc: Arc::new(proc), + #[inline(always)] pub(crate) fn handle<'req>( + &'req self, + req: &'req Request, + ) -> Pin + Send + 'req>> { + (self.0)(req) } } -} +}; diff --git a/ohkami/src/ohkami/mod.rs b/ohkami/src/ohkami/mod.rs index 04a6d686..7fad98bf 100644 --- a/ohkami/src/ohkami/mod.rs +++ b/ohkami/src/ohkami/mod.rs @@ -91,11 +91,12 @@ use crate::Method; ///
/// /// #### handler schema: -/// async ({path_params}?, {`FromRequest` type}s...) -> {`IntoResponse` type} +/// `async ({path_params}?, {FromRequest type}s...) -> {IntoResponse type}` /// /// #### path_params: /// A tuple of types that implement `FromParam` trait.\ -/// `String`, `&str`, and primitive integers are splecially allowed to be used without tuple: +/// If the path contains only one parameter, then you can omit the tuple.\ +/// (In current ohkami, at most *2* path params can be passed.) /// ///
/// @@ -118,6 +119,7 @@ use crate::Method; /// todo!() /// } /// ``` +#[cfg_attr(all(feature="DEBUG", test), derive(Clone))] pub struct Ohkami { pub(crate) routes: TrieRouter, @@ -218,12 +220,4 @@ impl Ohkami { router } - - #[cfg(all(feature="DEBUG", test))] - pub(crate) fn clone(&self) -> Self { - Self { - routes: self.routes.clone(), - fangs: self.fangs .clone(), - } - } } diff --git a/ohkami/src/ohkami/router/radix.rs b/ohkami/src/ohkami/router/radix.rs index 1b17f419..78d380d2 100644 --- a/ohkami/src/ohkami/router/radix.rs +++ b/ohkami/src/ohkami/router/radix.rs @@ -298,7 +298,7 @@ impl Node { } } - let mut res = (handler.proc)(req).await; + let mut res = handler.handle(req).await; for bf in self.back { if let Err(err_res) = bf.call(&mut res, req).await { diff --git a/ohkami/src/request/from_request.rs b/ohkami/src/request/from_request.rs index b210a0ff..60a8dc1b 100644 --- a/ohkami/src/request/from_request.rs +++ b/ohkami/src/request/from_request.rs @@ -1,5 +1,5 @@ use std::borrow::Cow; -use crate::{Request}; +use crate::{IntoResponse, Request, Response}; pub enum FromRequestError { @@ -23,29 +23,41 @@ pub enum FromRequestError { } } } -}; -impl std::error::Error for FromRequestError {} -impl std::fmt::Debug for FromRequestError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(&self) + impl std::error::Error for FromRequestError {} + impl std::fmt::Debug for FromRequestError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&self) + } } -} -impl std::fmt::Display for FromRequestError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(&self) + impl std::fmt::Display for FromRequestError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&self) + } } -} -/// Represents "retirieved from `Request`". + impl IntoResponse for FromRequestError { + fn into_response(self) -> Response { + Response::InternalServerError().text(match self { + Self::Owned(s) => Cow::Owned(s), + Self::Static(s) => Cow::Borrowed(s), + }) + } + } +}; + +/// "Retirieved from a `Request`". /// /// - `#[Query]` /// - `#[Payload]` /// /// implements this for a struct. /// -/// Of course, you can manually implement for any structs that can be extracted from request: +/// Of course, you can manually implement for your structs that can be extracted from a request: /// +///
+/// +/// --- /// *example.rs* /// ``` /// use ohkami::prelude::*; @@ -61,18 +73,29 @@ impl std::fmt::Display for FromRequestError { /// } /// } /// ``` +/// --- +/// +///
+/// +/// NOTE: *Cannot impl both `FromRequest` and `FromParam`*. pub trait FromRequest<'req>: Sized { - type Error: std::error::Error + 'static; + /// If this extraction never fails, `std::convert::Infallible` is recomended as this `Error`. + type Error: IntoResponse; + fn from_request(req: &'req Request) -> Result; } -/// Repredents "retrieved from path/query param". +/// "Retrieved from a path/query param". +/// +/// NOTE: *Cannot impl both `FromRequest` and `FromParam`*. pub trait FromParam<'p>: Sized { - type Error: std::error::Error; + /// If this extraction never fails, `std::convert::Infallible` is recomended as this `Error`. + type Error: IntoResponse; + /// `param` is percent-decoded: /// /// - `Cow::Borrowed(&'p str)` by default - /// - `Cow::Owned(String)` if it is decoded + /// - `Cow::Owned(String)` if ohkami has decoded it fn from_param(param: Cow<'p, str>) -> Result; } const _: () = { impl<'p> FromParam<'p> for String { diff --git a/ohkami/src/request/mod.rs b/ohkami/src/request/mod.rs index cadf203c..3a0bd05a 100644 --- a/ohkami/src/request/mod.rs +++ b/ohkami/src/request/mod.rs @@ -84,19 +84,22 @@ pub(crate) const PAYLOAD_LIMIT: usize = 1 << 32; /// ``` pub struct Request {pub(crate) _metadata: [u8; METADATA_SIZE], method: Method, - /// Headers of this request + /// Headers of this response /// /// - `.{Name}()` to get the value /// - `.set().{Name}(〜)` to mutate the value - /// - `.set().{Name}(append(〜))` to append + /// - `.set().{Name}({value})` to insert + /// - `.set().{Name}(None)` to remove + /// - `.set().{Name}(append({value}))` to append + /// + /// `{value}`: `String`, `&'static str`, `Cow<&'static, str>` /// /// --- /// - /// *`custom-header` feature required*: + /// *`custom-header` feature required* : /// /// - `.custom({Name})` to get the value - /// - `.set().custom({Name}, {value})` to mutate the value - /// - `.set().custom({Name}, append(〜))` to append + /// - `.set().custom({Name}, 〜)` to mutate the value like standard headers pub headers: RequestHeaders, pub(crate) path: Path, queries: QueryParams, diff --git a/ohkami/src/response/into_response.rs b/ohkami/src/response/into_response.rs index 818ff85c..f261662b 100644 --- a/ohkami/src/response/into_response.rs +++ b/ohkami/src/response/into_response.rs @@ -65,7 +65,7 @@ impl IntoResponse for &'static str { } } impl IntoResponse for String { - fn into_response(self) -> Response { + #[inline(always)] fn into_response(self) -> Response { Response::with(Status::OK).text(self) } } @@ -79,3 +79,9 @@ impl IntoResponse for std::borrow::Cow<'static, str> { Response::with(Status::OK).text(self) } } + +impl IntoResponse for std::convert::Infallible { + fn into_response(self) -> Response { + unsafe {std::hint::unreachable_unchecked()} + } +} diff --git a/ohkami/src/response/mod.rs b/ohkami/src/response/mod.rs index c225dcc4..e81deb0f 100644 --- a/ohkami/src/response/mod.rs +++ b/ohkami/src/response/mod.rs @@ -78,15 +78,18 @@ pub struct Response { /// /// - `.{Name}()` to get the value /// - `.set().{Name}(〜)` to mutate the value - /// - `.set().{Name}(append(〜))` to append + /// - `.set().{Name}({value})` to insert + /// - `.set().{Name}(None)` to remove + /// - `.set().{Name}(append({value}))` to append + /// + /// `{value}`: `String`, `&'static str`, `Cow<&'static, str>` /// /// --- /// - /// *`custom-header` feature required*: + /// *`custom-header` feature required* : /// /// - `.custom({Name})` to get the value - /// - `.set().custom({Name}, {value})` to mutate the value - /// - `.set().custom({Name}, append(〜))` to append + /// - `.set().custom({Name}, 〜)` to mutate the value like standard headers pub headers: ResponseHeaders, pub(crate) content: Option>, } const _: () = { diff --git a/ohkami/src/typed/response_body.rs b/ohkami/src/typed/response_body.rs index 0bb3288c..d96845d5 100644 --- a/ohkami/src/typed/response_body.rs +++ b/ohkami/src/typed/response_body.rs @@ -96,6 +96,7 @@ macro_rules! plain_text_responsebodies { #[test] fn assert_impls() { fn is_reponsebody() {} + is_reponsebody::<()>(); is_reponsebody::<&'static str>(); is_reponsebody::(); is_reponsebody::<&'_ String>(); diff --git a/ohkami_macros/src/payload.rs b/ohkami_macros/src/payload.rs index ffe0fb97..da88efdf 100644 --- a/ohkami_macros/src/payload.rs +++ b/ohkami_macros/src/payload.rs @@ -55,14 +55,14 @@ fn impl_payload_json(data: &ItemStruct, derive_deserialize: bool) -> Result ::ohkami::FromRequest<#impl_lifetime> for #struct_name<#struct_lifetime> { - type Error = ::ohkami::FromRequestError; - #[inline] fn from_request(req: &#impl_lifetime ::ohkami::Request) -> ::std::result::Result { + type Error = ::ohkami::Response; + #[inline] fn from_request(req: &#impl_lifetime ::ohkami::Request) -> ::std::result::Result { let payload = req.payload() - .ok_or_else(|| ::ohkami::FromRequestError::Static("Expected payload"))?; + .ok_or_else(|| ::ohkami::Response::BadRequest().text("Expected payload"))?; if !req.headers.ContentType().unwrap().starts_with("application/json") { - return ::std::result::Result::Err(::ohkami::FromRequestError::Static("Expected a payload of `Content-Type: application/json`")) + return ::std::result::Result::Err(::ohkami::Response::BadRequest().text("Expected a payload of `Content-Type: application/json`")) } - let __payload__ = ::ohkami::__internal__::parse_json(payload).map_err(|e| ::ohkami::FromRequestError::from(e))?; + let __payload__ = ::ohkami::__internal__::parse_json(payload).map_err(|e| ::ohkami::Response::InternalServerError().text(e.to_string()))?; ::std::result::Result::Ok(__payload__) } } @@ -103,7 +103,7 @@ fn impl_payload_urlencoded(data: &ItemStruct) -> Result { quote!{ #ident_str => #ident.replace(<#ty as ::ohkami::FromParam>::parse(v.as_bytes())?) .map_or(::std::result::Result::Ok(()), |_| - ::std::result::Result::Err(::ohkami::FromRequestError::Static(concat!("duplicated key: `", #ident_str,"`"))) + ::std::result::Result::Err(::ohkami::Response::BadRequest().text(concat!("Duplicated key: `", #ident_str,"`"))) )?, } }); @@ -112,7 +112,7 @@ fn impl_payload_urlencoded(data: &ItemStruct) -> Result { for (k, v) in ::ohkami::__internal__::parse_urlencoded(payload) { match &*k { #( #arms )* - unexpected => return ::std::result::Result::Err(::ohkami::FromRequestError::Owned(::std::format!("unexpected key: `{unexpected}`"))) + unexpected => return ::std::result::Result::Err(::ohkami::Response::BadRequest().text(::std::format!("Unexpected key: `{unexpected}`"))) } } } @@ -124,7 +124,7 @@ fn impl_payload_urlencoded(data: &ItemStruct) -> Result { quote!{ #ident, } } else { let ident_str = ident.to_string(); - quote!{ #ident: #ident.ok_or_else(|| ::ohkami::FromRequestError::Static(::std::concat!("`", #ident_str, "` is not found")))?, } + quote!{ #ident: #ident.ok_or_else(|| ::ohkami::Response::BadRequest().text(::std::concat!("`", #ident_str, "` is not found")))?, } } }); @@ -137,12 +137,12 @@ fn impl_payload_urlencoded(data: &ItemStruct) -> Result { Ok(quote!{ impl<#impl_lifetime> ::ohkami::FromRequest<#impl_lifetime> for #struct_name<#struct_lifetime> { - type Error = ::ohkami::FromRequestError; - fn from_request(req: &#impl_lifetime ::ohkami::Request) -> ::std::result::Result { + type Error = ::ohkami::Response; + fn from_request(req: &#impl_lifetime ::ohkami::Request) -> ::std::result::Result { let payload = req.payload() - .ok_or_else(|| ::ohkami::FromRequestError::Static("Expected a payload"))?; + .ok_or_else(|| ::ohkami::Response::BadRequest().text("Expected a payload"))?; if !req.headers.ContentType().unwrap().starts_with("application/x-www-form-urlencoded") { - return ::std::result::Result::Err(::ohkami::FromRequestError::Static("Expected an `application/x-www-form-urlencoded` payload")) + return ::std::result::Result::Err(::ohkami::Response::BadRequest().text("Expected an `application/x-www-form-urlencoded` payload")) } #declaring_exprs @@ -190,9 +190,9 @@ fn impl_payload_formdata(data: &ItemStruct) -> Result { impl PartType { fn into_method_call(&self) -> TokenStream { match self { - Self::Field => quote!{ form_part.into_field()?.text().map_err(|e| ::ohkami::FromRequestError::Owned(::std::format!("Invalid form text: {e}")))? }, - Self::Files => quote!{ form_part.into_files().map_err(|e| ::ohkami::FromRequestError::from(e))? }, - Self::File => quote!{ form_part.into_file().map_err(|e| ::ohkami::FromRequestError::from(e))? }, + Self::Field => quote!{ form_part.into_field().map_err(|e| ::ohkami::Response::InternalServerError().text(e))?.text().map_err(|e| ::ohkami::Response::InternalServerError().text(::std::format!("Invalid form text: {e}")))? }, + Self::Files => quote!{ form_part.into_files().map_err(|e| ::ohkami::Response::InternalServerError().text(e))? }, + Self::File => quote!{ form_part.into_file().map_err(|e| ::ohkami::Response::InternalServerError().text(e))? }, } } } @@ -213,10 +213,10 @@ fn impl_payload_formdata(data: &ItemStruct) -> Result { }).collect::>>()?; quote!{ - for form_part in ::ohkami::__internal__::parse_formparts(payload, &boundary)? { + for form_part in ::ohkami::__internal__::parse_formparts(payload, &boundary).map_err(|e| ::ohkami::Response::InternalServerError().text(e))? { match form_part.name() { #( #arms )* - unexpected => return ::std::result::Result::Err(::ohkami::FromRequestError::Owned(::std::format!("unexpected part in form-data: `{unexpected}`"))) + unexpected => return ::std::result::Result::Err(::ohkami::Response::BadRequest().text(::std::format!("unexpected part in form-data: `{unexpected}`"))) } } } @@ -228,7 +228,7 @@ fn impl_payload_formdata(data: &ItemStruct) -> Result { quote!{ #ident, } } else { let ident_str = ident.to_string(); - quote!{ #ident: #ident.ok_or_else(|| ::ohkami::FromRequestError::Static(::std::concat!("Field `", #ident_str, "` is not found in the form-data")))?, } + quote!{ #ident: #ident.ok_or_else(|| ::ohkami::Response::BadRequest().text(::std::concat!("Field `", #ident_str, "` is not found in the form-data")))?, } } }); @@ -241,13 +241,14 @@ fn impl_payload_formdata(data: &ItemStruct) -> Result { Ok(quote!{ impl<#impl_lifetime> ::ohkami::FromRequest<#impl_lifetime> for #struct_name<#struct_lifetime> { - type Error = ::ohkami::FromRequestError; - fn from_request(req: &#impl_lifetime ::ohkami::Request) -> ::std::result::Result { + type Error = ::ohkami::Response; + + fn from_request(req: &#impl_lifetime ::ohkami::Request) -> ::std::result::Result { let payload = req.payload() - .ok_or_else(|| ::ohkami::FromRequestError::Static("Expected a payload"))?; + .ok_or_else(|| ::ohkami::Response::BadRequest().text("Expected a payload"))?; - #[cold] const fn __expected_multipart_formdata_and_boundary() -> ::ohkami::FromRequestError { - ::ohkami::FromRequestError::Static("Expected `multipart/form-data` and a boundary") + #[cold] fn __expected_multipart_formdata_and_boundary() -> ::ohkami::Response { + ::ohkami::Response::BadRequest().text("Expected `multipart/form-data` and a boundary") } let ("multipart/form-data", boundary) = req.headers.ContentType().unwrap() .split_once("; boundary=") diff --git a/ohkami_macros/src/query.rs b/ohkami_macros/src/query.rs index 4a735c4e..182e8a06 100644 --- a/ohkami_macros/src/query.rs +++ b/ohkami_macros/src/query.rs @@ -35,23 +35,23 @@ pub(super) fn Query(data: TokenStream) -> Result { quote!{ #field_name: req.query::<#inner_type>(#field_name_str) // Option> .transpose() - .map_err(|e| ::ohkami::FromRequestError::Owned(e.to_string()))?, + .map_err(|e| ::ohkami::Response::InternalServerError().text(e.to_string()))?, } } else { quote!{ #field_name: req.query::<#field_type>(#field_name_str) // Option> - .ok_or_else(|| ::ohkami::FromRequestError::Static( + .ok_or_else(|| ::ohkami::Response::BadRequest().text( concat!("Expected query parameter `", #field_name_str, "`") ))? - .map_err(|e| ::ohkami::FromRequestError::Owned(e.to_string()))?, + .map_err(|e| ::ohkami::Response::InternalServerError().text(e.to_string()))?, } } }); quote!{ impl<#impl_lifetime> ::ohkami::FromRequest<#impl_lifetime> for #struct_name<#struct_lifetime> { - type Error = ::ohkami::FromRequestError; - #[inline] fn from_request(req: &#impl_lifetime ::ohkami::Request) -> ::std::result::Result { + type Error = ::ohkami::Response; + #[inline] fn from_request(req: &#impl_lifetime ::ohkami::Request) -> ::std::result::Result { ::std::result::Result::Ok(Self { #( #fields )* })