Skip to content

Commit

Permalink
feat(volo-http): support service_fn (#317)
Browse files Browse the repository at this point in the history
* feat(volo-http): support `service_fn` as route

* chore(volo-http): remove extractor for `HeaderMap`

Extracting `HeaderMap` needs a `clone` and it may cost a long time.
To use the `HeaderMap` without copy or clone, the `service_fn` is
a better choice.

---------

Signed-off-by: Yu Li <[email protected]>
  • Loading branch information
yukiiiteru authored Jan 8, 2024
1 parent 660ec92 commit 4efb1e4
Show file tree
Hide file tree
Showing 7 changed files with 100 additions and 22 deletions.
26 changes: 20 additions & 6 deletions examples/src/http/simple.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::{net::SocketAddr, sync::Arc, time::Duration};
use std::{convert::Infallible, net::SocketAddr, sync::Arc, time::Duration};

use faststr::FastStr;
use serde::{Deserialize, Serialize};
Expand All @@ -10,7 +10,7 @@ use volo_http::{
layer::TimeoutLayer,
middleware::{self, Next},
response::IntoResponse,
route::{get, post, MethodRouter, Router},
route::{from_handler, get, post, service_fn, MethodRouter, Router},
Address, BodyIncoming, Bytes, ConnectionInfo, CookieJar, HttpContext, Json, MaybeInvalid,
Method, Params, Response, Server, StatusCode, Uri,
};
Expand Down Expand Up @@ -68,7 +68,7 @@ async fn post_with_form(Form(info): Form<Login>) -> Result<String, StatusCode> {
process_login(info)
}

async fn test(
async fn get_and_post(
u: Uri,
m: Method,
data: MaybeInvalid<FastStr>,
Expand Down Expand Up @@ -100,6 +100,10 @@ async fn extension(Extension(state): Extension<Arc<State>>) -> String {
format!("State {{ foo: {}, bar: {} }}\n", state.foo, state.bar)
}

async fn service_fn_test(cx: &mut HttpContext, req: BodyIncoming) -> Result<Response, Infallible> {
Ok(format!("cx: {cx:?}, req: {req:?}").into_response())
}

async fn timeout_handler(uri: Uri, peer: Address) -> StatusCode {
tracing::info!("Timeout on `{}`, peer: {}", uri, peer);
StatusCode::INTERNAL_SERVER_ERROR
Expand Down Expand Up @@ -127,14 +131,14 @@ fn user_form_router() -> Router {
MethodRouter::builder()
// curl "http://localhost:8080/user/login?username=admin&password=admin"
// curl "http://localhost:8080/user/login?username=admin&password=password"
.get(get_with_query)
.get(from_handler(get_with_query))
// curl http://localhost:8080/user/login \
// -X POST \
// -d 'username=admin&password=admin'
// curl http://localhost:8080/user/login \
// -X POST \
// -d 'username=admin&password=password'
.post(post_with_form)
.post(from_handler(post_with_form))
.build(),
)
}
Expand All @@ -145,7 +149,10 @@ fn test_router() -> Router {
// curl http://127.0.0.1:8080/test/extract -X POST -d "114514"
.route(
"/test/extract",
MethodRouter::builder().get(test).post(test).build(),
MethodRouter::builder()
.get(from_handler(get_and_post))
.post(from_handler(get_and_post))
.build(),
)
// curl http://127.0.0.1:8080/test/timeout
.route(
Expand All @@ -158,6 +165,13 @@ fn test_router() -> Router {
.route("/test/conn_show", get(conn_show))
// curl http://127.0.0.1:8080/test/extension
.route("/test/extension", get(extension))
// curl http://127.0.0.1:8080/test/service_fn
.route(
"/test/service_fn",
MethodRouter::builder()
.get(service_fn(service_fn_test))
.build(),
)
}

// You can use the following commands for testing cookies
Expand Down
1 change: 1 addition & 0 deletions volo-http/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use crate::param::Params;
static X_FORWARDED_HOST: HeaderName = HeaderName::from_static("x-forwarded-host");
static X_FORWARDED_PROTO: HeaderName = HeaderName::from_static("x-forwarded-proto");

#[derive(Debug)]
pub struct HttpContext {
pub peer: Address,
pub method: Method,
Expand Down
9 changes: 1 addition & 8 deletions volo-http/src/extract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use futures_util::Future;
use http_body_util::BodyExt;
use hyper::{
body::Incoming,
http::{header, HeaderMap, Method, StatusCode, Uri},
http::{header, Method, StatusCode, Uri},
};
use serde::de::DeserializeOwned;
use volo::net::Address;
Expand Down Expand Up @@ -144,13 +144,6 @@ impl<S: Sync> FromContext<S> for ConnectionInfo {
}
}

impl<S: Sync> FromContext<S> for HeaderMap {
type Rejection = Infallible;
async fn from_context(cx: &mut HttpContext, _state: &S) -> Result<Self, Self::Rejection> {
Ok(cx.headers.clone())
}
}

impl<T, S> FromRequest<S, private::ViaContext> for T
where
T: FromContext<S> + Sync,
Expand Down
1 change: 1 addition & 0 deletions volo-http/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ pub mod request;
pub mod response;
pub mod route;
pub mod server;
pub(crate) mod service_fn;

mod macros;

Expand Down
2 changes: 1 addition & 1 deletion volo-http/src/param.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use std::slice::Iter;

use bytes::{BufMut, Bytes, BytesMut};

#[derive(Clone)]
#[derive(Clone, Debug)]
pub struct Params {
pub(crate) inner: Vec<(Bytes, Bytes)>,
}
Expand Down
17 changes: 10 additions & 7 deletions volo-http/src/route.rs
Original file line number Diff line number Diff line change
Expand Up @@ -373,12 +373,8 @@ pub struct MethodRouterBuilder<S> {
macro_rules! impl_method_register_for_builder {
($( $method:ident ),*) => {
$(
pub fn $method<H, T>(mut self, handler: H) -> Self
where
for<'a> H: Handler<T, S> + Clone + Send + Sync + 'a,
for<'a> T: 'a,
{
self.router.$method = MethodEndpoint::Handler(DynHandler::new(handler));
pub fn $method(mut self, ep: MethodEndpoint<S>) -> Self {
self.router.$method = ep;
self
}
)+
Expand Down Expand Up @@ -420,7 +416,7 @@ macro_rules! impl_method_register {
for<'a> T: 'a,
S: Clone + Send + Sync + 'static,
{
MethodRouterBuilder::new().$method(h).build()
MethodRouterBuilder::new().$method(MethodEndpoint::from_handler(h)).build()
}
)+
};
Expand Down Expand Up @@ -591,6 +587,13 @@ where
MethodEndpoint::from_service(srv)
}

pub fn service_fn<F>(f: F) -> MethodEndpoint<()>
where
F: for<'r> crate::service_fn::Callback<'r> + Clone + Send + Sync + 'static,
{
MethodEndpoint::from_service(crate::service_fn::service_fn(f))
}

#[derive(Clone)]
struct RouteForStatusCode(StatusCode);

Expand Down
66 changes: 66 additions & 0 deletions volo-http/src/service_fn.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
use std::{convert::Infallible, fmt, future::Future};

use hyper::body::Incoming;
use motore::service::Service;

use crate::{HttpContext, Response};

/// Returns a new [`ServiceFn`] with the given closure.
///
/// This lets you build a [`Service`] from an async function that returns a [`Result`].
pub fn service_fn<F>(f: F) -> ServiceFn<F> {
ServiceFn { f }
}

/// A [`Service`] implemented by a closure. See the docs for [`service_fn`] for more details.
#[derive(Copy, Clone)]
pub struct ServiceFn<F> {
f: F,
}

impl<F> Service<HttpContext, Incoming> for ServiceFn<F>
where
F: for<'r> Callback<'r>,
{
type Response = Response;
type Error = Infallible;

fn call<'s, 'cx>(
&'s self,
cx: &'cx mut HttpContext,
req: Incoming,
) -> impl Future<Output = Result<Self::Response, Self::Error>> {
(self.f).call(cx, req)
}
}

impl<F> fmt::Debug for ServiceFn<F> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ServiceFn")
.field("f", &format_args!("{}", std::any::type_name::<F>()))
.finish()
}
}

/// [`Service`] for binding lifetime to return value while using closure.
/// This is just a temporary workaround for lifetime issues.
///
/// Related issue: https://github.com/rust-lang/rust/issues/70263.
/// Related RFC: https://github.com/rust-lang/rfcs/pull/3216.
pub trait Callback<'r> {
type Future: Future<Output = Result<Response, Infallible>> + Send + 'r;

fn call(&self, cx: &'r mut HttpContext, req: Incoming) -> Self::Future;
}

impl<'r, F, Fut> Callback<'r> for F
where
F: Fn(&'r mut HttpContext, Incoming) -> Fut,
Fut: Future<Output = Result<Response, Infallible>> + Send + 'r,
{
type Future = Fut;

fn call(&self, cx: &'r mut HttpContext, req: Incoming) -> Self::Future {
self(cx, req)
}
}

0 comments on commit 4efb1e4

Please sign in to comment.