diff --git a/benches/Cargo.toml b/benches/Cargo.toml index 22343e64..c3c4287c 100644 --- a/benches/Cargo.toml +++ b/benches/Cargo.toml @@ -5,6 +5,6 @@ edition = "2021" authors = ["kanarus "] [dependencies] -ohkami = { version = "0.14.0", path = "../ohkami", features = ["rt_tokio", "DEBUG"] } +ohkami = { version = "0.15.0", path = "../ohkami", features = ["rt_tokio", "DEBUG"] } http = "1.0.0" rustc-hash = "1.1" \ No newline at end of file diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 5a557f99..73f8cefd 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -5,12 +5,13 @@ members = [ "hello", "realworld", "quick_start", + "json_response", ] [workspace.dependencies] # To assure "DEBUG" feature be off even if DEBUGing `../ohkami`, # explicitly set `default-features = false` -ohkami = { version = "0.14.0", path = "../ohkami", default-features = false, features = ["rt_tokio", "utils", "testing"] } +ohkami = { version = "0.15.0", path = "../ohkami", default-features = false, features = ["rt_tokio", "utils", "testing"] } tokio = { version = "1", features = ["full"] } sqlx = { version = "0.7.3", features = ["runtime-tokio-native-tls", "postgres", "macros", "chrono", "uuid"] } tracing = "0.1" diff --git a/examples/json_response/Cargo.toml b/examples/json_response/Cargo.toml new file mode 100644 index 00000000..e844981c --- /dev/null +++ b/examples/json_response/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "json_response" +version = "0.1.0" +edition = "2021" +authors = ["kanarus "] + +[dependencies] +ohkami = { workspace = true } +tokio = { workspace = true } \ No newline at end of file diff --git a/examples/json_response/src/main.rs b/examples/json_response/src/main.rs new file mode 100644 index 00000000..f0059c32 --- /dev/null +++ b/examples/json_response/src/main.rs @@ -0,0 +1,40 @@ +use ohkami::{typed::ResponseBody, Ohkami, Route}; + +#[ResponseBody(JSONS)] +struct User { + id: u64, + name: String, +} + +async fn single_user() -> User { + User { + id: 42, + name: String::from("ohkami"), + } +} + +async fn multiple_users() -> Vec { + vec![ + User { + id: 42, + name: String::from("ohkami"), + }, + User { + id: 1024, + name: String::from("bynari"), + } + ] +} + +async fn nullable_user() -> Option { + None +} + +#[tokio::main] +async fn main() { + Ohkami::new(( + "/single" .GET(single_user), + "/multiple".GET(multiple_users), + "/nullable".GET(nullable_user), + )).howl("localhost:5000").await +} diff --git a/ohkami/Cargo.toml b/ohkami/Cargo.toml index 641361c4..a2a402ae 100644 --- a/ohkami/Cargo.toml +++ b/ohkami/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ohkami" -version = "0.14.0" +version = "0.15.0" edition = "2021" authors = ["kanarus "] description = "Build web app in intuitive and declarative code" @@ -17,7 +17,7 @@ features = ["rt_tokio", "custom-header"] [dependencies] ohkami_lib = { version = "=0.1.1", path = "../ohkami_lib" } -ohkami_macros = { version = "=0.5.2", path = "../ohkami_macros" } +ohkami_macros = { version = "=0.6.0", path = "../ohkami_macros" } tokio = { version = "1", optional = true, features = ["net", "rt", "io-util", "sync"] } async-std = { version = "1", optional = true } byte_reader = { version = "2.0.0", features = ["text"] } diff --git a/ohkami/src/fang/builtin/jwt.rs b/ohkami/src/fang/builtin/jwt.rs index 8e60d44e..227c2928 100644 --- a/ohkami/src/fang/builtin/jwt.rs +++ b/ohkami/src/fang/builtin/jwt.rs @@ -338,7 +338,7 @@ impl JWT { #[test] async fn test_jwt_verify_senario() { use crate::prelude::*; use crate::{testing::*, Memory}; - use crate::typed::{ResponseBody, status::OK}; + use crate::typed::{ResponseBody, body_type, status::OK}; use std::{sync::OnceLock, sync::Mutex, collections::HashMap, borrow::Cow}; @@ -405,6 +405,7 @@ impl JWT { familly_name: String, } impl ResponseBody for Profile { + type Type = body_type::JSON; fn into_response_with(self, status: Status) -> Response { Response::with(status).json(self) } diff --git a/ohkami/src/handler/handlers.rs b/ohkami/src/handler/handlers.rs index d11f3ca0..6e6d815c 100644 --- a/ohkami/src/handler/handlers.rs +++ b/ohkami/src/handler/handlers.rs @@ -111,7 +111,7 @@ macro_rules! Route { use ::serde::{Serialize, Deserialize}; use super::{Handlers, Route}; use crate::{FromRequest, IntoResponse, Response, Request, Status}; - use crate::typed::{ResponseBody, status::{OK, Created}}; + use crate::typed::{ResponseBody, body_type, status::{OK, Created}}; enum APIError { @@ -134,6 +134,7 @@ macro_rules! Route { password: String, } const _: () = { impl ResponseBody for User { + type Type = body_type::JSON; fn into_response_with(self, status: Status) -> Response { Response::with(status).json(self) } diff --git a/ohkami/src/response/into_response.rs b/ohkami/src/response/into_response.rs index f261662b..bf7aeb48 100644 --- a/ohkami/src/response/into_response.rs +++ b/ohkami/src/response/into_response.rs @@ -59,26 +59,26 @@ impl IntoResponse for Result { } } -impl IntoResponse for &'static str { - fn into_response(self) -> Response { - Response::with(Status::OK).text(self) - } -} -impl IntoResponse for String { - #[inline(always)] fn into_response(self) -> Response { - Response::with(Status::OK).text(self) - } -} -impl IntoResponse for &'_ String { - fn into_response(self) -> Response { - Response::with(Status::OK).text(self.clone()) - } -} -impl IntoResponse for std::borrow::Cow<'static, str> { - fn into_response(self) -> Response { - Response::with(Status::OK).text(self) - } -} +// impl IntoResponse for &'static str { +// fn into_response(self) -> Response { +// Response::with(Status::OK).text(self) +// } +// } +// impl IntoResponse for String { +// #[inline(always)] fn into_response(self) -> Response { +// Response::with(Status::OK).text(self) +// } +// } +// impl IntoResponse for &'_ String { +// fn into_response(self) -> Response { +// Response::with(Status::OK).text(self.clone()) +// } +// } +// impl IntoResponse for std::borrow::Cow<'static, str> { +// fn into_response(self) -> Response { +// Response::with(Status::OK).text(self) +// } +// } impl IntoResponse for std::convert::Infallible { fn into_response(self) -> Response { diff --git a/ohkami/src/typed/mod.rs b/ohkami/src/typed/mod.rs index dd331863..14127ed8 100644 --- a/ohkami/src/typed/mod.rs +++ b/ohkami/src/typed/mod.rs @@ -1,10 +1,11 @@ pub mod status; mod response_body; -pub use response_body::ResponseBody; +pub use response_body::{ResponseBody, body_type}; pub use ohkami_macros::ResponseBody; pub(crate) mod parse_payload; #[cfg(test)] mod _test_parse_payload; pub use parse_payload::{File}; + pub use ohkami_macros::{Payload, Query}; diff --git a/ohkami/src/typed/response_body.rs b/ohkami/src/typed/response_body.rs index d96845d5..39795161 100644 --- a/ohkami/src/typed/response_body.rs +++ b/ohkami/src/typed/response_body.rs @@ -50,9 +50,37 @@ use std::borrow::Cow; /// } /// ``` pub trait ResponseBody: Serialize { + /// Select from `ohkami::typed::body_type` module + type Type: BodyType; fn into_response_with(self, status: Status) -> Response; } + +pub trait BodyType {} +macro_rules! body_type { + ($( $name:ident, )*) => { + pub mod body_type { + $( + pub struct $name; + impl super::BodyType for $name {} + )* + } + }; +} body_type! { + Empty, + JSON, + HTML, + Text, + Other, +} + +impl crate::IntoResponse for RB { + fn into_response(self) -> Response { + self.into_response_with(Status::OK) + } +} + impl ResponseBody for () { + type Type = body_type::Empty; fn into_response_with(self, status: Status) -> Response { Response { status, @@ -61,10 +89,53 @@ impl ResponseBody for () { } } } + +const _: (/* JSON utility impls */) = { + impl> ResponseBody for Option { + type Type = body_type::JSON; + fn into_response_with(self, status: Status) -> Response { + Response::with(status).json(self) + } + } + + impl> ResponseBody for Vec { + type Type = body_type::JSON; + #[inline] fn into_response_with(self, status: Status) -> Response { + Response::with(status).json(self) + } + } + + impl> ResponseBody for &[RB] { + type Type = body_type::JSON; + fn into_response_with(self, status: Status) -> Response { + Response::with(status).json(self) + } + } + + /// `impl, const N: usize> ResponseBody for [RB; N]` + /// is not available becasue `serde` only provides following 33 `Serialize` impls... + macro_rules! response_body_of_json_array_of_len { + ($($len:literal)*) => { + $( + impl> ResponseBody for [RB; $len] { + type Type = body_type::JSON; + #[inline] fn into_response_with(self, status: Status) -> Response { + Response::with(status).json(self) + } + } + )* + }; + } response_body_of_json_array_of_len! { + 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 + 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 + } +}; + macro_rules! plain_text_responsebodies { ($( $text_type:ty: $self:ident => $content:expr, )*) => { $( impl ResponseBody for $text_type { + type Type = body_type::Text; #[inline] fn into_response_with(self, status: Status) -> Response { let content = {let $self = self; $content}; @@ -94,12 +165,13 @@ macro_rules! plain_text_responsebodies { #[cfg(test)] #[test] fn assert_impls() { - fn is_reponsebody() {} + fn is_empty_reponsebody>() {} + is_empty_reponsebody::<()>(); - is_reponsebody::<()>(); - is_reponsebody::<&'static str>(); - is_reponsebody::(); - is_reponsebody::<&'_ String>(); - is_reponsebody::>(); - is_reponsebody::>(); + fn is_text_reponsebody>() {} + is_text_reponsebody::<&'static str>(); + is_text_reponsebody::(); + is_text_reponsebody::<&'_ String>(); + is_text_reponsebody::>(); + is_text_reponsebody::>(); } diff --git a/ohkami/src/utils/text.rs b/ohkami/src/utils/text.rs index 128fc732..d859e083 100644 --- a/ohkami/src/utils/text.rs +++ b/ohkami/src/utils/text.rs @@ -1,9 +1,9 @@ #![allow(non_snake_case)] -use crate::{Response, IntoResponse, Status}; +use crate::{Response, Status}; +use crate::typed::{ResponseBody, body_type}; use crate::response::ResponseHeaders; use crate::serde::Serialize; -use crate::typed::ResponseBody; use std::borrow::Cow; @@ -21,12 +21,8 @@ impl Serialize for Text { self.content.serialize(serializer) } } -impl IntoResponse for Text { - #[inline(always)] fn into_response(self) -> Response { - self.into_response_with(Status::OK) - } -} impl ResponseBody for Text { + type Type = body_type::Text; #[inline] fn into_response_with(self, status: Status) -> Response { let content = match self.content { Cow::Borrowed(str) => Cow::Borrowed(str.as_bytes()), @@ -60,12 +56,8 @@ impl Serialize for HTML { self.content.serialize(serializer) } } -impl IntoResponse for HTML { - #[inline(always)] fn into_response(self) -> Response { - self.into_response_with(Status::OK) - } -} impl ResponseBody for HTML { + type Type = body_type::HTML; #[inline] fn into_response_with(self, status: Status) -> Response { let content = match self.content { Cow::Borrowed(str) => Cow::Borrowed(str.as_bytes()), diff --git a/ohkami_macros/Cargo.toml b/ohkami_macros/Cargo.toml index bea676e8..6e2431a9 100644 --- a/ohkami_macros/Cargo.toml +++ b/ohkami_macros/Cargo.toml @@ -3,7 +3,7 @@ proc-macro = true [package] name = "ohkami_macros" -version = "0.5.2" +version = "0.6.0" edition = "2021" authors = ["kanarus "] description = "Proc macros for ohkami - intuitive and declarative web framework" diff --git a/ohkami_macros/src/response.rs b/ohkami_macros/src/response.rs index 16c417b4..63cc771d 100644 --- a/ohkami_macros/src/response.rs +++ b/ohkami_macros/src/response.rs @@ -13,38 +13,21 @@ pub(super) fn ResponseBody(format: TokenStream, data: TokenStream) -> Result quote! { - #data - - impl<#generics_params> ::ohkami::typed::ResponseBody for #name<#generics_params> - #generics_where - { - #[inline(always)] fn into_response_with(self, status: ::ohkami::Status) -> ::ohkami::Response { - ::ohkami::Response::with(status).json(self) - } - } - - impl<#generics_params> ::ohkami::IntoResponse for #name<#generics_params> - #generics_where - { - #[inline] fn into_response(self) -> ::ohkami::Response { - ::ohkami::Response::with(::ohkami::Status::OK).json(self) - } - } - }, - ResponseFormat::JSONS => { - let derive_serialize = quote! { + Ok(match &format { + ResponseFormat::JSON | ResponseFormat::JSONS => { + let derive_serialize = matches!(format, ResponseFormat::JSONS).then_some(quote! { #[derive(::ohkami::serde::Serialize)] #[::ohkami::__internal__::consume_struct] #data - }; + }); let data = { let mut data = data.clone(); - data.attrs.retain(|a| a.path.to_token_stream().to_string() != "serde"); - for f in &mut data.fields { - f.attrs.retain(|a| a.path.to_token_stream().to_string() != "serde") + if matches!(format, ResponseFormat::JSONS) { + data.attrs.retain(|a| a.path.to_token_stream().to_string() != "serde"); + for f in &mut data.fields { + f.attrs.retain(|a| a.path.to_token_stream().to_string() != "serde") + } } data }; @@ -56,19 +39,12 @@ pub(super) fn ResponseBody(format: TokenStream, data: TokenStream) -> Result ::ohkami::typed::ResponseBody for #name<#generics_params> #generics_where { + type Type = ::ohkami::typed::body_type::JSON; #[inline(always)] fn into_response_with(self, status: ::ohkami::Status) -> ::ohkami::Response { ::ohkami::Response::with(status).json(self) } } - - impl<#generics_params> ::ohkami::IntoResponse for #name<#generics_params> - #generics_where - { - #[inline] fn into_response(self) -> ::ohkami::Response { - ::ohkami::Response::with(::ohkami::Status::OK).json(self) - } - } } - }, + } }) }