From 0fd17d4181e9e03414ed5b37ff0b4a7f4a9263de Mon Sep 17 00:00:00 2001 From: David Pedersen Date: Thu, 19 Aug 2021 21:16:44 +0200 Subject: [PATCH] Bring back `Handler::into_service` (#212) It was removed as part of https://github.com/tokio-rs/axum/pull/184 but I do actually think it has some utility. So makes sense to keep even if axum doesn't use it directly for routing. --- src/handler/future.rs | 15 +++++++- src/handler/into_service.rs | 73 +++++++++++++++++++++++++++++++++++++ src/handler/mod.rs | 31 ++++++++++++++++ src/tests/mod.rs | 21 +++++++++++ 4 files changed, 139 insertions(+), 1 deletion(-) create mode 100644 src/handler/into_service.rs diff --git a/src/handler/future.rs b/src/handler/future.rs index d921494c..999b96a9 100644 --- a/src/handler/future.rs +++ b/src/handler/future.rs @@ -2,11 +2,15 @@ use crate::body::{box_body, BoxBody}; use crate::util::{Either, EitherProj}; -use futures_util::{future::BoxFuture, ready}; +use futures_util::{ + future::{BoxFuture, Map}, + ready, +}; use http::{Method, Request, Response}; use http_body::Empty; use pin_project_lite::pin_project; use std::{ + convert::Infallible, fmt, future::Future, pin::Pin, @@ -59,3 +63,12 @@ where f.debug_struct("OnMethodFuture").finish() } } + +opaque_future! { + /// The response future for [`IntoService`](super::IntoService). + pub type IntoServiceFuture = + Map< + BoxFuture<'static, Response>, + fn(Response) -> Result, Infallible>, + >; +} diff --git a/src/handler/into_service.rs b/src/handler/into_service.rs new file mode 100644 index 00000000..ffc0fde0 --- /dev/null +++ b/src/handler/into_service.rs @@ -0,0 +1,73 @@ +use super::Handler; +use crate::body::BoxBody; +use http::{Request, Response}; +use std::{ + convert::Infallible, + fmt, + marker::PhantomData, + task::{Context, Poll}, +}; +use tower::Service; + +/// An adapter that makes a [`Handler`] into a [`Service`]. +/// +/// Created with [`Handler::into_service`]. +pub struct IntoService { + handler: H, + _marker: PhantomData (B, T)>, +} + +impl IntoService { + pub(super) fn new(handler: H) -> Self { + Self { + handler, + _marker: PhantomData, + } + } +} + +impl fmt::Debug for IntoService { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_tuple("IntoService") + .field(&format_args!("...")) + .finish() + } +} + +impl Clone for IntoService +where + H: Clone, +{ + fn clone(&self) -> Self { + Self { + handler: self.handler.clone(), + _marker: PhantomData, + } + } +} + +impl Service> for IntoService +where + H: Handler + Clone + Send + 'static, + B: Send + 'static, +{ + type Response = Response; + type Error = Infallible; + type Future = super::future::IntoServiceFuture; + + fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { + // `IntoService` can only be constructed from async functions which are always ready, or from + // `Layered` which bufferes in `::call` and is therefore also always + // ready. + Poll::Ready(Ok(())) + } + + fn call(&mut self, req: Request) -> Self::Future { + use futures_util::future::FutureExt; + + let handler = self.handler.clone(); + let future = Handler::call(handler, req).map(Ok::<_, Infallible> as _); + + super::future::IntoServiceFuture { future } + } +} diff --git a/src/handler/mod.rs b/src/handler/mod.rs index d091c6b5..ee54cbe8 100644 --- a/src/handler/mod.rs +++ b/src/handler/mod.rs @@ -21,6 +21,9 @@ use std::{ use tower::{BoxError, Layer, Service, ServiceExt}; pub mod future; +mod into_service; + +pub use self::into_service::IntoService; /// Route requests to the given handler regardless of the HTTP method of the /// request. @@ -255,6 +258,34 @@ pub trait Handler: Clone + Send + Sized + 'static { { Layered::new(layer.layer(any(self))) } + + /// Convert the handler into a [`Service`]. + /// + /// This allows you to serve a single handler if you don't need any routing: + /// + /// ```rust + /// use axum::{ + /// Server, handler::Handler, http::{Uri, Method}, response::IntoResponse, + /// }; + /// use tower::make::Shared; + /// use std::net::SocketAddr; + /// + /// async fn handler(method: Method, uri: Uri, body: String) -> impl IntoResponse { + /// format!("received `{} {}` with body `{:?}`", method, uri, body) + /// } + /// + /// let service = handler.into_service(); + /// + /// # async { + /// Server::bind(&SocketAddr::from(([127, 0, 0, 1], 3000))) + /// .serve(Shared::new(service)) + /// .await?; + /// # Ok::<_, hyper::Error>(()) + /// # }; + /// ``` + fn into_service(self) -> IntoService { + IntoService::new(self) + } } #[async_trait] diff --git a/src/tests/mod.rs b/src/tests/mod.rs index ee65d957..b273401f 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -3,6 +3,7 @@ use crate::{ extract, handler::{any, delete, get, on, patch, post, Handler}, + response::IntoResponse, route, routing::nest, routing::{MethodFilter, RoutingDsl}, @@ -604,6 +605,26 @@ async fn multiple_methods_for_one_handler() { assert_eq!(res.status(), StatusCode::OK); } +#[tokio::test] +async fn handler_into_service() { + async fn handle(body: String) -> impl IntoResponse { + format!("you said: {}", body) + } + + let addr = run_in_background(handle.into_service()).await; + + let client = reqwest::Client::new(); + + let res = client + .post(format!("http://{}", addr)) + .body("hi there!") + .send() + .await + .unwrap(); + assert_eq!(res.status(), StatusCode::OK); + assert_eq!(res.text().await.unwrap(), "you said: hi there!"); +} + /// Run a `tower::Service` in the background and get a URI for it. pub(crate) async fn run_in_background(svc: S) -> SocketAddr where