diff --git a/Cargo.lock b/Cargo.lock index fe588da2..47e54559 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -514,9 +514,18 @@ version = "0.0.0" dependencies = [ "anyhow", "async-stream", +<<<<<<< HEAD +======= + "async-trait", + "bytes", + "http", + "hyper 1.0.0-rc.4", +>>>>>>> init "lazy_static", "metainfo", + "motore", "pilota", + "serde", "tokio", "tokio-stream", "tracing", @@ -524,6 +533,7 @@ dependencies = [ "volo", "volo-gen", "volo-grpc", + "volo-http", "volo-thrift", ] @@ -788,6 +798,29 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "http-body" +version = "1.0.0-rc.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "951dfc2e32ac02d67c90c0d65bd27009a635dc9b381a2cc7d284ab01e3a0150d" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.0-rc.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08ef12f041acdd397010e5fb6433270c147d3b8b2d0a840cd7fff8e531dca5c8" +dependencies = [ + "bytes", + "futures-util", + "http", + "http-body 1.0.0-rc.2", + "pin-project-lite", +] + [[package]] name = "httparse" version = "1.8.0" @@ -818,7 +851,7 @@ dependencies = [ "futures-util", "h2", "http", - "http-body", + "http-body 0.4.5", "httparse", "httpdate", "itoa", @@ -830,6 +863,27 @@ dependencies = [ "want", ] +[[package]] +name = "hyper" +version = "1.0.0-rc.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d280a71f348bcc670fc55b02b63c53a04ac0bf2daff2980795aeaf53edae10e6" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "h2", + "http", + "http-body 1.0.0-rc.2", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "tokio", + "tracing", + "want", +] + [[package]] name = "hyper-rustls" version = "0.24.1" @@ -838,7 +892,7 @@ checksum = "8d78e1e73ec14cf7375674f74d7dde185c8206fd9dea6fb6295e8a98098aaa97" dependencies = [ "futures-util", "http", - "hyper", + "hyper 0.14.27", "rustls", "tokio", "tokio-rustls", @@ -850,12 +904,30 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" dependencies = [ - "hyper", + "hyper 0.14.27", "pin-project-lite", "tokio", "tokio-io-timeout", ] +[[package]] +name = "hyper-util" +version = "0.0.0" +source = "git+https://github.com/hyperium/hyper-util.git#f898015fc9eca9f459ddac521db278d904099e89" +dependencies = [ + "futures-channel", + "futures-util", + "http", + "hyper 1.0.0-rc.4", + "once_cell", + "pin-project-lite", + "socket2 0.4.9", + "tokio", + "tower", + "tower-service", + "tracing", +] + [[package]] name = "iana-time-zone" version = "0.1.58" @@ -1731,8 +1803,8 @@ dependencies = [ "futures-util", "h2", "http", - "http-body", - "hyper", + "http-body 0.4.5", + "hyper 0.14.27", "hyper-rustls", "ipnet", "js-sys", @@ -1944,9 +2016,15 @@ dependencies = [ [[package]] name = "serde_json" +<<<<<<< HEAD version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65" +======= +version = "1.0.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "693151e1ac27563d6dbcec9dee9fbd5da8539b20fa14ad3752b2e6d363ace360" +>>>>>>> init dependencies = [ "itoa", "ryu", @@ -2608,8 +2686,8 @@ dependencies = [ "h2", "hex", "http", - "http-body", - "hyper", + "http-body 0.4.5", + "hyper 0.14.27", "hyper-timeout", "matchit", "metainfo", @@ -2629,6 +2707,23 @@ dependencies = [ [[package]] name = "volo-http" version = "0.0.0" +dependencies = [ + "bytes", + "futures-util", + "http", + "http-body-util", + "hyper 1.0.0-rc.4", + "hyper-util", + "matchit", + "mime", + "motore", + "pin-project-lite", + "serde", + "serde_json", + "thiserror", + "tokio", + "tracing", +] [[package]] name = "volo-macros" diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 33097d14..33b933af 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -62,6 +62,10 @@ path = "src/unknown/thrift_server.rs" name = "unknown-thrift-client" path = "src/unknown/thrift_client.rs" +[[bin]] +name = "http" +path = "src/http/http.rs" + [dependencies] anyhow.workspace = true @@ -77,5 +81,11 @@ pilota.workspace = true volo = { path = "../volo" } volo-grpc = { path = "../volo-grpc" } volo-thrift = { path = "../volo-thrift" } +volo-http = { path = "../volo-http" } volo-gen = { path = "./volo-gen" } +bytes.workspace = true +http.workspace = true +hyper = { version = "1.0.0-rc.4", features = ["server", "http1", "http2"] } +motore.workspace = true +serde.workspace = true diff --git a/examples/src/http/http.rs b/examples/src/http/http.rs new file mode 100644 index 00000000..b8c883f0 --- /dev/null +++ b/examples/src/http/http.rs @@ -0,0 +1,63 @@ +use std::{convert::Infallible, net::SocketAddr}; + +use bytes::Bytes; +use http::{Response, StatusCode}; +use hyper::body::Incoming; +use motore::service::service_fn; +use serde::{Deserialize, Serialize}; +use volo_http::{ + request::Json, + route::{Route, Router}, + HttpContext, +}; + +async fn hello( + _cx: &mut HttpContext, + _request: Incoming, +) -> Result, Infallible> { + Ok(Response::new("hello, world")) +} + +async fn echo(cx: &mut HttpContext, _request: Incoming) -> Result, Infallible> { + if let Some(echo) = cx.params.get("echo") { + return Ok(Response::new(echo.clone())); + } + Ok(Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Bytes::new()) + .unwrap()) +} + +#[derive(Serialize, Deserialize, Debug)] +struct Person { + name: String, + age: u8, + phones: Vec, +} + +async fn json( + _cx: &mut HttpContext, + Json(request): Json, +) -> Result, Infallible> { + let first_phone = request + .phones + .get(0) + .map(|p| p.as_str()) + .unwrap_or("no number"); + println!( + "{} is {} years old, {}'s first phone number is {}", + request.name, request.age, request.name, first_phone + ); + Ok(Response::new(())) +} + +#[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()) + .serve(SocketAddr::from(([127, 0, 0, 1], 3000))) + .await + .unwrap(); +} diff --git a/volo-http/Cargo.toml b/volo-http/Cargo.toml index 1f384e27..898f86fb 100644 --- a/volo-http/Cargo.toml +++ b/volo-http/Cargo.toml @@ -12,6 +12,22 @@ readme = "README.md" categories = ["asynchronous", "network-programming", "web-programming"] keywords = ["async", "http"] -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - [dependencies] +hyper = { version = "1.0.0-rc.4", features = ["server", "http1", "http2"] } +tokio = { version = "1", features = ["full"] } +http-body-util = "0.1.0-rc.3" +http = { version = "0.2" } +hyper-util = { git = "https://github.com/hyperium/hyper-util.git" } +matchit = { version = "0.7" } +motore = { version = "0.3" } +tracing.workspace = true +futures-util.workspace = true +pin-project-lite = "0.2" +bytes.workspace = true +serde_json = "1" +thiserror.workspace = true +mime = "0.3" +serde = "1" + +[dev-dependencies] +serde = { version = "1", features = ["derive"] } diff --git a/volo-http/src/dispatch.rs b/volo-http/src/dispatch.rs new file mode 100644 index 00000000..e593cd57 --- /dev/null +++ b/volo-http/src/dispatch.rs @@ -0,0 +1,74 @@ +use std::{future::Future, marker::PhantomData}; + +use http::Response; +use hyper::body::Incoming; + +use crate::{request::FromRequest, response::RespBody, DynError, HttpContext}; + +pub(crate) struct DispatchService { + inner: S, + _marker: PhantomData<(IB, OB)>, +} + +impl DispatchService { + pub(crate) fn new(service: S) -> Self { + Self { + inner: service, + _marker: PhantomData, + } + } +} + +impl Clone for DispatchService +where + S: Clone, +{ + fn clone(&self) -> Self { + Self { + inner: self.inner.clone(), + _marker: PhantomData, + } + } +} + +unsafe impl Send for DispatchService where S: Send {} + +unsafe impl Sync for DispatchService where S: Sync {} + +impl motore::Service for DispatchService +where + S: motore::Service> + Send + Sync + 'static, + S::Error: std::error::Error + Send + Sync + 'static, + OB: Into, + IB: FromRequest + Send, + for<'cx> ::FromFut<'cx>: std::marker::Send, +{ + type Response = Response; + + type Error = DynError; + + 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 { + match IB::from(&*cx, req).await { + Ok(body) => self + .inner + .call(cx, body) + .await + .map(|resp| { + let (parts, body) = resp.into_parts(); + Response::from_parts(parts, body.into()) + }) + .map_err(|e| Box::new(e) as DynError), + Err(response) => Ok(response), + } + } + } +} diff --git a/volo-http/src/layer.rs b/volo-http/src/layer.rs new file mode 100644 index 00000000..7c4c4e96 --- /dev/null +++ b/volo-http/src/layer.rs @@ -0,0 +1,105 @@ +use std::future::Future; + +use http::{Method, Request, Response, StatusCode}; +use http_body_util::Full; +use hyper::body::{Bytes, Incoming}; + +use crate::HttpContext; + +pub trait Layer { + type Service: motore::Service>; + + fn layer(self, inner: S) -> Self::Service; +} + +pub trait LayerExt { + fn method( + self, + method: Method, + ) -> FilterLayer) -> Result<(), StatusCode>>> + where + Self: Sized, + { + self.filter(Box::new( + move |cx: &mut HttpContext, _: &Request| { + if cx.method == method { + Ok(()) + } else { + Err(StatusCode::METHOD_NOT_ALLOWED) + } + }, + )) + } + + fn filter(self, f: F) -> FilterLayer + where + Self: Sized, + F: Fn(&mut HttpContext, &Request) -> Result<(), StatusCode>, + { + FilterLayer { f } + } +} + +pub struct FilterLayer { + f: F, +} + +impl Layer for FilterLayer +where + S: motore::Service, Response = Response>> + + Send + + Sync + + 'static, + F: Fn(&mut HttpContext, &Request) -> Result<(), StatusCode> + Send + Sync, +{ + type Service = Filter; + + fn layer(self, inner: S) -> Self::Service { + Filter { + service: inner, + f: self.f, + } + } +} + +pub struct Filter { + service: S, + f: F, +} + +impl motore::Service> for Filter +where + S: motore::Service, Response = Response>> + + Send + + Sync + + 'static, + F: Fn(&mut HttpContext, &Request) -> Result<(), StatusCode> + Send + Sync, +{ + type Response = S::Response; + + type Error = S::Error; + + type Future<'cx> = impl Future> + Send + 'cx + where + HttpContext: 'cx, + Self: 'cx; + + fn call<'cx, 's>( + &'s self, + cx: &'cx mut HttpContext, + req: Request, + ) -> Self::Future<'cx> + where + 's: 'cx, + { + async move { + if let Err(status) = (self.f)(cx, &req) { + return Ok(Response::builder() + .status(status) + .body(Full::new(Bytes::new())) + .unwrap()); + } + self.service.call(cx, req).await + } + } +} diff --git a/volo-http/src/lib.rs b/volo-http/src/lib.rs index 8b137891..c076c34c 100644 --- a/volo-http/src/lib.rs +++ b/volo-http/src/lib.rs @@ -1 +1,76 @@ +#![feature(impl_trait_in_assoc_type)] +pub(crate) mod dispatch; +pub mod layer; +pub mod param; +pub mod request; +pub mod response; +pub mod route; + +use std::{future::Future, net::SocketAddr}; + +use http::{Extensions, HeaderMap, HeaderValue, Method, Uri, Version}; +use hyper::{ + body::{Body, Incoming}, + Request, Response, +}; +use param::Params; + +pub type DynError = Box; + +pub struct HttpContextInner { + pub(crate) peer: SocketAddr, + + pub(crate) method: Method, + pub(crate) uri: Uri, + pub(crate) version: Version, + pub(crate) headers: HeaderMap, + pub(crate) extensions: Extensions, +} + +pub struct HttpContext { + pub peer: SocketAddr, + pub method: Method, + pub uri: Uri, + pub version: Version, + pub headers: HeaderMap, + pub extensions: Extensions, + + pub params: Params, +} + +#[derive(Clone)] +pub struct MotoreService { + peer: SocketAddr, + inner: S, +} + +impl hyper::service::Service> for MotoreService +where + OB: Body, + S: motore::Service<(), (HttpContextInner, Incoming), Response = Response> + Clone, + S::Error: Into, +{ + type Response = S::Response; + + type Error = S::Error; + + type Future = impl Future>; + + fn call(&self, req: Request) -> Self::Future { + let s = self.inner.clone(); + let peer = self.peer; + async move { + let (parts, req) = req.into_parts(); + let cx = HttpContextInner { + peer, + method: parts.method, + uri: parts.uri, + version: parts.version, + headers: parts.headers, + extensions: parts.extensions, + }; + s.call(&mut (), (cx, req)).await + } + } +} diff --git a/volo-http/src/param.rs b/volo-http/src/param.rs new file mode 100644 index 00000000..34d27614 --- /dev/null +++ b/volo-http/src/param.rs @@ -0,0 +1,51 @@ +use std::slice::Iter; + +use bytes::{BufMut, Bytes, BytesMut}; + +pub struct Params { + inner: Vec<(Bytes, Bytes)>, +} + +impl From> for Params { + fn from(params: matchit::Params) -> Self { + let mut inner = Vec::with_capacity(params.len()); + let mut capacity = 0; + for (k, v) in params.iter() { + capacity += k.len(); + capacity += v.len(); + } + + let mut buf = BytesMut::with_capacity(capacity); + + for (k, v) in params.iter() { + buf.put(k.as_bytes()); + let k = buf.split().freeze(); + buf.put(v.as_bytes()); + let v = buf.split().freeze(); + inner.push((k, v)); + } + + Self { inner } + } +} + +impl Params { + pub fn len(&self) -> usize { + self.inner.len() + } + + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + pub fn iter(&self) -> Iter<'_, (Bytes, Bytes)> { + self.inner.iter() + } + + pub fn get>(&self, k: K) -> Option<&Bytes> { + self.iter() + .filter(|(ik, _)| ik.as_ref() == k.as_ref()) + .map(|(_, v)| v) + .next() + } +} diff --git a/volo-http/src/request.rs b/volo-http/src/request.rs new file mode 100644 index 00000000..7891728b --- /dev/null +++ b/volo-http/src/request.rs @@ -0,0 +1,93 @@ +use bytes::Bytes; +use futures_util::Future; +use http::{header, HeaderMap, Response, StatusCode}; +use http_body_util::BodyExt; +use hyper::body::Incoming; +use serde::de::DeserializeOwned; + +use crate::{response::RespBody, HttpContext}; + +pub trait FromRequest: Sized { + type FromFut<'cx>: Future>> + Send + 'cx + where + Self: 'cx; + + fn from(cx: &HttpContext, body: Incoming) -> Self::FromFut<'_>; +} + +impl FromRequest for Incoming { + type FromFut<'cx> = impl Future>> + Send + 'cx + where + Self: 'cx; + + fn from(_cx: &HttpContext, body: Incoming) -> Self::FromFut<'_> { + async { Ok(body) } + } +} + +pub struct Json(pub T); + +impl FromRequest for Json { + type FromFut<'cx> = impl Future>> + Send + 'cx + where + Self: 'cx; + + fn from(cx: &HttpContext, body: Incoming) -> Self::FromFut<'_> { + async move { + if !json_content_type(&cx.headers) { + return Err(Response::builder() + .status(StatusCode::UNSUPPORTED_MEDIA_TYPE) + .body(Bytes::new().into()) + .unwrap()); + } + + match body.collect().await { + Ok(body) => { + let body = body.to_bytes(); + match serde_json::from_slice::(body.as_ref()) { + Ok(t) => Ok(Self(t)), + Err(e) => { + tracing::warn!("json serialization error {e}"); + Err(Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Bytes::new().into()) + .unwrap()) + } + } + } + Err(e) => { + tracing::warn!("collect body error: {e}"); + Err(Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Bytes::new().into()) + .unwrap()) + } + } + } + } +} + +fn json_content_type(headers: &HeaderMap) -> bool { + let content_type = if let Some(content_type) = headers.get(header::CONTENT_TYPE) { + content_type + } else { + return false; + }; + + let content_type = if let Ok(content_type) = content_type.to_str() { + content_type + } else { + return false; + }; + + let mime = if let Ok(mime) = content_type.parse::() { + mime + } else { + return false; + }; + + let is_json_content_type = mime.type_() == "application" + && (mime.subtype() == "json" || mime.suffix().map_or(false, |name| name == "json")); + + is_json_content_type +} diff --git a/volo-http/src/response.rs b/volo-http/src/response.rs new file mode 100644 index 00000000..76505198 --- /dev/null +++ b/volo-http/src/response.rs @@ -0,0 +1,79 @@ +use std::{ + pin::Pin, + task::{Context, Poll}, +}; + +use futures_util::{ready, stream}; +use http_body_util::{Full, StreamBody}; +use hyper::body::{Body, Bytes, Frame}; +use pin_project_lite::pin_project; + +use crate::DynError; + +pin_project! { + #[project = RespBodyProj] + pub enum RespBody { + Stream { + #[pin] inner: StreamBody, DynError>> + Send + Sync>>>, + }, + Full { + #[pin] inner: Full, + }, + } +} + +impl Body for RespBody { + type Data = Bytes; + + type Error = DynError; + + fn poll_frame( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll, Self::Error>>> { + match self.project() { + RespBodyProj::Stream { inner } => inner.poll_frame(cx), + RespBodyProj::Full { inner } => { + Poll::Ready(ready!(inner.poll_frame(cx)).map(|result| Ok(result.unwrap()))) + } + } + } +} + +impl From> for RespBody { + fn from(value: Full) -> Self { + Self::Full { inner: value } + } +} + +impl From for RespBody { + fn from(value: Bytes) -> Self { + Self::Full { + inner: Full::new(value), + } + } +} + +impl From for RespBody { + fn from(value: String) -> Self { + Self::Full { + inner: Full::new(value.into()), + } + } +} + +impl From<&'static str> for RespBody { + fn from(value: &'static str) -> Self { + Self::Full { + inner: Full::new(value.into()), + } + } +} + +impl From<()> for RespBody { + fn from(_: ()) -> Self { + Self::Full { + inner: Full::new(Bytes::new()), + } + } +} diff --git a/volo-http/src/route.rs b/volo-http/src/route.rs new file mode 100644 index 00000000..c039a2b9 --- /dev/null +++ b/volo-http/src/route.rs @@ -0,0 +1,287 @@ +use std::{future::Future, net::SocketAddr}; + +use http::{Method, Response, StatusCode}; +use http_body_util::Full; +use hyper::{ + body::{Bytes, Incoming}, + server::conn::http1, +}; +use hyper_util::rt::TokioIo; +use tokio::net::TcpListener; + +use crate::{ + dispatch::DispatchService, request::FromRequest, response::RespBody, DynError, HttpContext, + HttpContextInner, MotoreService, +}; + +pub type DynService = motore::BoxCloneService, DynError>; + +#[derive(Clone)] +pub struct Router { + inner: matchit::Router, +} + +impl Router { + pub fn build() -> RouterBuilder { + Default::default() + } +} + +impl motore::Service<(), (HttpContextInner, Incoming)> for Router { + type Response = Response; + + type Error = DynError; + + type Future<'cx> = impl Future> + Send + 'cx + where + HttpContextInner: 'cx, + Self: 'cx; + + fn call<'cx, 's>( + &'s self, + _cx: &'cx mut (), + cxreq: (HttpContextInner, Incoming), + ) -> Self::Future<'cx> + where + 's: 'cx, + { + async move { + let (cx, req) = cxreq; + if let Ok(matched) = self.inner.at(cx.uri.path()) { + let mut context = HttpContext { + peer: cx.peer, + method: cx.method, + uri: cx.uri.clone(), + version: cx.version, + headers: cx.headers, + extensions: cx.extensions, + params: matched.params.into(), + }; + matched.value.call(&mut context, req).await + } else { + Ok(Response::builder() + .status(StatusCode::NOT_FOUND) + .body(Full::new(Bytes::new()).into()) + .unwrap()) + } + } + } +} + +#[derive(Default)] +pub struct RouterBuilder { + routes: matchit::Router, +} + +impl RouterBuilder { + pub fn new() -> Self { + Default::default() + } + + pub fn route(mut self, uri: R, route: S) -> Self + where + R: Into, + S: motore::Service, Error = DynError> + + Send + + Sync + + Clone + + 'static, + { + if let Err(e) = self.routes.insert(uri, motore::BoxCloneService::new(route)) { + panic!("routing error: {e}"); + } + self + } + + pub async fn serve(self, addr: SocketAddr) -> Result<(), DynError> { + let listener = TcpListener::bind(addr).await?; + let router = Router { inner: self.routes }; + + loop { + let s = router.clone(); + let (stream, peer) = listener.accept().await?; + + let io = TokioIo::new(stream); + + tokio::task::spawn(async move { + if let Err(err) = http1::Builder::new() + .serve_connection(io, MotoreService { peer, inner: s }) + .await + { + tracing::warn!("error serving connection: {:?}", err); + } + }); + } + } +} + +#[derive(Default, Clone)] +pub struct Route { + options: Option, + get: Option, + post: Option, + put: Option, + delete: Option, + head: Option, + trace: Option, + connect: Option, + patch: Option, +} + +impl Route { + pub fn new() -> Self { + Default::default() + } + + pub fn builder() -> RouteBuilder { + RouteBuilder { route: Self::new() } + } +} + +impl motore::Service for Route { + type Response = Response; + + type Error = DynError; + + 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 { + match cx.method { + Method::GET => { + if let Some(service) = &self.get { + service.call(cx, req).await + } else { + Ok(Response::builder() + .status(StatusCode::NOT_FOUND) + .body("".into()) + .unwrap()) + } + } + Method::POST => { + if let Some(service) = &self.post { + service.call(cx, req).await + } else { + Ok(Response::builder() + .status(StatusCode::NOT_FOUND) + .body("".into()) + .unwrap()) + } + } + Method::PUT => { + if let Some(service) = &self.put { + service.call(cx, req).await + } else { + Ok(Response::builder() + .status(StatusCode::NOT_FOUND) + .body("".into()) + .unwrap()) + } + } + Method::DELETE => { + if let Some(service) = &self.delete { + service.call(cx, req).await + } else { + Ok(Response::builder() + .status(StatusCode::NOT_FOUND) + .body("".into()) + .unwrap()) + } + } + Method::HEAD => { + if let Some(service) = &self.head { + service.call(cx, req).await + } else { + Ok(Response::builder() + .status(StatusCode::NOT_FOUND) + .body("".into()) + .unwrap()) + } + } + Method::OPTIONS => { + if let Some(service) = &self.options { + service.call(cx, req).await + } else { + Ok(Response::builder() + .status(StatusCode::NOT_FOUND) + .body("".into()) + .unwrap()) + } + } + Method::CONNECT => { + if let Some(service) = &self.connect { + service.call(cx, req).await + } else { + Ok(Response::builder() + .status(StatusCode::NOT_FOUND) + .body("".into()) + .unwrap()) + } + } + Method::PATCH => { + if let Some(service) = &self.patch { + service.call(cx, req).await + } else { + Ok(Response::builder() + .status(StatusCode::NOT_FOUND) + .body("".into()) + .unwrap()) + } + } + Method::TRACE => { + if let Some(service) = &self.trace { + service.call(cx, req).await + } else { + Ok(Response::builder() + .status(StatusCode::NOT_FOUND) + .body("".into()) + .unwrap()) + } + } + _ => Ok(Response::builder() + .status(StatusCode::METHOD_NOT_ALLOWED) + .body("".into()) + .unwrap()), + } + } + } +} + +macro_rules! impl_method_register { + ($( $method:ident ),*) => { + $( + pub fn $method(mut self, handler: S) -> Self + where + S: motore::Service> + + Send + + Sync + + Clone + + 'static, + S::Error: std::error::Error + Send + Sync, + OB: Into + 'static, + IB: FromRequest + Send + 'static, + { + self.route.$method = Some(motore::BoxCloneService::new(DispatchService::new(handler))); + self + } + )+ + }; +} + +pub struct RouteBuilder { + route: Route, +} + +impl RouteBuilder { + impl_method_register!(options, get, post, put, delete, head, trace, connect, patch); + + pub fn build(self) -> Route { + self.route + } +}