Skip to content

Commit

Permalink
feat(client): allow to add a closure as a middleware in client
Browse files Browse the repository at this point in the history
  • Loading branch information
joelwurtz committed Jan 17, 2025
1 parent 99f7c97 commit 25eab4c
Show file tree
Hide file tree
Showing 6 changed files with 184 additions and 68 deletions.
40 changes: 36 additions & 4 deletions client/src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,10 @@ use crate::{
connect::Connect,
date::DateTimeService,
error::Error,
pool,
middleware, pool,
resolver::{base_resolver, ResolverService},
response::Response,
service::{base_service, HttpService},
service::{Service, ServiceRequest},
service::{async_fn::AsyncFn, http::base_service, HttpService, Service, ServiceRequest},
timeout::TimeoutConfig,
tls::{
connector::{self, Connector},
Expand Down Expand Up @@ -69,7 +68,7 @@ impl ClientBuilder {
/// // trait implement for the logic of middleware. most of the types are boilerplate
/// // that can be copy/pasted. the real logic goes into `async fn call`
/// impl<'r, 'c> Service<ServiceRequest<'r, 'c>> for MyMiddleware {
/// type Response = Response<'c>;
/// type Response = Response;
/// type Error = Error;
///
/// async fn call(&self, req: ServiceRequest<'r, 'c>) -> Result<Self::Response, Self::Error> {
Expand Down Expand Up @@ -117,6 +116,39 @@ impl ClientBuilder {
self
}

/// add a middleware function to client builder.
///
/// func is an async closure, that receive the next middleware in the chain and the incoming request.
///
/// # Examples
/// ```rust
/// use xitca_client::{
/// error::Error,
/// ClientBuilder, HttpService, Response, Service, ServiceRequest
/// };
/// use xitca_http::http::HeaderValue;
///
/// // start a new client builder and apply our middleware to it:
/// let builder = ClientBuilder::new()
/// // use a closure to receive HttpService and construct my middleware type.
/// .middleware_fn(async |mut req: ServiceRequest, http_service: &HttpService| {
/// req.req.headers_mut().insert("x-my-header", HeaderValue::from_static("my-value"));
///
/// http_service.call(req).await
/// });
/// ```
pub fn middleware_fn<F>(mut self, func: F) -> Self
where
F: for<'s, 'r, 'c> AsyncFn<(ServiceRequest<'r, 'c>, &'s HttpService), Output = Result<Response, Error>>
+ Send
+ Sync
+ 'static,
for<'s, 'r, 'c> <F as AsyncFn<(ServiceRequest<'r, 'c>, &'s HttpService)>>::Future: Send,
{
self.service = Box::new(middleware::AsyncFn::new(self.service, func));
self
}

#[cfg(feature = "openssl")]
/// enable openssl as tls connector.
pub fn openssl(mut self) -> Self {
Expand Down
33 changes: 33 additions & 0 deletions client/src/middleware/async_fn.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
use crate::{
error::Error,
response::Response,
service::{async_fn, Service, ServiceRequest},
};

/// middleware to wrap async function as a service.
pub struct AsyncFn<S, F> {
service: S,
func: F,
}

impl<S, F> AsyncFn<S, F> {
pub fn new(service: S, func: F) -> Self {
Self { service, func }
}
}

impl<'r, 'c, S, F> Service<ServiceRequest<'r, 'c>> for AsyncFn<S, F>
where
S: for<'r2, 'c2> Service<ServiceRequest<'r, 'c>, Response = Response, Error = Error> + Send + Sync,
F: for<'r3, 'c3, 's3> async_fn::AsyncFn<(ServiceRequest<'r, 'c>, &'s3 S), Output = Result<Response, Error>>
+ Send
+ Sync,
for<'r4, 'c4, 's4> <F as async_fn::AsyncFn<(ServiceRequest<'r4, 'c4>, &'s4 S)>>::Future: Send,
{
type Response = Response;
type Error = Error;

async fn call(&self, req: ServiceRequest<'r, 'c>) -> Result<Self::Response, Self::Error> {
self.func.call((req, &self.service)).await
}
}
2 changes: 2 additions & 0 deletions client/src/middleware/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@
mod redirect;

mod async_fn;
#[cfg(feature = "compress")]
mod decompress;

#[cfg(feature = "compress")]
pub use decompress::Decompress;

pub(crate) use async_fn::AsyncFn;
pub use redirect::FollowRedirect;
44 changes: 44 additions & 0 deletions client/src/service/async_fn.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#![allow(non_snake_case)]

use core::future::Future;

/// Same as `std::ops::Fn` trait but for async output.
///
/// It is necessary in the the HRTB bounds for async fn's with reference parameters because it
/// allows the output future to be bound to the parameter lifetime.
/// `F: for<'a> AsyncFn<(&'a u8,) Output=u8>`
pub trait AsyncFn<Arg> {
type Output;
type Future: Future<Output = Self::Output>;

fn call(&self, arg: Arg) -> Self::Future;
}

