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