diff --git a/examples/hello/src/main.rs b/examples/hello/src/main.rs index 32a2505f..9022f2e4 100644 --- a/examples/hello/src/main.rs +++ b/examples/hello/src/main.rs @@ -12,13 +12,13 @@ mod hello_handler { use ohkami::utils::{Payload, Query}; #[Query] - pub struct HelloQuery { - name: String, + pub struct HelloQuery<'q> { + name: &'q str, repeat: Option, } - pub async fn hello_by_query(c: Context, - HelloQuery { name, repeat }: HelloQuery + pub async fn hello_by_query<'h>(c: Context, + HelloQuery { name, repeat }: HelloQuery<'h> ) -> Response { tracing::info!("\ Called `hello_by_query`\ @@ -31,13 +31,13 @@ mod hello_handler { #[Payload(JSON)] #[derive(serde::Deserialize)] - pub struct HelloRequest { - name: String, + pub struct HelloRequest<'n> { + name: &'n str, repeat: Option, } - pub async fn hello_by_json(c: Context, - HelloRequest { name, repeat }: HelloRequest + pub async fn hello_by_json<'h>(c: Context, + HelloRequest { name, repeat }: HelloRequest<'h> ) -> Response { tracing::info!("\ Called `hello_by_query`\ @@ -58,8 +58,8 @@ mod hello_handler { mod fangs { use ohkami::{Context, Request, Fang, IntoFang}; - pub struct AppendServer; - impl IntoFang for AppendServer { + pub struct SetServer; + impl IntoFang for SetServer { fn into_fang(self) -> Fang { Fang(|c: &mut Context| { c.set_headers() @@ -77,7 +77,7 @@ mod fangs { pub struct LogRequest; impl IntoFang for LogRequest { fn into_fang(self) -> Fang { - Fang(|req: &mut Request| { + Fang(|req: &Request| { let __method__ = req.method; let __path__ = req.path(); @@ -101,7 +101,7 @@ async fn main() { .with_max_level(tracing::Level::INFO) .init(); - let hello_ohkami = Ohkami::with((AppendServer, LogRequest), ( + let hello_ohkami = Ohkami::with((SetServer, LogRequest), ( "/query". GET(hello_handler::hello_by_query), "/json". diff --git a/examples/quick_start/src/main.rs b/examples/quick_start/src/main.rs index 33b86fc2..cb53e4bf 100644 --- a/examples/quick_start/src/main.rs +++ b/examples/quick_start/src/main.rs @@ -4,7 +4,7 @@ async fn health_check(c: Context) -> Response { c.NoContent() } -async fn hello(c: Context, name: String) -> Response { +async fn hello(c: Context, name: &str) -> Response { c.OK().text(format!("Hello, {name}!")) } diff --git a/ohkami/Cargo.toml b/ohkami/Cargo.toml index 8f6ea193..86885ec9 100644 --- a/ohkami/Cargo.toml +++ b/ohkami/Cargo.toml @@ -36,9 +36,9 @@ DEBUG = [ "tokio?/macros", "async-std?/attributes", ] -default = [ - "rt_tokio", - #"rt_async-std", - "DEBUG", - # "nightly" -] +#default = [ +# "rt_tokio", +# #"rt_async-std", +# "DEBUG", +# # "nightly" +#] \ No newline at end of file diff --git a/ohkami/src/layer1_req_res/request/from_request.rs b/ohkami/src/layer1_req_res/request/from_request.rs index 19ca79a9..62e8a266 100644 --- a/ohkami/src/layer1_req_res/request/from_request.rs +++ b/ohkami/src/layer1_req_res/request/from_request.rs @@ -7,7 +7,7 @@ use crate::{Request}; /// - `#[Query]` /// - `#[Payload(JSON)]` /// - `#[Payload(URLEncoded)]` -/// - ( `#[Payload(Form)]` ) +/// - `#[Payload(Form)]` /// /// implement this by default. /// @@ -15,39 +15,57 @@ use crate::{Request}; /// /// Of course, you can manually implement for any structs that can be extracted from request: /// -/// ```ignore +/// ``` +/// use ohkami::prelude::*; +/// /// struct HasPayload(bool); /// -/// impl FromRequest for HasPayload { -/// fn parse(req: &Request) -> Result { +/// impl ohkami::FromRequest<'_> for HasPayload { +/// type Error = std::convert::Infallible; +/// fn parse(req: &Request) -> Result { /// Ok(Self( -/// req.payload.is_some() +/// req.payload().is_some() /// )) /// } /// } /// ``` -pub trait FromRequest: Sized { +pub trait FromRequest<'req>: Sized { type Error: std::fmt::Display + 'static; - fn parse(req: &Request) -> Result; + fn parse(req: &'req Request) -> Result; } -pub trait FromParam: Sized { +pub trait FromParam<'p>: Sized { type Error: std::fmt::Display; - fn from_param(param: &str) -> Result; + fn from_param(param: Cow<'p, str>) -> Result; } const _: () = { - impl FromParam for String { - type Error = std::str::Utf8Error; - fn from_param(param: &str) -> Result { + impl<'p> FromParam<'p> for String { + type Error = std::convert::Infallible; + fn from_param(param: Cow<'p, str>) -> Result { Ok(param.to_string()) } } + impl<'p> FromParam<'p> for Cow<'p, str> { + type Error = std::convert::Infallible; + fn from_param(param: Cow<'p, str>) -> Result { + Ok(param) + } + } + impl<'p> FromParam<'p> for &'p str { + type Error = &'static str; + fn from_param(param: Cow<'p, str>) -> Result { + match param { + Cow::Borrowed(s) => Ok(s), + Cow::Owned(_) => Err("Found percent decoded `String`, can't convert into `&str` in `from_param`"), + } + } + } macro_rules! unsigned_integers { ($( $unsigned_int:ty ),*) => { $( - impl FromParam for $unsigned_int { + impl<'p> FromParam<'p> for $unsigned_int { type Error = Cow<'static, str>; - fn from_param(param: &str) -> Result { + fn from_param(param: Cow<'p, str>) -> Result { let digit_bytes = param.as_bytes(); if digit_bytes.is_empty() {return Err(Cow::Borrowed("Expected a number nut found an empty string"))} match digit_bytes[0] { diff --git a/ohkami/src/layer1_req_res/request/mod.rs b/ohkami/src/layer1_req_res/request/mod.rs index d06d5143..59b5e242 100644 --- a/ohkami/src/layer1_req_res/request/mod.rs +++ b/ohkami/src/layer1_req_res/request/mod.rs @@ -132,7 +132,7 @@ impl Request { percent_decode_utf8(unsafe {self.path.as_bytes()}).unwrap() } - #[inline] pub fn query(&self, key: &str) -> Option> { + #[inline] pub fn query<'req, Value: FromParam<'req>>(&'req self, key: &str) -> Option> { self.queries.get(key).map(Value::from_param) } pub fn append_query(&mut self, key: impl Into>, value: impl Into>) { diff --git a/ohkami/src/layer1_req_res/request/parse_payload.rs b/ohkami/src/layer1_req_res/request/parse_payload.rs index 9ddf5dbc..f6faf9ce 100644 --- a/ohkami/src/layer1_req_res/request/parse_payload.rs +++ b/ohkami/src/layer1_req_res/request/parse_payload.rs @@ -465,15 +465,15 @@ pub struct Parse<'a> { for (k, v) in crate::__internal__::parse_urlencoded(buf) { match &*k { - "id" => id.replace(::from_param(&v).map_err(|e| format!("{e:?}"))?) + "id" => id.replace(::from_param(v).map_err(|e| format!("{e:?}"))?) .map_or(::std::result::Result::Ok(()), |_| ::std::result::Result::Err(::std::borrow::Cow::Borrowed("duplicated key: `id`")) )?, - "name" => name.replace(::from_param(&v).map_err(|e| format!("{e:?}"))?) + "name" => name.replace(::from_param(v).map_err(|e| format!("{e:?}"))?) .map_or(::std::result::Result::Ok(()), |_| ::std::result::Result::Err(::std::borrow::Cow::Borrowed("duplicated key: `name`")) )?, - "age" => age.replace(::from_param(&v).map_err(|e| format!("{e:?}"))?) + "age" => age.replace(::from_param(v).map_err(|e| format!("{e:?}"))?) .map_or(::std::result::Result::Ok(()), |_| ::std::result::Result::Err(::std::borrow::Cow::Borrowed("duplicated key: `age`")), )?, @@ -500,15 +500,15 @@ pub struct Parse<'a> { for (k, v) in crate::__internal__::parse_urlencoded(buf) { match &*k { - "id" => id.replace(::from_param(&v).map_err(|e| Cow::Owned(format!("{e:?}")))?) + "id" => id.replace(::from_param(v).map_err(|e| Cow::Owned(format!("{e:?}")))?) .map_or(::std::result::Result::Ok(()), |_| ::std::result::Result::Err(::std::borrow::Cow::Borrowed("duplicated key: `id`")) )?, - "name" => name.replace(::from_param(&v).map_err(|e| Cow::Owned(format!("{e:?}")))?) + "name" => name.replace(::from_param(v).map_err(|e| Cow::Owned(format!("{e:?}")))?) .map_or(::std::result::Result::Ok(()), |_| ::std::result::Result::Err(::std::borrow::Cow::Borrowed("duplicated key: `name`")) )?, - "age" => age.replace(::from_param(&v).map_err(|e| Cow::Owned(format!("{e:?}")))?) + "age" => age.replace(::from_param(v).map_err(|e| Cow::Owned(format!("{e:?}")))?) .map_or(::std::result::Result::Ok(()), |_| ::std::result::Result::Err(::std::borrow::Cow::Borrowed("duplicated key: `age`")), )?, diff --git a/ohkami/src/layer1_req_res/request/queries.rs b/ohkami/src/layer1_req_res/request/queries.rs index f91ad49c..f790b85b 100644 --- a/ohkami/src/layer1_req_res/request/queries.rs +++ b/ohkami/src/layer1_req_res/request/queries.rs @@ -27,13 +27,16 @@ pub struct QueryParams { (std::str::from_utf8(k.as_bytes()).unwrap(), std::str::from_utf8(v.as_bytes()).unwrap()) }) } - #[inline] pub(crate) fn get(&self, key: &str) -> Option<&str> { + #[inline] pub(crate) fn get(&self, key: &str) -> Option> { let key = key.as_bytes(); for kv in unsafe {self.params.get_unchecked(0..self.next)} { unsafe { let (k, v) = kv.assume_init_ref(); if key == k.as_bytes() { - return Some((|| std::str::from_utf8(v.as_bytes()).unwrap())()) + return Some((|| match v { + CowSlice::Ref(slice) => Cow::Borrowed(std::str::from_utf8(slice.as_bytes()).unwrap()), + CowSlice::Own(vec) => Cow::Owned(String::from_utf8(vec.to_vec()).unwrap()), + })()) } } } @@ -66,7 +69,7 @@ const _: () = { impl PartialEq for QueryParams { fn eq(&self, other: &Self) -> bool { for (k, v) in self.iter() { - if other.get(k) != Some(v) { + if other.get(k) != Some(Cow::Borrowed(v)) { return false } } diff --git a/ohkami/src/layer3_fang_handler/handler/handlers.rs b/ohkami/src/layer3_fang_handler/handler/handlers.rs index e4e63e3c..132e2340 100644 --- a/ohkami/src/layer3_fang_handler/handler/handlers.rs +++ b/ohkami/src/layer3_fang_handler/handler/handlers.rs @@ -102,10 +102,10 @@ macro_rules! Route { pub const DB: __::Database = __::Database; mod __ { pub struct Database; impl Database { - pub async fn insert_returning_id(&self, Model: impl for<'de>serde::Deserialize<'de>) -> Result { + pub async fn insert_returning_id(&self, Model: impl serde::Deserialize<'_>) -> Result { Ok(42) } - pub async fn update_returning_id(&self, Model: impl for<'de>serde::Deserialize<'de>) -> Result { + pub async fn update_returning_id(&self, Model: impl serde::Deserialize<'_>) -> Result { Ok(24) } } @@ -113,12 +113,12 @@ macro_rules! Route { } #[derive(Deserialize)] - struct CreateUser { - name: String, - password: String, - } impl FromRequest for CreateUser { + struct CreateUser<'c> { + name: &'c str, + password: &'c str, + } impl<'req> FromRequest<'req> for CreateUser<'req> { type Error = Cow<'static, str>; - fn parse(req: &crate::Request) -> Result> { + fn parse(req: &'req crate::Request) -> Result> { let payload = req.payload().ok_or_else(|| Cow::Borrowed("Payload expected"))?; match req.headers.ContentType() { Some("application/json") => serde_json::from_slice(payload).map_err(|e| Cow::Owned(e.to_string())), @@ -127,28 +127,31 @@ macro_rules! Route { } } - async fn create_user(c: Context, payload: CreateUser) -> Response { + async fn create_user<'req>(c: Context, payload: CreateUser<'req>) -> Response { let CreateUser { name, password } = payload; if let Err(_) = mock::authenticate().await { return c.Unauthorized() } - let Ok(id) = mock::DB.insert_returning_id(CreateUser{ - name: name.clone(), - password: password.clone(), - }).await else {return c.InternalServerError()}; + let Ok(id) = mock::DB.insert_returning_id(CreateUser{ name, password }).await else { + return c.InternalServerError(); + }; - c.Created().json(User { id, name, password }) + c.Created().json(User { + id, + name: name.to_string(), + password: password.to_string(), + }) } #[derive(Deserialize)] - struct UpdateUser { - name: Option, - password: Option, - } impl FromRequest for UpdateUser { + struct UpdateUser<'u> { + name: Option<&'u str>, + password: Option<&'u str>, + } impl<'req> FromRequest<'req> for UpdateUser<'req> { type Error = Cow<'static, str>; - fn parse(req: &crate::Request) -> Result> { + fn parse(req: &'req crate::Request) -> Result> { let payload = req.payload().ok_or_else(|| Cow::Borrowed("Payload expected"))?; match req.headers.ContentType() { Some("application/json") => serde_json::from_slice(payload).map_err(|e| Cow::Owned(e.to_string())), @@ -157,17 +160,14 @@ macro_rules! Route { } } - async fn update_user(c: Context, req: UpdateUser) -> Response { - let UpdateUser { name, password } = req; - + async fn update_user<'req>(c: Context, body: UpdateUser<'req>) -> Response { if let Err(_) = mock::authenticate().await { return c.Unauthorized() } - if let Err(_) = mock::DB.update_returning_id(UpdateUser { - name: name.clone(), - password: password.clone(), - }).await {return c.InternalServerError()}; + if let Err(_) = mock::DB.update_returning_id(body).await { + return c.InternalServerError(); + }; c.NoContent() } diff --git a/ohkami/src/layer3_fang_handler/handler/into_handler.rs b/ohkami/src/layer3_fang_handler/handler/into_handler.rs index 8b9f18d4..b3010f94 100644 --- a/ohkami/src/layer3_fang_handler/handler/into_handler.rs +++ b/ohkami/src/layer3_fang_handler/handler/into_handler.rs @@ -4,7 +4,7 @@ use crate::{ Context, Response, layer0_lib::{percent_decode_utf8}, - layer1_req_res::{FromRequest, FromParam}, + layer1_req_res::{FromRequest, FromParam}, Request, }; #[cfg(feature="websocket")] use crate::websocket::WebSocketContext; @@ -23,15 +23,22 @@ pub trait IntoHandler { async {res} }) } -#[inline(always)] fn from_param_bytes( - param_bytes_maybe_percent_encoded: &[u8] +#[inline(always)] fn from_param_bytes<'p, P: FromParam<'p>>( + param_bytes_maybe_percent_encoded: &'p [u8] ) -> Result> { let param = percent_decode_utf8(param_bytes_maybe_percent_encoded) .map_err(|e| Cow::Owned(e.to_string()))?; -

::from_param(¶m) +

::from_param(param) .map_err(|e| Cow::Owned(e.to_string())) } +#[inline(always)] fn from_request<'fr, 'req, R: FromRequest<'fr>>( + req: &'req Request +) -> Result>::Error> { + ::parse(unsafe { + std::mem::transmute::<&'req _, &'fr _>(req) // + }) +} const _: (/* only Context */) = { impl IntoHandler<(Context,)> for F @@ -68,8 +75,29 @@ const _: (/* FromParam */) = { } with_single_path_param! { String, u8, u16, u32, u64, u128, usize } + impl<'req, F, Fut> IntoHandler<(Context, &'req str)> for F + where + F: Fn(Context, &'req str) -> Fut + Send + Sync + 'static, + Fut: Future + Send + Sync + 'static, + { + fn into_handler(self) -> Handler { + Handler::new(move |c, req| + match from_param_bytes(unsafe {req.path.assume_one_param()}) { + Ok(p1) => Box::pin(self(c, p1)), + Err(e) => __bad_request(&c, e) + } + ) + } + } + #[cfg(test)] fn __() { + async fn h1(_c: Context, _param: String) -> Response {todo!()} + async fn h2(_c: Context, _param: &str) -> Response {todo!()} + + let _ = h1.into_handler(); + let _ = h2.into_handler(); + } - impl IntoHandler<(Context, (P1,))> for F + impl<'req, F, Fut, P1:FromParam<'req>> IntoHandler<(Context, (P1,))> for F where F: Fn(Context, (P1,)) -> Fut + Send + Sync + 'static, Fut: Future + Send + Sync + 'static, @@ -86,7 +114,7 @@ const _: (/* FromParam */) = { } } - impl IntoHandler<(Context, (P1, P2))> for F + impl<'req, F, Fut, P1:FromParam<'req>, P2:FromParam<'req>> IntoHandler<(Context, (P1, P2))> for F where F: Fn(Context, (P1, P2)) -> Fut + Send + Sync + 'static, Fut: Future + Send + Sync + 'static, @@ -104,14 +132,14 @@ const _: (/* FromParam */) = { }; const _: (/* FromRequest items */) = { - impl IntoHandler<(Context, Item1)> for F + impl<'req, F, Fut, Item1:FromRequest<'req>> IntoHandler<(Context, Item1)> for F where F: Fn(Context, Item1) -> Fut + Send + Sync + 'static, Fut: Future + Send + Sync + 'static, { fn into_handler(self) -> Handler { Handler::new(move |c, req| - match Item1::parse(req) { + match from_request::(req) { Ok(item1) => Box::pin(self(c, item1)), Err(e) => __bad_request(&c, e) } @@ -119,14 +147,14 @@ const _: (/* FromRequest items */) = { } } - impl IntoHandler<(Context, Item1, Item2)> for F + impl<'req, F, Fut, Item1:FromRequest<'req>, Item2:FromRequest<'req>> IntoHandler<(Context, Item1, Item2)> for F where F: Fn(Context, Item1, Item2) -> Fut + Send + Sync + 'static, Fut: Future + Send + Sync + 'static, { fn into_handler(self) -> Handler { Handler::new(move |c, req| - match (Item1::parse(req), Item2::parse(req)) { + match (from_request::(req), from_request::(req)) { (Ok(item1), Ok(item2)) => Box::pin(self(c, item1, item2)), (Err(e), _) => __bad_request(&c, e), (_, Err(e)) => __bad_request(&c, e), @@ -139,7 +167,7 @@ const _: (/* FromRequest items */) = { const _: (/* single FromParam and FromRequest items */) = { macro_rules! with_single_path_param_and_from_request_items { ($( $param_type:ty ),*) => {$( - impl IntoHandler<(Context, $param_type, Item1)> for F + impl<'req, F, Fut, Item1:FromRequest<'req>> IntoHandler<(Context, $param_type, Item1)> for F where F: Fn(Context, $param_type, Item1) -> Fut + Send + Sync + 'static, Fut: Future + Send + Sync + 'static, @@ -150,7 +178,7 @@ const _: (/* single FromParam and FromRequest items */) = { // `params` has already `append`ed once before this code let p1 = unsafe {req.path.assume_one_param()}; - match (from_param_bytes(p1), Item1::parse(req)) { + match (from_param_bytes(p1), from_request(req)) { (Ok(p1), Ok(item1)) => Box::pin(self(c, p1, item1)), (Err(e), _) => __bad_request(&c, e), (_, Err(e)) => __bad_request(&c, e), @@ -159,7 +187,7 @@ const _: (/* single FromParam and FromRequest items */) = { } } - impl IntoHandler<(Context, $param_type, Item1, Item2)> for F + impl<'req, F, Fut, Item1:FromRequest<'req>, Item2:FromRequest<'req>> IntoHandler<(Context, $param_type, Item1, Item2)> for F where F: Fn(Context, $param_type, Item1, Item2) -> Fut + Send + Sync + 'static, Fut: Future + Send + Sync + 'static, @@ -170,7 +198,7 @@ const _: (/* single FromParam and FromRequest items */) = { // `params` has already `append`ed once before this code let p1 = unsafe {req.path.assume_one_param()}; - match (from_param_bytes(p1), Item1::parse(req), Item2::parse(req)) { + match (from_param_bytes(p1), from_request::(req), from_request::(req)) { (Ok(p1), Ok(item1), Ok(item2)) => Box::pin(self(c, p1, item1, item2)), (Err(e),_,_) => __bad_request(&c, e), (_,Err(e),_) => __bad_request(&c, e), @@ -181,12 +209,12 @@ const _: (/* single FromParam and FromRequest items */) = { } )*}; } with_single_path_param_and_from_request_items! { - String, u8, u16, u32, u64, u128, usize + String, &'req str, u8, u16, u32, u64, u128, usize } }; const _: (/* one FromParam and FromRequest items */) = { - impl IntoHandler<(Context, (P1,), Item1)> for F + impl<'req, F, Fut, P1:FromParam<'req>, Item1:FromRequest<'req>> IntoHandler<(Context, (P1,), Item1)> for F where F: Fn(Context, (P1,), Item1) -> Fut + Send + Sync + 'static, Fut: Future + Send + Sync + 'static, @@ -197,7 +225,7 @@ const _: (/* one FromParam and FromRequest items */) = { // `params` has already `append`ed once before this code let p1 = unsafe {req.path.assume_one_param()}; - match (from_param_bytes(p1), Item1::parse(req)) { + match (from_param_bytes(p1), from_request::(req)) { (Ok(p1), Ok(item1)) => Box::pin(self(c, (p1,), item1)), (Err(e),_) => __bad_request(&c, e), (_,Err(e)) => __bad_request(&c, e), @@ -206,7 +234,7 @@ const _: (/* one FromParam and FromRequest items */) = { } } - impl IntoHandler<(Context, (P1,), Item1, Item2)> for F + impl<'req, F, Fut, P1:FromParam<'req>, Item1:FromRequest<'req>, Item2:FromRequest<'req>> IntoHandler<(Context, (P1,), Item1, Item2)> for F where F: Fn(Context, (P1,), Item1, Item2) -> Fut + Send + Sync + 'static, Fut: Future + Send + Sync + 'static, @@ -217,7 +245,7 @@ const _: (/* one FromParam and FromRequest items */) = { // `params` has already `append`ed once before this code let p1 = unsafe {req.path.assume_one_param()}; - match (from_param_bytes(p1), Item1::parse(req), Item2::parse(req)) { + match (from_param_bytes(p1), from_request::(req), from_request::(req)) { (Ok(p1), Ok(item1), Ok(item2)) => Box::pin(self(c, (p1,), item1, item2)), (Err(e),_,_) => __bad_request(&c, e), (_,Err(e),_) => __bad_request(&c, e), @@ -229,7 +257,7 @@ const _: (/* one FromParam and FromRequest items */) = { }; const _: (/* two PathParams and FromRequest items */) = { - impl IntoHandler<(Context, (P1, P2), Item1)> for F + impl<'req, F, Fut, P1:FromParam<'req>, P2:FromParam<'req>, Item1:FromRequest<'req>> IntoHandler<(Context, (P1, P2), Item1)> for F where F: Fn(Context, (P1, P2), Item1) -> Fut + Send + Sync + 'static, Fut: Future + Send + Sync + 'static, @@ -240,7 +268,7 @@ const _: (/* two PathParams and FromRequest items */) = { // `params` has already `append`ed twice before this code let (p1, p2) = unsafe {req.path.assume_two_params()}; - match (from_param_bytes(p1), from_param_bytes(p2), Item1::parse(req)) { + match (from_param_bytes(p1), from_param_bytes(p2), from_request::(req)) { (Ok(p1), Ok(p2), Ok(item1)) => Box::pin(self(c, (p1, p2), item1)), (Err(e),_,_) => __bad_request(&c, e), (_,Err(e),_) => __bad_request(&c, e), @@ -250,7 +278,7 @@ const _: (/* two PathParams and FromRequest items */) = { } } - impl IntoHandler<(Context, (P1, P2), Item1, Item2)> for F + impl<'req, F, Fut, P1:FromParam<'req>, P2:FromParam<'req>, Item1:FromRequest<'req>, Item2:FromRequest<'req>> IntoHandler<(Context, (P1, P2), Item1, Item2)> for F where F: Fn(Context, (P1, P2), Item1, Item2) -> Fut + Send + Sync + 'static, Fut: Future + Send + Sync + 'static, @@ -261,7 +289,7 @@ const _: (/* two PathParams and FromRequest items */) = { // `params` has already `append`ed twice before this code let (p1, p2) = unsafe {req.path.assume_two_params()}; - match (from_param_bytes(p1), from_param_bytes(p2), Item1::parse(req), Item2::parse(req)) { + match (from_param_bytes(p1), from_param_bytes(p2), from_request::(req), from_request::(req)) { (Ok(p1), Ok(p2), Ok(item1), Ok(item2)) => Box::pin(self(c, (p1, p2), item1, item2)), (Err(e),_,_,_) => __bad_request(&c, e), (_,Err(e),_,_) => __bad_request(&c, e), @@ -275,7 +303,7 @@ const _: (/* two PathParams and FromRequest items */) = { #[cfg(feature="websocket")] const _: (/* requires upgrade to websocket */) = { - impl IntoHandler<(WebSocketContext,)> for F + impl<'req, F, Fut> IntoHandler<(WebSocketContext,)> for F where F: Fn(WebSocketContext) -> Fut + Send + Sync + 'static, Fut: Future + Send + Sync + 'static, @@ -290,7 +318,7 @@ const _: (/* requires upgrade to websocket */) = { } } - impl IntoHandler<(WebSocketContext, P1)> for F + impl<'req, F, Fut, P1:FromParam<'req>> IntoHandler<(WebSocketContext, P1)> for F where F: Fn(WebSocketContext, P1) -> Fut + Send + Sync + 'static, Fut: Future + Send + Sync + 'static, @@ -308,7 +336,7 @@ const _: (/* requires upgrade to websocket */) = { }).requires_upgrade() } } - impl IntoHandler<(WebSocketContext, P1, P2)> for F + impl<'req, F, Fut, P1:FromParam<'req>, P2:FromParam<'req>> IntoHandler<(WebSocketContext, P1, P2)> for F where F: Fn(WebSocketContext, P1, P2) -> Fut + Send + Sync + 'static, Fut: Future + Send + Sync + 'static, @@ -326,7 +354,7 @@ const _: (/* requires upgrade to websocket */) = { }).requires_upgrade() } } - impl IntoHandler<(WebSocketContext, (P1,))> for F + impl<'req, F, Fut, P1:FromParam<'req>> IntoHandler<(WebSocketContext, (P1,))> for F where F: Fn(WebSocketContext, (P1,)) -> Fut + Send + Sync + 'static, Fut: Future + Send + Sync + 'static, @@ -344,7 +372,7 @@ const _: (/* requires upgrade to websocket */) = { }).requires_upgrade() } } - impl IntoHandler<(WebSocketContext, (P1, P2))> for F + impl<'req, F, Fut, P1:FromParam<'req>, P2:FromParam<'req>> IntoHandler<(WebSocketContext, (P1, P2))> for F where F: Fn(WebSocketContext, (P1, P2)) -> Fut + Send + Sync + 'static, Fut: Future + Send + Sync + 'static, diff --git a/ohkami/src/layer3_fang_handler/handler/mod.rs b/ohkami/src/layer3_fang_handler/handler/mod.rs index e5b2c091..c924b59a 100644 --- a/ohkami/src/layer3_fang_handler/handler/mod.rs +++ b/ohkami/src/layer3_fang_handler/handler/mod.rs @@ -25,14 +25,14 @@ pub struct Handler { > } - impl Handler { - fn new( - proc: (impl Fn(Context, &mut Request) -> Pin< - Box - + Send + 'static - > + fn new<'h>( + proc: ( + impl Fn(Context, &mut Request) -> Pin< + Box + + Send + 'static + > > + Send + Sync + 'static ) ) -> Self { diff --git a/ohkami/src/layer4_router/radix.rs b/ohkami/src/layer4_router/radix.rs index 15befd05..ac5debb8 100644 --- a/ohkami/src/layer4_router/radix.rs +++ b/ohkami/src/layer4_router/radix.rs @@ -198,11 +198,12 @@ impl Node { } for pattern in target.patterns { - if &path[0] == &b'/' {path = &path[1..]} else { + if path.is_empty() || unsafe {path.get_unchecked(0)} != &b'/' { // At least one `pattern` to match is remaining // but path doesn't start with '/' return Ok(None) } + path = unsafe {path.get_unchecked(1..)}; match pattern { Pattern::Static(s) => path = match path.strip_prefix(*s) { Some(remaining) => remaining, diff --git a/ohkami/src/layer5_ohkami/mod.rs b/ohkami/src/layer5_ohkami/mod.rs index 7f903ffd..fc6aab6d 100644 --- a/ohkami/src/layer5_ohkami/mod.rs +++ b/ohkami/src/layer5_ohkami/mod.rs @@ -88,8 +88,29 @@ use crate::{ /// - async (`Context`, {`FromRequest` values...}) -> `Response` /// - async (`Context`, {path_params}, {`FromRequest` values...}) -> `Response` /// -/// path_param:A type that impls `FromParam`, or a tuple of `FromParam` types +/// #### path_param: +/// A tuple of types that implement `FromParam` trait.\ +/// `String`, `&str`, and primitive integers are splecially allowed to be used without tuple: /// +/// ``` +/// use ohkami::prelude::*; +/// +/// struct MyParam; +/// impl<'p> ohkami::FromParam<'p> for MyParam { +/// type Error = std::convert::Infallible; +/// fn from_param(param: std::borrow::Cow<'p, str>) -> Result { +/// Ok(MyParam) +/// } +/// } +/// +/// async fn handler_1(c: Context, param: (MyParam,)) -> Response { +/// todo!() +/// } +/// +/// async fn handler_2(c: Context, str_param: &str) -> Response { +/// todo!() +/// } +/// ``` pub struct Ohkami { pub(crate) routes: TrieRouter, diff --git a/ohkami/src/layer6_testing/_test.rs b/ohkami/src/layer6_testing/_test.rs index 0a237497..32d9841e 100644 --- a/ohkami/src/layer6_testing/_test.rs +++ b/ohkami/src/layer6_testing/_test.rs @@ -63,7 +63,7 @@ async fn hello(c: Context) -> Response { let res = testing_example.oneshot(TestRequest::PUT("/users") .json(CreateUser { - name: format!("kanarus"), + name: "kanarus", age: None, })).await; assert_eq!(res.status(), Status::Created); @@ -106,14 +106,14 @@ async fn get_user(c: Context, id: usize) -> Response { #[derive(serde::Deserialize)] #[cfg_attr(test, derive(serde::Serialize))] -struct CreateUser { - name: String, +struct CreateUser<'c> { + name: &'c str, age: Option, } // Can't use `#[Payload(JSON)]` here becasue this test is within `ohkami` -impl crate::FromRequest for CreateUser { +impl<'req> crate::FromRequest<'req> for CreateUser<'req> { type Error = ::std::borrow::Cow<'static, str>; - fn parse(req: &Request) -> Result> { + fn parse(req: &'req Request) -> Result> { let Some(payload) = req.payload() else {return Err(::std::borrow::Cow::Borrowed("Expected a payload"))}; match req.headers.ContentType() { @@ -123,9 +123,9 @@ impl crate::FromRequest for CreateUser { } } } -async fn create_user(c: Context, payload: CreateUser) -> Response { +async fn create_user<'h>(c: Context, payload: CreateUser<'h>) -> Response { c.Created().json(User { - name: payload.name, + name: payload.name.to_string(), age: payload.age.unwrap_or(0), }) } diff --git a/ohkami/src/lib.rs b/ohkami/src/lib.rs index ee873807..83b28c34 100644 --- a/ohkami/src/lib.rs +++ b/ohkami/src/lib.rs @@ -209,9 +209,9 @@ mod __rt__ { #[cfg(all(feature="rt_async-std", feature="DEBUG"))] pub(crate) use async_std::test; - #[cfg(feature="rt_tokio")] + #[cfg(all(feature="websocket", feature="rt_tokio"))] pub(crate) use tokio::net::TcpStream; - #[cfg(feature="rt_async-std")] + #[cfg(all(feature="websocket", feature="rt_async-std"))] pub(crate) use async_std::net::TcpStream; #[cfg(feature="rt_tokio")] @@ -334,7 +334,7 @@ pub mod __internal__ { c.NoContent() } - async fn hello(c: Context, name: String) -> Response { + async fn hello(c: Context, name: &str) -> Response { c.OK().text(format!("Hello, {name}!")) } diff --git a/ohkami/src/x_utils/now.rs b/ohkami/src/x_utils/now.rs index 5851fa4a..a110f85a 100644 --- a/ohkami/src/x_utils/now.rs +++ b/ohkami/src/x_utils/now.rs @@ -31,7 +31,7 @@ struct UTCDateTime { const SHORT_MONTHS: [&str; 12] = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; fn push_hundreds(buf: &mut String, n: u8) { - debug_assert!(n < 100, "Called `push_hundreds` for `n` greater than 100"); + debug_assert!(n < 100, "Called `push_hundreds` for `n` that's 100 or greater"); buf.push((n/10 + b'0') as char); buf.push((n%10 + b'0') as char); } @@ -45,6 +45,7 @@ struct UTCDateTime { let day = date.day() as u8; if day < 10 { + buf.push('0'); buf.push((day + b'0') as char); } else { push_hundreds(&mut buf, day); diff --git a/ohkami_macros/src/components.rs b/ohkami_macros/src/components.rs index a4388be6..1dcd9e2d 100644 --- a/ohkami_macros/src/components.rs +++ b/ohkami_macros/src/components.rs @@ -1,8 +1,14 @@ use proc_macro2::{TokenStream, Span, Ident}; use quote::{format_ident, ToTokens}; -use syn::{Result, Error, parse2, ItemStruct, Attribute, PathSegment, Type, Fields, parse_str}; +use syn::{Result, Error, parse2, ItemStruct, Attribute, PathSegment, Type, Fields, parse_str, GenericParam, Lifetime, LifetimeDef}; +pub(crate) fn from_request_lifetime() -> GenericParam { + GenericParam::Lifetime(LifetimeDef::new( + Lifetime::new("'__impl_from_request_lifetime", Span::call_site()) + )) +} + pub(crate) struct FieldData { pub(crate) ident: Ident, pub(crate) ty: Type, @@ -83,7 +89,7 @@ pub(crate) fn parse_struct(macro_name: &str, input: TokenStream) -> Result Result 0 { + if struct_tokens.generics.lifetimes().count() >= 2 { return Err(Error::new(Span::call_site(), format!( - "`#[{macro_name}]` doesn't support lifetime params" + "`#[{macro_name}]` doesn't support multiple lifetime params" ))) } diff --git a/ohkami_macros/src/lib.rs b/ohkami_macros/src/lib.rs index d5df2a37..e4a783b1 100644 --- a/ohkami_macros/src/lib.rs +++ b/ohkami_macros/src/lib.rs @@ -106,7 +106,9 @@ pub fn Query(_: proc_macro::TokenStream, data: proc_macro::TokenStream) -> proc_ /// /// ### Form /// -/// - Available value types : `String` or `File` or `Vec`. +/// **NOTE**:This can't handle reference types like `&str` in current version. Wait for the development! +/// +/// - Available value types : `String`, `File`, `Vec`. /// - Form part of kebab-case-name is handled by field of snake_case version of the name ( example: `name="submitter-name"` is handled by field `submitter_name` ). /// /// diff --git a/ohkami_macros/src/payload.rs b/ohkami_macros/src/payload.rs index 8448ecfc..c8f8ee36 100644 --- a/ohkami_macros/src/payload.rs +++ b/ohkami_macros/src/payload.rs @@ -24,11 +24,23 @@ pub(super) fn Payload(format: TokenStream, data: TokenStream) -> Result Result { let struct_name = &data.ident; + + let (impl_lifetime, struct_lifetime) = match data.generics.lifetimes().count() { + 0 => ( + from_request_lifetime(), + None, + ), + 1 => ( + data.generics.params.first().unwrap().clone(), + Some(data.generics.params.first().unwrap().clone()), + ), + _ => return Err(syn::Error::new(Span::call_site(), "#[Payload] doesn't support multiple lifetime params")), + }; Ok(quote!{ - impl ::ohkami::FromRequest for #struct_name { + impl<#impl_lifetime> ::ohkami::FromRequest<#impl_lifetime> for #struct_name<#struct_lifetime> { type Error = ::std::borrow::Cow<'static, str>; - fn parse<'req>(req: &'req ::ohkami::Request) -> ::std::result::Result> { + fn parse(req: &#impl_lifetime ::ohkami::Request) -> ::std::result::Result> { let payload = req.payload() .ok_or_else(|| ::std::borrow::Cow::Borrowed("Expected payload"))?; if !req.headers.ContentType().unwrap().starts_with("application/json") { @@ -45,6 +57,18 @@ fn impl_payload_urlencoded(data: &ItemStruct) -> Result { let struct_name = &data.ident; let fields_data = FieldData::collect_from_struct_fields(&data.fields)?; + let (impl_lifetime, struct_lifetime) = match data.generics.lifetimes().count() { + 0 => ( + from_request_lifetime(), + None, + ), + 1 => ( + data.generics.params.first().unwrap().clone(), + Some(data.generics.params.first().unwrap().clone()), + ), + _ => return Err(syn::Error::new(Span::call_site(), "#[Payload] doesn't support multiple lifetime params")), + }; + let declaring_exprs = { let exprs = fields_data.iter().map(|FieldData { ident, ty, .. }| { quote!{ @@ -96,9 +120,9 @@ fn impl_payload_urlencoded(data: &ItemStruct) -> Result { }; Ok(quote!{ - impl ::ohkami::FromRequest for #struct_name { + impl<#impl_lifetime> ::ohkami::FromRequest<#impl_lifetime> for #struct_name<#struct_lifetime> { type Error = ::std::borrow::Cow<'static, str>; - fn parse(req: &::ohkami::Request) -> ::std::result::Result> { + fn parse(req: &#impl_lifetime ::ohkami::Request) -> ::std::result::Result> { let payload = req.payload() .ok_or_else(|| ::std::borrow::Cow::Borrowed("Expected a payload"))?; if !req.headers.ContentType().unwrap().starts_with("application/x-www-form-urlencoded") { @@ -118,6 +142,18 @@ fn impl_payload_formdata(data: &ItemStruct) -> Result { let struct_name = &data.ident; let fields_data = FieldData::collect_from_struct_fields(&data.fields)?; + let (impl_lifetime, struct_lifetime) = match data.generics.lifetimes().count() { + 0 => ( + from_request_lifetime(), + None, + ), + 1 => ( + data.generics.params.first().unwrap().clone(), + Some(data.generics.params.first().unwrap().clone()), + ), + _ => return Err(syn::Error::new(Span::call_site(), "#[Payload] doesn't support multiple lifetime params")), + }; + // `#[Payload(Form)]` doesn't accept optional fields if fields_data.iter().any(|FieldData { is_optional, .. }| *is_optional) { return Err(syn::Error::new(Span::mixed_site(), "`Option<_>` is not available in `#[Payload(Form)]`")) @@ -188,9 +224,9 @@ fn impl_payload_formdata(data: &ItemStruct) -> Result { }; Ok(quote!{ - impl ::ohkami::FromRequest for #struct_name { + impl<#impl_lifetime> ::ohkami::FromRequest<#impl_lifetime> for #struct_name<#struct_lifetime> { type Error = ::std::borrow::Cow<'static, str>; - fn parse(req: &::ohkami::Request) -> ::std::result::Result> { + fn parse(req: &#impl_lifetime ::ohkami::Request) -> ::std::result::Result> { let payload = req.payload() .ok_or_else(|| ::std::borrow::Cow::Borrowed("Expected a payload"))?; diff --git a/ohkami_macros/src/query.rs b/ohkami_macros/src/query.rs index 8f0b8cb0..5e986d2d 100644 --- a/ohkami_macros/src/query.rs +++ b/ohkami_macros/src/query.rs @@ -1,4 +1,4 @@ -use proc_macro2::{TokenStream}; +use proc_macro2::{TokenStream, Span}; use quote::{quote, ToTokens}; use syn::{Result, parse_str, Type}; @@ -11,7 +11,18 @@ pub(super) fn Query(data: TokenStream) -> Result { let impl_from_request = { let struct_name = &data.ident; - let lifetimes = &data.generics; // checked to only contains lifetimes in `parse_struct` + + let (impl_lifetime, struct_lifetime) = match &data.generics.lifetimes().count() { + 0 => ( + from_request_lifetime(), + None, + ), + 1 => ( + data.generics.params.first().unwrap().clone(), + Some(data.generics.params.first().unwrap().clone()), + ), + _ => return Err(syn::Error::new(Span::call_site(), "#[Query] doesn't support multiple lifetime params")) + }; let fields = data.fields.iter().map(|f| { let field_name = f.ident.as_ref().unwrap(/* already checked in `parse_struct` */); @@ -38,9 +49,9 @@ pub(super) fn Query(data: TokenStream) -> Result { }); quote!{ - impl #lifetimes ::ohkami::FromRequest for #struct_name #lifetimes { + impl<#impl_lifetime> ::ohkami::FromRequest<#impl_lifetime> for #struct_name<#struct_lifetime> { type Error = ::std::borrow::Cow<'static, str>; - fn parse(req: &::ohkami::Request) -> ::std::result::Result> { + fn parse(req: &#impl_lifetime ::ohkami::Request) -> ::std::result::Result> { ::std::result::Result::Ok(Self { #( #fields )* })