macro_rules! async_closure_impl {
($($arg: ident),*) => {
impl<Func, Fut, $($arg,)*> AsyncFn<($($arg,)*)> for Func
where
Func: Fn($($arg),*) -> Fut,
Fut: Future,
{
type Output = Fut::Output;
type Future = Fut;

#[inline]
fn call(&self, ($($arg,)*): ($($arg,)*)) -> Self::Future {
self($($arg,)*)
}
}
}
}

async_closure_impl! {}
async_closure_impl! { A }
async_closure_impl! { A, B }
async_closure_impl! { A, B, C }
async_closure_impl! { A, B, C, D }
async_closure_impl! { A, B, C, D, E }
async_closure_impl! { A, B, C, D, E, F }
async_closure_impl! { A, B, C, D, E, F, G }
async_closure_impl! { A, B, C, D, E, F, G, H }
async_closure_impl! { A, B, C, D, E, F, G, H, I }
67 changes: 3 additions & 64 deletions client/src/service.rs → client/src/service/http.rs
Original file line number Diff line number Diff line change
@@ -1,75 +1,14 @@
use core::{future::Future, pin::Pin, time::Duration};

use crate::{
body::BoxBody,
client::Client,
connect::Connect,
error::Error,
http::{Request, Version},
http::Version,
pool::{exclusive, shared},
response::Response,
service::ServiceDyn,
uri::Uri,
Service, ServiceRequest,
};

type BoxFuture<'f, T, E> = Pin<Box<dyn Future<Output = Result<T, E>> + Send + 'f>>;

/// trait for composable http services. Used for middleware,resolver and tls connector.
pub trait Service<Req> {
type Response;
type Error;

fn call(&self, req: Req) -> impl Future<Output = Result<Self::Response, Self::Error>> + Send;
}

pub trait ServiceDyn<Req> {
type Response;
type Error;

fn call<'s>(&'s self, req: Req) -> BoxFuture<'s, Self::Response, Self::Error>
where
Req: 's;
}

impl<S, Req> ServiceDyn<Req> for S
where
S: Service<Req>,
{
type Response = S::Response;
type Error = S::Error;

#[inline]
fn call<'s>(&'s self, req: Req) -> BoxFuture<'s, Self::Response, Self::Error>
where
Req: 's,
{
Box::pin(Service::call(self, req))
}
}

impl<I, Req> Service<Req> for Box<I>
where
Req: Send,
I: ServiceDyn<Req> + ?Sized + Send + Sync,
{
type Response = I::Response;
type Error = I::Error;

#[inline]
async fn call(&self, req: Req) -> Result<Self::Response, Self::Error> {
ServiceDyn::call(&**self, req).await
}
}

/// request type for middlewares.
/// It's similar to [RequestBuilder] type but with additional side effect enabled.
///
/// [RequestBuilder]: crate::request::RequestBuilder
pub struct ServiceRequest<'r, 'c> {
pub req: &'r mut Request<BoxBody>,
pub client: &'c Client,
pub timeout: Duration,
}

/// type alias for object safe wrapper of type implement [Service] trait.
pub type HttpService =
Box<dyn for<'r, 'c> ServiceDyn<ServiceRequest<'r, 'c>, Response = Response, Error = Error> + Send + Sync>;
Expand Down
66 changes: 66 additions & 0 deletions client/src/service/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
pub(crate) mod async_fn;
pub(crate) mod http;

use core::{future::Future, pin::Pin, time::Duration};

use crate::{body::BoxBody, client::Client, http::Request};
pub use http::HttpService;

type BoxFuture<'f, T, E> = Pin<Box<dyn Future<Output = Result<T, E>> + Send + 'f>>;

/// trait for composable http services. Used for middleware,resolver and tls connector.
pub trait Service<Req> {
type Response;
type Error;

fn call(&self, req: Req) -> impl Future<Output = Result<Self::Response, Self::Error>> + Send;
}

pub trait ServiceDyn<Req> {
type Response;
type Error;

fn call<'s>(&'s self, req: Req) -> BoxFuture<'s, Self::Response, Self::Error>
where
Req: 's;
}

impl<S, Req> ServiceDyn<Req> for S
where
S: Service<Req>,
{
type Response = S::Response;
type Error = S::Error;

#[inline]
fn call<'s>(&'s self, req: Req) -> BoxFuture<'s, Self::Response, Self::Error>
where
Req: 's,
{
Box::pin(Service::call(self, req))
}
}

impl<I, Req> Service<Req> for Box<I>
where
Req: Send,
I: ServiceDyn<Req> + ?Sized + Send + Sync,
{
type Response = I::Response;
type Error = I::Error;

#[inline]
async fn call(&self, req: Req) -> Result<Self::Response, Self::Error> {
ServiceDyn::call(&**self, req).await
}
}

/// request type for middlewares.
/// It's similar to [RequestBuilder] type but with additional side effect enabled.
///
/// [RequestBuilder]: crate::request::RequestBuilder
pub struct ServiceRequest<'r, 'c> {
pub req: &'r mut Request<BoxBody>,
pub client: &'c Client,
pub timeout: Duration,
}

0 comments on commit 25eab4c

Please sign in to comment.