Skip to main content

timvw_rust_runtime/
client.rs

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