Skip to main content

roas_arazzo_executor/
client.rs

1//! Ready-made clients built on [`reqwest`], behind the `reqwest`
2//! feature.
3//!
4//! One type over both of reqwest's clients — blocking and async — the
5//! way `roas-http-fetcher` does it for the loader, so the choice is a
6//! type parameter rather than two APIs.
7
8use crate::http::{
9    AsyncHttpClient, ClientError, HttpClient, HttpRequest, HttpResponse, SendFuture, SleepFuture,
10};
11use std::time::Duration;
12
13/// An HTTP client for the executor.
14///
15/// `Client<reqwest::blocking::Client>` implements [`HttpClient`] and
16/// `Client<reqwest::Client>` implements [`AsyncHttpClient`].
17#[derive(Clone, Debug)]
18pub struct Client<C> {
19    client: C,
20}
21
22impl<C> Client<C> {
23    /// Use a client the caller has already configured — with its own
24    /// timeouts, proxy, TLS or middleware.
25    pub fn with(client: C) -> Self {
26        Self { client }
27    }
28}
29
30impl Client<reqwest::blocking::Client> {
31    /// A blocking client with reqwest's defaults.
32    ///
33    /// # Panics
34    ///
35    /// If reqwest cannot build its default client — the same condition
36    /// `reqwest::blocking::Client::new` panics on.
37    #[must_use]
38    pub fn blocking() -> Self {
39        Self::with(reqwest::blocking::Client::new())
40    }
41}
42
43impl Client<reqwest::Client> {
44    /// An async client with reqwest's defaults.
45    #[must_use]
46    pub fn asynchronous() -> Self {
47        Self::with(reqwest::Client::new())
48    }
49}
50
51impl HttpClient for Client<reqwest::blocking::Client> {
52    fn send(&mut self, request: &HttpRequest) -> Result<HttpResponse, ClientError> {
53        let method = method(request)?;
54        let mut builder = self.client.request(method, &request.url);
55        for (name, value) in &request.headers {
56            builder = builder.header(name, value);
57        }
58        if let Some(body) = &request.body {
59            builder = builder.body(body.clone());
60        }
61        if let Some(timeout) = request.timeout {
62            builder = builder.timeout(timeout);
63        }
64        let response = builder.send().map_err(ClientError::new)?;
65        let status = response.status().as_u16();
66        let headers = headers(response.headers());
67        let body = response.bytes().map_err(ClientError::new)?.to_vec();
68        Ok(HttpResponse {
69            status,
70            headers,
71            body,
72        })
73    }
74}
75
76impl AsyncHttpClient for Client<reqwest::Client> {
77    fn send<'a>(&'a mut self, request: &'a HttpRequest) -> SendFuture<'a> {
78        let client = self.client.clone();
79        Box::pin(async move {
80            let method = method(request)?;
81            let mut builder = client.request(method, &request.url);
82            for (name, value) in &request.headers {
83                builder = builder.header(name, value);
84            }
85            if let Some(body) = &request.body {
86                builder = builder.body(body.clone());
87            }
88            if let Some(timeout) = request.timeout {
89                builder = builder.timeout(timeout);
90            }
91            let response = builder.send().await.map_err(ClientError::new)?;
92            let status = response.status().as_u16();
93            let headers = headers(response.headers());
94            let body = response.bytes().await.map_err(ClientError::new)?.to_vec();
95            Ok(HttpResponse {
96                status,
97                headers,
98                body,
99            })
100        })
101    }
102
103    fn sleep(&self, duration: Duration) -> SleepFuture<'_> {
104        Box::pin(tokio::time::sleep(duration))
105    }
106}
107
108fn method(request: &HttpRequest) -> Result<reqwest::Method, ClientError> {
109    reqwest::Method::from_bytes(request.method.as_bytes()).map_err(ClientError::new)
110}
111
112fn headers(headers: &reqwest::header::HeaderMap) -> Vec<(String, String)> {
113    headers
114        .iter()
115        .map(|(name, value)| {
116            (
117                name.to_string(),
118                value.to_str().unwrap_or_default().to_owned(),
119            )
120        })
121        .collect()
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127
128    #[test]
129    fn a_method_is_read_from_what_the_step_wrote() {
130        let request = HttpRequest {
131            method: "PATCH".to_owned(),
132            ..HttpRequest::default()
133        };
134        assert_eq!(method(&request).unwrap(), reqwest::Method::PATCH);
135        let bad = HttpRequest {
136            method: "not a method".to_owned(),
137            ..HttpRequest::default()
138        };
139        assert!(method(&bad).is_err());
140    }
141
142    #[test]
143    fn headers_come_across_as_pairs() {
144        let mut map = reqwest::header::HeaderMap::new();
145        map.insert("x-thing", "value".parse().expect("a header value"));
146        assert_eq!(headers(&map), [("x-thing".to_owned(), "value".to_owned())]);
147    }
148
149    #[test]
150    fn both_clients_can_be_built() {
151        let _ = Client::blocking();
152        let _ = Client::asynchronous();
153        let _ = Client::with(reqwest::Client::new());
154    }
155}