extensions middleware and request extensions (#56)

* extensions middleware and request extensions

* fix publish

* request initialisers

* simplify example

* docs

* changelog

* Apply suggestions from code review

Co-authored-by: Alex Butler <alexheretic@gmail.com>

Co-authored-by: Alex Butler <alexheretic@gmail.com>
This commit is contained in:
Conrad Ludgate 2022-09-30 10:14:31 +01:00 committed by GitHub
parent 3c00388cfb
commit 85e520f78b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 172 additions and 21 deletions

View file

@ -8,6 +8,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
- Implementation of `Debug` trait for `RequestBuilder`. - Implementation of `Debug` trait for `RequestBuilder`.
- A new `RequestInitialiser` trait that can be added to `ClientWithMiddleware`
- A new `Extension` initialiser that adds extensions to the request
- Adds `with_extension` method functionality to `RequestBuilder` that can add extensions for the `send` method to use - deprecating `send_with_extensions`.
## [0.1.6] - 2022-04-21
Absolutely nothing changed
## [0.1.5] - 2022-02-21 ## [0.1.5] - 2022-02-21

View file

@ -10,6 +10,7 @@ use task_local_extensions::Extensions;
use crate::error::Result; use crate::error::Result;
use crate::middleware::{Middleware, Next}; use crate::middleware::{Middleware, Next};
use crate::RequestInitialiser;
/// A `ClientBuilder` is used to build a [`ClientWithMiddleware`]. /// A `ClientBuilder` is used to build a [`ClientWithMiddleware`].
/// ///
@ -17,6 +18,7 @@ use crate::middleware::{Middleware, Next};
pub struct ClientBuilder { pub struct ClientBuilder {
client: Client, client: Client,
middleware_stack: Vec<Arc<dyn Middleware>>, middleware_stack: Vec<Arc<dyn Middleware>>,
initialiser_stack: Vec<Arc<dyn RequestInitialiser>>,
} }
impl ClientBuilder { impl ClientBuilder {
@ -24,6 +26,7 @@ impl ClientBuilder {
ClientBuilder { ClientBuilder {
client, client,
middleware_stack: Vec::new(), middleware_stack: Vec::new(),
initialiser_stack: Vec::new(),
} }
} }
@ -47,9 +50,33 @@ impl ClientBuilder {
self self
} }
/// Convenience method to attach a request initialiser.
///
/// If you need to keep a reference to the initialiser after attaching, use [`with_arc_init`].
///
/// [`with_arc_init`]: Self::with_arc_init
pub fn with_init<I>(self, initialiser: I) -> Self
where
I: RequestInitialiser,
{
self.with_arc_init(Arc::new(initialiser))
}
/// Add a request initialiser to the chain. [`with_init`] is more ergonomic if you don't need the `Arc`.
///
/// [`with_init`]: Self::with_init
pub fn with_arc_init(mut self, initialiser: Arc<dyn RequestInitialiser>) -> Self {
self.initialiser_stack.push(initialiser);
self
}
/// Returns a `ClientWithMiddleware` using this builder configuration. /// Returns a `ClientWithMiddleware` using this builder configuration.
pub fn build(self) -> ClientWithMiddleware { pub fn build(self) -> ClientWithMiddleware {
ClientWithMiddleware::new(self.client, self.middleware_stack) ClientWithMiddleware {
inner: self.client,
middleware_stack: self.middleware_stack.into_boxed_slice(),
initialiser_stack: self.initialiser_stack.into_boxed_slice(),
}
} }
} }
@ -59,6 +86,7 @@ impl ClientBuilder {
pub struct ClientWithMiddleware { pub struct ClientWithMiddleware {
inner: reqwest::Client, inner: reqwest::Client,
middleware_stack: Box<[Arc<dyn Middleware>]>, middleware_stack: Box<[Arc<dyn Middleware>]>,
initialiser_stack: Box<[Arc<dyn RequestInitialiser>]>,
} }
impl ClientWithMiddleware { impl ClientWithMiddleware {
@ -70,6 +98,8 @@ impl ClientWithMiddleware {
ClientWithMiddleware { ClientWithMiddleware {
inner: client, inner: client,
middleware_stack: middleware_stack.into(), middleware_stack: middleware_stack.into(),
// TODO(conradludgate) - allow downstream code to control this manually if desired
initialiser_stack: Box::new([]),
} }
} }
@ -105,10 +135,14 @@ impl ClientWithMiddleware {
/// See [`Client::request`] /// See [`Client::request`]
pub fn request<U: IntoUrl>(&self, method: Method, url: U) -> RequestBuilder { pub fn request<U: IntoUrl>(&self, method: Method, url: U) -> RequestBuilder {
RequestBuilder { let req = RequestBuilder {
inner: self.inner.request(method, url), inner: self.inner.request(method, url),
client: self.clone(), client: self.clone(),
} extensions: Extensions::new(),
};
self.initialiser_stack
.iter()
.fold(req, |req, i| i.init(req))
} }
/// See [`Client::execute`] /// See [`Client::execute`]
@ -134,6 +168,7 @@ impl From<Client> for ClientWithMiddleware {
ClientWithMiddleware { ClientWithMiddleware {
inner: client, inner: client,
middleware_stack: Box::new([]), middleware_stack: Box::new([]),
initialiser_stack: Box::new([]),
} }
} }
} }
@ -152,6 +187,7 @@ impl fmt::Debug for ClientWithMiddleware {
pub struct RequestBuilder { pub struct RequestBuilder {
inner: reqwest::RequestBuilder, inner: reqwest::RequestBuilder,
client: ClientWithMiddleware, client: ClientWithMiddleware,
extensions: Extensions,
} }
impl RequestBuilder { impl RequestBuilder {
@ -164,14 +200,14 @@ impl RequestBuilder {
{ {
RequestBuilder { RequestBuilder {
inner: self.inner.header(key, value), inner: self.inner.header(key, value),
client: self.client, ..self
} }
} }
pub fn headers(self, headers: HeaderMap) -> Self { pub fn headers(self, headers: HeaderMap) -> Self {
RequestBuilder { RequestBuilder {
inner: self.inner.headers(headers), inner: self.inner.headers(headers),
client: self.client, ..self
} }
} }
@ -182,7 +218,7 @@ impl RequestBuilder {
{ {
RequestBuilder { RequestBuilder {
inner: self.inner.basic_auth(username, password), inner: self.inner.basic_auth(username, password),
client: self.client, ..self
} }
} }
@ -192,49 +228,49 @@ impl RequestBuilder {
{ {
RequestBuilder { RequestBuilder {
inner: self.inner.bearer_auth(token), inner: self.inner.bearer_auth(token),
client: self.client, ..self
} }
} }
pub fn body<T: Into<Body>>(self, body: T) -> Self { pub fn body<T: Into<Body>>(self, body: T) -> Self {
RequestBuilder { RequestBuilder {
inner: self.inner.body(body), inner: self.inner.body(body),
client: self.client, ..self
} }
} }
pub fn timeout(self, timeout: Duration) -> Self { pub fn timeout(self, timeout: Duration) -> Self {
RequestBuilder { RequestBuilder {
inner: self.inner.timeout(timeout), inner: self.inner.timeout(timeout),
client: self.client, ..self
} }
} }
pub fn multipart(self, multipart: Form) -> Self { pub fn multipart(self, multipart: Form) -> Self {
RequestBuilder { RequestBuilder {
inner: self.inner.multipart(multipart), inner: self.inner.multipart(multipart),
client: self.client, ..self
} }
} }
pub fn query<T: Serialize + ?Sized>(self, query: &T) -> Self { pub fn query<T: Serialize + ?Sized>(self, query: &T) -> Self {
RequestBuilder { RequestBuilder {
inner: self.inner.query(query), inner: self.inner.query(query),
client: self.client, ..self
} }
} }
pub fn form<T: Serialize + ?Sized>(self, form: &T) -> Self { pub fn form<T: Serialize + ?Sized>(self, form: &T) -> Self {
RequestBuilder { RequestBuilder {
inner: self.inner.form(form), inner: self.inner.form(form),
client: self.client, ..self
} }
} }
pub fn json<T: Serialize + ?Sized>(self, json: &T) -> Self { pub fn json<T: Serialize + ?Sized>(self, json: &T) -> Self {
RequestBuilder { RequestBuilder {
inner: self.inner.json(json), inner: self.inner.json(json),
client: self.client, ..self
} }
} }
@ -242,22 +278,44 @@ impl RequestBuilder {
self.inner.build() self.inner.build()
} }
/// Inserts the extension into this request builder
pub fn with_extension<T: Send + Sync + 'static>(mut self, extension: T) -> Self {
self.extensions.insert(extension);
self
}
/// Returns a mutable reference to the internal set of extensions for this request
pub fn extensions(&mut self) -> &mut Extensions {
&mut self.extensions
}
pub async fn send(self) -> Result<Response> { pub async fn send(self) -> Result<Response> {
let req = self.inner.build()?; let Self {
self.client.execute(req).await inner,
client,
mut extensions,
} = self;
let req = inner.build()?;
client.execute_with_extensions(req, &mut extensions).await
} }
/// Sends a request with initial [`Extensions`]. /// Sends a request with initial [`Extensions`].
#[deprecated = "use the with_extension method and send directly"]
pub async fn send_with_extensions(self, ext: &mut Extensions) -> Result<Response> { pub async fn send_with_extensions(self, ext: &mut Extensions) -> Result<Response> {
let req = self.inner.build()?; let Self { inner, client, .. } = self;
self.client.execute_with_extensions(req, ext).await let req = inner.build()?;
client.execute_with_extensions(req, ext).await
} }
// TODO(conradludgate): fix this method to take `&self`. It's currently useless as it is.
// I'm tempted to make this breaking change without a major bump, but I'll wait for now
#[deprecated = "This method was badly replicated from the base RequestBuilder. If you somehow made use of this method, it will break next major version"]
pub fn try_clone(self) -> Option<Self> { pub fn try_clone(self) -> Option<Self> {
let client = self.client; self.inner.try_clone().map(|inner| RequestBuilder {
self.inner inner,
.try_clone() client: self.client,
.map(|inner| RequestBuilder { inner, client }) extensions: self.extensions,
})
} }
} }

View file

@ -52,7 +52,9 @@ pub struct ReadmeDoctests;
mod client; mod client;
mod error; mod error;
mod middleware; mod middleware;
mod req_init;
pub use client::{ClientBuilder, ClientWithMiddleware, RequestBuilder}; pub use client::{ClientBuilder, ClientWithMiddleware, RequestBuilder};
pub use error::{Error, Result}; pub use error::{Error, Result};
pub use middleware::{Middleware, Next}; pub use middleware::{Middleware, Next};
pub use req_init::{Extension, RequestInitialiser};

View file

@ -0,0 +1,84 @@
use crate::RequestBuilder;
/// When attached to a [`ClientWithMiddleware`] (generally using [`with_init`]), it is run
/// whenever the client starts building a request, in the order it was attached.
///
/// # Example
///
/// ```
/// use reqwest_middleware::{RequestInitialiser, RequestBuilder};
///
/// struct AuthInit;
///
/// impl RequestInitialiser for AuthInit {
/// fn init(&self, req: RequestBuilder) -> RequestBuilder {
/// req.bearer_auth("my_auth_token")
/// }
/// }
/// ```
///
/// [`ClientWithMiddleware`]: crate::ClientWithMiddleware
/// [`with_init`]: crate::ClientBuilder::with_init
pub trait RequestInitialiser: 'static + Send + Sync {
fn init(&self, req: RequestBuilder) -> RequestBuilder;
}
impl<F> RequestInitialiser for F
where
F: Send + Sync + 'static + Fn(RequestBuilder) -> RequestBuilder,
{
fn init(&self, req: RequestBuilder) -> RequestBuilder {
(self)(req)
}
}
/// A middleware that inserts the value into the [`Extensions`](task_local_extensions::Extensions) during the call.
///
/// This is a good way to inject extensions to middleware deeper in the stack
///
/// ```
/// use reqwest::{Client, Request, Response};
/// use reqwest_middleware::{ClientBuilder, Middleware, Next, Result, Extension};
/// use task_local_extensions::Extensions;
///
/// #[derive(Clone)]
/// struct LogName(&'static str);
/// struct LoggingMiddleware;
///
/// #[async_trait::async_trait]
/// impl Middleware for LoggingMiddleware {
/// async fn handle(
/// &self,
/// req: Request,
/// extensions: &mut Extensions,
/// next: Next<'_>,
/// ) -> Result<Response> {
/// // get the log name or default to "unknown"
/// let name = extensions
/// .get()
/// .map(|&LogName(name)| name)
/// .unwrap_or("unknown");
/// println!("[{name}] Request started {req:?}");
/// let res = next.run(req, extensions).await;
/// println!("[{name}] Result: {res:?}");
/// res
/// }
/// }
///
/// async fn run() {
/// let reqwest_client = Client::builder().build().unwrap();
/// let client = ClientBuilder::new(reqwest_client)
/// .with_init(Extension(LogName("my-client")))
/// .with(LoggingMiddleware)
/// .build();
/// let resp = client.get("https://truelayer.com").send().await.unwrap();
/// println!("TrueLayer page HTML: {}", resp.text().await.unwrap());
/// }
/// ```
pub struct Extension<T>(pub T);
impl<T: Send + Sync + Clone + 'static> RequestInitialiser for Extension<T> {
fn init(&self, req: RequestBuilder) -> RequestBuilder {
req.with_extension(self.0.clone())
}
}