diff --git a/Cargo.lock b/Cargo.lock index 47e54559..602cf0bf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2708,6 +2708,7 @@ dependencies = [ name = "volo-http" version = "0.0.0" dependencies = [ + "async-trait", "bytes", "futures-util", "http", diff --git a/examples/src/http/http.rs b/examples/src/http/http.rs index b8c883f0..fd99e1a9 100644 --- a/examples/src/http/http.rs +++ b/examples/src/http/http.rs @@ -1,11 +1,12 @@ use std::{convert::Infallible, net::SocketAddr}; use bytes::Bytes; -use http::{Response, StatusCode}; +use http::{Method, Response, StatusCode, Uri}; use hyper::body::Incoming; use motore::service::service_fn; use serde::{Deserialize, Serialize}; use volo_http::{ + handler::HandlerService, request::Json, route::{Route, Router}, HttpContext, @@ -51,12 +52,34 @@ async fn json( Ok(Response::new(())) } +async fn test( + u: Uri, + m: Method, + Json(request): Json, +) -> Result<&'static str, (StatusCode, &'static str)> { + println!("{u:?}"); + println!("{m:?}"); + println!("{request:?}"); + if u.to_string().ends_with("a") { + Ok("a") // http://localhost:3000/test?a=a + } else { + Err((StatusCode::BAD_REQUEST, "b")) // http://localhost:3000/test?a=bb + } +} + #[tokio::main(flavor = "multi_thread")] async fn main() { Router::build() .route("/", Route::builder().get(service_fn(hello)).build()) .route("/:echo", Route::builder().get(service_fn(echo)).build()) .route("/user", Route::builder().post(service_fn(json)).build()) + .route( + "/test", + Route::builder() + .get(HandlerService::new(test)) + .post(HandlerService::new(test)) + .build(), + ) .serve(SocketAddr::from(([127, 0, 0, 1], 3000))) .await .unwrap(); diff --git a/volo-http/Cargo.toml b/volo-http/Cargo.toml index 898f86fb..6f56dce4 100644 --- a/volo-http/Cargo.toml +++ b/volo-http/Cargo.toml @@ -28,6 +28,7 @@ serde_json = "1" thiserror.workspace = true mime = "0.3" serde = "1" +async-trait.workspace = true [dev-dependencies] serde = { version = "1", features = ["derive"] } diff --git a/volo-http/src/extract.rs b/volo-http/src/extract.rs new file mode 100644 index 00000000..f7350f46 --- /dev/null +++ b/volo-http/src/extract.rs @@ -0,0 +1,38 @@ +use http::{Method, Response, Uri}; + +use crate::{response::IntoResponse, HttpContext}; + +#[async_trait::async_trait] +pub trait FromContext: Sized { + type Rejection: IntoResponse; + async fn from_context(context: &HttpContext) -> Result; +} +#[async_trait::async_trait] +impl FromContext for Option +where + T: FromContext, +{ + type Rejection = Response<()>; // Infallible + + async fn from_context(context: &HttpContext) -> Result { + Ok(T::from_context(context).await.ok()) + } +} + +#[async_trait::async_trait] +impl FromContext for Uri { + type Rejection = Response<()>; // Infallible + + async fn from_context(context: &HttpContext) -> Result { + Ok(context.uri.clone()) + } +} + +#[async_trait::async_trait] +impl FromContext for Method { + type Rejection = Response<()>; + + async fn from_context(context: &HttpContext) -> Result { + Ok(context.method.clone()) + } +} diff --git a/volo-http/src/handler.rs b/volo-http/src/handler.rs new file mode 100644 index 00000000..aaf88fb9 --- /dev/null +++ b/volo-http/src/handler.rs @@ -0,0 +1,123 @@ +use std::{future::Future, marker::PhantomData}; + +use http::Response; +use hyper::body::Incoming; + +use crate::{ + extract::FromContext, + request::FromRequest, + response::{IntoResponse, RespBody}, + HttpContext, +}; + +impl Clone for HandlerService +where + H: Clone, +{ + fn clone(&self) -> Self { + Self { + h: self.h.clone(), + _mark: PhantomData, + } + } +} +pub trait Handler { + type Future<'r>: Future> + Send + 'r + where + Self: 'r; + fn call(self, context: &mut HttpContext, req: Incoming) -> Self::Future<'_>; +} + +macro_rules! impl_handler { + ( + [$($ty:ident),*], $last:ident + ) => { + #[allow(non_snake_case, unused_mut, unused_variables)] + impl Handler<($($ty,)* $last,)> for F + where + F: FnOnce($($ty,)* $last) -> Fut + Clone + Send, + Fut: Future + Send, + $( for<'r> $ty: FromContext + Send + 'r, )* + for<'r> $last: FromRequest + Send + 'r, + Res: IntoResponse, + { + type Future<'r> = impl Future> + Send + 'r + where Self: 'r; + + fn call(self, context: &mut HttpContext, req: Incoming) -> Self::Future<'_> { + async move { + $( + let $ty = match $ty::from_context(context).await { + Ok(value) => value, + Err(rejection) => return rejection.into_response(), + }; + )* + let $last = match $last::from(context, req).await { + Ok(value) => value, + Err(rejection) => return rejection, + }; + self($($ty,)* $last).await.into_response() + } + } + } + }; +} + +impl_handler!([], T1); +impl_handler!([T1], T2); +impl_handler!([T1, T2], T3); +impl_handler!([T1, T2, T3], T4); +impl_handler!([T1, T2, T3, T4], T5); +impl_handler!([T1, T2, T3, T4, T5], T6); +impl_handler!([T1, T2, T3, T4, T5, T6], T7); +impl_handler!([T1, T2, T3, T4, T5, T6, T7], T8); +impl_handler!([T1, T2, T3, T4, T5, T6, T7, T8], T9); +impl_handler!([T1, T2, T3, T4, T5, T6, T7, T8, T9], T10); +impl_handler!([T1, T2, T3, T4, T5, T6, T7, T8, T9, T10], T11); +impl_handler!([T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11], T12); +impl_handler!([T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12], T13); +impl_handler!( + [T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13], + T14 +); +impl_handler!( + [T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14], + T15 +); +impl_handler!( + [T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15], + T16 +); + +pub struct HandlerService { + h: H, + _mark: PhantomData, +} + +impl HandlerService { + pub fn new(h: H) -> Self { + Self { + h, + _mark: PhantomData, + } + } +} + +impl motore::Service for HandlerService +where + for<'r> H: Handler + Clone + Send + Sync + 'r, +{ + type Response = Response; + type Error = http::Error; + type Future<'cx> = impl Future> + Send + 'cx + where + HttpContext: 'cx, + Self: 'cx; + + fn call<'cx, 's>(&'s self, cx: &'cx mut HttpContext, req: Incoming) -> Self::Future<'cx> + where + 's: 'cx, + { + async move { Ok(self.h.clone().call(cx, req).await) } + } +} diff --git a/volo-http/src/lib.rs b/volo-http/src/lib.rs index c076c34c..bd5d3551 100644 --- a/volo-http/src/lib.rs +++ b/volo-http/src/lib.rs @@ -1,6 +1,8 @@ #![feature(impl_trait_in_assoc_type)] pub(crate) mod dispatch; +pub mod extract; +pub mod handler; pub mod layer; pub mod param; pub mod request; diff --git a/volo-http/src/request.rs b/volo-http/src/request.rs index 7891728b..03f58c2f 100644 --- a/volo-http/src/request.rs +++ b/volo-http/src/request.rs @@ -5,7 +5,11 @@ use http_body_util::BodyExt; use hyper::body::Incoming; use serde::de::DeserializeOwned; -use crate::{response::RespBody, HttpContext}; +use crate::{ + extract::FromContext, + response::{IntoResponse, RespBody}, + HttpContext, +}; pub trait FromRequest: Sized { type FromFut<'cx>: Future>> + Send + 'cx @@ -15,6 +19,24 @@ pub trait FromRequest: Sized { fn from(cx: &HttpContext, body: Incoming) -> Self::FromFut<'_>; } +impl FromRequest for T +where + T: FromContext, +{ + type FromFut<'cx> = impl Future>> + Send + 'cx + where + Self: 'cx; + + fn from(cx: &HttpContext, _body: Incoming) -> Self::FromFut<'_> { + async move { + match T::from_context(cx).await { + Ok(value) => Ok(value), + Err(rejection) => Err(rejection.into_response()), + } + } + } +} + impl FromRequest for Incoming { type FromFut<'cx> = impl Future>> + Send + 'cx where diff --git a/volo-http/src/response.rs b/volo-http/src/response.rs index 76505198..dd3acf85 100644 --- a/volo-http/src/response.rs +++ b/volo-http/src/response.rs @@ -4,6 +4,7 @@ use std::{ }; use futures_util::{ready, stream}; +use http::{Response, StatusCode}; use http_body_util::{Full, StreamBody}; use hyper::body::{Body, Bytes, Frame}; use pin_project_lite::pin_project; @@ -77,3 +78,62 @@ impl From<()> for RespBody { } } } + +pub trait IntoResponse { + fn into_response(self) -> Response; +} + +impl IntoResponse for Response +where + T: Into, +{ + fn into_response(self) -> Response { + let (parts, body) = self.into_parts(); + Response::from_parts(parts, body.into()) + } +} + +impl IntoResponse for T +where + T: Into, +{ + fn into_response(self) -> Response { + Response::builder() + .status(StatusCode::OK) + .body(self.into()) + .unwrap() + } +} + +impl IntoResponse for Result +where + R: IntoResponse, + E: IntoResponse, +{ + fn into_response(self) -> Response { + match self { + Ok(value) => value.into_response(), + Err(err) => err.into_response(), + } + } +} + +impl IntoResponse for (StatusCode, T) +where + T: IntoResponse, +{ + fn into_response(self) -> Response { + let mut resp = self.1.into_response(); + *resp.status_mut() = self.0; + resp + } +} + +impl IntoResponse for StatusCode { + fn into_response(self) -> Response { + Response::builder() + .status(self) + .body(String::new().into()) + .unwrap() + } +}