Skip to main content

timvw_rust_runtime/
client.rs

1use std::fmt;
2#[cfg(not(target_family = "wasm"))]
3use std::sync::{Mutex, PoisonError};
4use std::task::{Context, Poll};
5
6#[cfg(not(target_family = "wasm"))]
7use tower::util::BoxCloneSyncService;
8use tower::{Layer, Service, ServiceExt};
9
10use crate::maybe::BoxFuture;
11use crate::{Body, BoxError, Error, MaybeSend, MaybeSync, Method, RequestBuilder, Response};
12
13/// The service a [`Client`] sends requests through, with its type erased: tower's `BoxCloneSyncService`.
14///
15/// On WebAssembly (`target_family = "wasm"`) it is a [`LocalBoxCloneService`](crate::LocalBoxCloneService), which is
16/// neither `Send` nor `Sync`, like the futures of reqwest's `fetch` backend.
17#[cfg(not(target_family = "wasm"))]
18pub type HttpService = BoxCloneSyncService<http::Request<Body>, http::Response<Body>, BoxError>;
19/// The service a [`Client`] sends requests through, with its type erased.
20///
21/// On WebAssembly (`target_family = "wasm"`) it is a [`LocalBoxCloneService`](crate::LocalBoxCloneService), which is
22/// neither `Send` nor `Sync`, like the futures of reqwest's `fetch` backend; natively tower's `BoxCloneSyncService`.
23#[cfg(target_family = "wasm")]
24pub type HttpService =
25    crate::LocalBoxCloneService<http::Request<Body>, http::Response<Body>, BoxError>;
26
27/// Sends the requests of a generated client through a tower service.
28///
29/// [`Client::default`] sends with reqwest (feature `reqwest`) and retries transient failures (feature `retry`).
30/// [`Client::from_service`] takes any other service, as is, and [`Client::layer`] wraps the current one in a tower
31/// layer:
32///
33/// ```no_run
34/// # #[cfg(feature = "reqwest")]
35/// # fn demo() {
36/// use std::time::Duration;
37/// use timvw_rust_runtime::Client;
38///
39/// let client = Client::default().layer(tower::timeout::TimeoutLayer::new(Duration::from_secs(30)));
40/// # }
41/// ```
42#[derive(Clone)]
43pub struct Client {
44    service: HttpService,
45}
46
47impl Client {
48    /// A client that sends with a new `reqwest::Client`, retrying with [`RetryPolicy::default`] (feature `retry`).
49    ///
50    /// [`RetryPolicy::default`]: crate::retry::RetryPolicy
51    #[cfg(feature = "reqwest")]
52    pub fn new() -> Self {
53        Self::from_reqwest(reqwest::Client::new())
54    }
55
56    /// A client that sends with the given `reqwest::Client`, for example one with a proxy or a custom TLS setup, and
57    /// retries like [`Client::new`].
58    ///
59    /// For other retries, or none, start from the bare service instead:
60    /// `Client::from_service(ReqwestService::new(client))`, then add the layers you want.
61    #[cfg(feature = "reqwest")]
62    pub fn from_reqwest(client: reqwest::Client) -> Self {
63        let client = Self::from_service(crate::ReqwestService::new(client));
64        #[cfg(feature = "retry")]
65        let client = client.layer(crate::retry::RetryLayer::default());
66        client
67    }
68
69    /// A client that sends with any tower service from `http::Request<Body>` to `http::Response<B>`.
70    ///
71    /// The service must be `Clone` and `Send`, not `Sync`: services such as `ConcurrencyLimit` that cannot be shared
72    /// between threads are held behind a lock that is only taken to clone them. On WebAssembly
73    /// (`target_family = "wasm"`) it need not be `Send` either ([`MaybeSend`]), nor its future and body.
74    pub fn from_service<S, B>(service: S) -> Self
75    where
76        S: Service<http::Request<Body>, Response = http::Response<B>> + Clone + MaybeSend + 'static,
77        S::Error: Into<BoxError>,
78        S::Future: MaybeSend + 'static,
79        B: http_body::Body + MaybeSend + MaybeSync + 'static,
80        B::Data: Into<bytes::Bytes>,
81        B::Error: Into<BoxError>,
82    {
83        let service = service
84            .map_response(|response: http::Response<B>| response.map(Body::wrap))
85            .map_err(Into::into);
86        #[cfg(not(target_family = "wasm"))]
87        let service = BoxCloneSyncService::new(SyncService(Mutex::new(service)));
88        #[cfg(target_family = "wasm")]
89        let service = crate::LocalBoxCloneService::new(service);
90        Self { service }
91    }
92
93    /// Wraps the client's service in a tower layer: retries, timeouts, rate limits, tracing, ...
94    ///
95    /// Layers apply from the inside out: the last one added sees a request first.
96    pub fn layer<L, B>(self, layer: L) -> Self
97    where
98        L: Layer<HttpService>,
99        L::Service: Service<http::Request<Body>, Response = http::Response<B>>
100            + Clone
101            + MaybeSend
102            + 'static,
103        <L::Service as Service<http::Request<Body>>>::Error: Into<BoxError>,
104        <L::Service as Service<http::Request<Body>>>::Future: MaybeSend + 'static,
105        B: http_body::Body + MaybeSend + MaybeSync + 'static,
106        B::Data: Into<bytes::Bytes>,
107        B::Error: Into<BoxError>,
108    {
109        Self::from_service(layer.layer(self.service))
110    }
111
112    /// The service requests go through.
113    pub fn service(&self) -> &HttpService {
114        &self.service
115    }
116
117    /// Starts a request to `url`.
118    pub fn request(&self, method: Method, url: &str) -> RequestBuilder {
119        RequestBuilder::new(self.clone(), method, url)
120    }
121
122    /// Sends a request.
123    pub async fn execute(&self, request: http::Request<Body>) -> Result<Response, Error> {
124        let response = self
125            .service
126            .clone()
127            .oneshot(request)
128            .await
129            .map_err(Error::request)?;
130        Ok(Response::from(response))
131    }
132}
133
134/// With feature `reqwest` [`Client::new`]: reqwest, with retries when feature `retry` is on; without it a client whose every request fails, to be
135/// replaced with [`Client::from_service`].
136impl Default for Client {
137    fn default() -> Self {
138        #[cfg(feature = "reqwest")]
139        {
140            Self::new()
141        }
142        #[cfg(not(feature = "reqwest"))]
143        {
144            Self::from_service(NoService)
145        }
146    }
147}
148
149impl fmt::Debug for Client {
150    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
151        f.debug_struct("Client").finish_non_exhaustive()
152    }
153}
154
155/// Makes a `Clone + Send` service `Sync` (not needed on WebAssembly, where nothing is `Sync`). Every request clones the service, under the lock, and calls the clone, so
156/// the lock is never held across `poll_ready` or `call` on a shared value.
157#[cfg(not(target_family = "wasm"))]
158struct SyncService<S>(Mutex<S>);
159
160#[cfg(not(target_family = "wasm"))]
161impl<S: Clone> Clone for SyncService<S> {
162    fn clone(&self) -> Self {
163        let service = self
164            .0
165            .lock()
166            .unwrap_or_else(PoisonError::into_inner)
167            .clone();
168        Self(Mutex::new(service))
169    }
170}
171
172#[cfg(not(target_family = "wasm"))]
173impl<S: Service<R>, R> Service<R> for SyncService<S> {
174    type Response = S::Response;
175    type Error = S::Error;
176    type Future = S::Future;
177
178    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
179        self.0
180            .get_mut()
181            .unwrap_or_else(PoisonError::into_inner)
182            .poll_ready(cx)
183    }
184
185    fn call(&mut self, request: R) -> Self::Future {
186        self.0
187            .get_mut()
188            .unwrap_or_else(PoisonError::into_inner)
189            .call(request)
190    }
191}
192
193/// The service of a client built without an HTTP client: every request fails.
194#[cfg_attr(feature = "reqwest", allow(dead_code))]
195#[derive(Clone, Copy, Debug)]
196struct NoService;
197
198impl Service<http::Request<Body>> for NoService {
199    type Response = http::Response<Body>;
200    type Error = BoxError;
201    type Future = BoxFuture<'static, Result<Self::Response, Self::Error>>;
202
203    fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
204        Poll::Ready(Ok(()))
205    }
206
207    fn call(&mut self, _: http::Request<Body>) -> Self::Future {
208        Box::pin(async {
209            Err("no HTTP client: enable the `reqwest` feature of timvw-rust-runtime or set one with Client::from_service".into())
210        })
211    }
212}