timvw_rust_runtime/
client.rs1use 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#[cfg(not(target_family = "wasm"))]
18pub type HttpService = BoxCloneSyncService<http::Request<Body>, http::Response<Body>, BoxError>;
19#[cfg(target_family = "wasm")]
24pub type HttpService =
25 crate::LocalBoxCloneService<http::Request<Body>, http::Response<Body>, BoxError>;
26
27#[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 #[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 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 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 pub fn service(&self) -> &HttpService {
119 &self.service
120 }
121
122 pub fn request(&self, method: Method, url: &str) -> RequestBuilder {
124 RequestBuilder::new(self.clone(), method, url)
125 }
126
127 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)]
147impl 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#[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#[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}