square_api_client/http/client/
http_client.rs

1//! HTTP Client to send HTTP Requests and read the responses.
2
3use std::fs::File;
4use std::io::Read;
5use std::{fmt::Debug, time::Duration};
6
7use log::error;
8use reqwest::multipart::{self, Part};
9use reqwest_middleware::{ClientBuilder, ClientWithMiddleware};
10use reqwest_retry::policies::ExponentialBackoff;
11use reqwest_retry::RetryTransientMiddleware;
12use serde::Serialize;
13
14use crate::http::client::http_client_configuration::RetryConfiguration;
15use crate::{http::HttpResponse, models::errors::ApiError};
16
17use super::HttpClientConfiguration;
18
19/// HTTP Client to send HTTP Requests and read the responses.
20#[derive(Clone, Debug)]
21pub struct HttpClient {
22    /// The wrapped lib client
23    pub client: reqwest::Client,
24    pub retry_client: ClientWithMiddleware,
25}
26
27impl HttpClient {
28    /// Instantiates a new `HttpClient` given the provided `HttpClientConfiguration`.
29    pub fn try_new(config: &HttpClientConfiguration) -> Result<Self, ApiError> {
30        let mut client_builder = reqwest::ClientBuilder::new();
31        client_builder = client_builder.timeout(Duration::from_secs(config.timeout.into()));
32        client_builder = client_builder.user_agent(&config.user_agent);
33        client_builder = client_builder.default_headers((&config.default_headers).try_into()?);
34        let client = client_builder.build().map_err(|e| {
35            let msg = format!("Failed to build client: {}", e);
36            error!("{}", msg);
37            ApiError::new(&msg)
38        })?;
39        let retry_policy = create_retry_policy(&config.retry_configuration);
40        let retry_client = ClientBuilder::new(client.clone())
41            .with(RetryTransientMiddleware::new_with_policy(retry_policy))
42            .build();
43        Ok(Self {
44            client,
45            retry_client,
46        })
47    }
48
49    /// Sends an HTTP GET
50    pub async fn get(&self, url: &str) -> Result<HttpResponse, ApiError> {
51        let response = self.retry_client.get(url).send().await.map_err(|e| {
52            let msg = format!("Error getting {}: {}", url, e);
53            error!("{}", msg);
54            ApiError::new(&msg)
55        })?;
56        Ok(HttpResponse::new(response))
57    }
58
59    /// Sends an HTTP POST
60    pub async fn post<T: Serialize + ?Sized>(
61        &self,
62        url: &str,
63        body: &T,
64    ) -> Result<HttpResponse, ApiError> {
65        let response = self.retry_client.post(url).json(body).send().await.map_err(|e| {
66            let msg = format!("Error posting to {}: {}", url, e);
67            error!("{}", msg);
68            ApiError::new(&msg)
69        })?;
70        Ok(HttpResponse::new(response))
71    }
72
73    /// Sends an HTTP POST with multipart form data
74    pub async fn post_multipart<T: Debug + Serialize>(
75        &self,
76        url: &str,
77        body: &T,
78        filepath: &str,
79    ) -> Result<HttpResponse, ApiError> {
80        let request = serde_json::to_string(body).map_err(|e| {
81            let msg =
82                format!("Error serializing request body - url: {}, body: {:?}: {}", url, body, e);
83            error!("{}", msg);
84            ApiError::new(&msg)
85        })?;
86
87        let mut file = File::open(filepath).map_err(|e| {
88            let msg = format!("Error opening file {}: {}", filepath, e);
89            error!("{}", msg);
90            ApiError::new(&msg)
91        })?;
92        let mut vec = Vec::new();
93        let _reader = file.read_to_end(&mut vec);
94        let mime = get_mime_type(filepath)?;
95        let part = Part::stream(vec).mime_str(mime).map_err(|e| {
96            let msg = format!(
97                "Error applying content type {} to form part for file {}: {}",
98                mime, filepath, e
99            );
100            error!("{}", msg);
101            ApiError::new(&msg)
102        })?;
103
104        let form = multipart::Form::new().text("request", request).part("file", part);
105
106        let response = self.client.post(url).multipart(form).send().await.map_err(|e| {
107            let msg = format!("Error posting to {}: {}", url, e);
108            error!("{}", msg);
109            ApiError::new(&msg)
110        })?;
111        Ok(HttpResponse::new(response))
112    }
113
114    /// Sends an HTTP POST without any body
115    pub async fn empty_post(&self, url: &str) -> Result<HttpResponse, ApiError> {
116        let response = self.client.post(url).send().await.map_err(|e| {
117            let msg = format!("Error posting to {}: {}", url, e);
118            error!("{}", msg);
119            ApiError::new(&msg)
120        })?;
121        Ok(HttpResponse::new(response))
122    }
123
124    /// Sends an HTTP PUT
125    pub async fn put<T: Serialize>(&self, url: &str, body: &T) -> Result<HttpResponse, ApiError> {
126        let response = self.retry_client.put(url).json(body).send().await.map_err(|e| {
127            let msg = format!("Error putting to {}: {}", url, e);
128            error!("{}", msg);
129            ApiError::new(&msg)
130        })?;
131        Ok(HttpResponse::new(response))
132    }
133
134    /// Sends an HTTP PUT with multipart form data
135    pub async fn put_multipart<T: Debug + Serialize>(
136        &self,
137        url: &str,
138        body: &T,
139        filepath: &str,
140    ) -> Result<HttpResponse, ApiError> {
141        let request = serde_json::to_string(body).map_err(|e| {
142            let msg =
143                format!("Error serializing request body - url: {}, body: {:?}: {}", url, body, e);
144            error!("{}", msg);
145            ApiError::new(&msg)
146        })?;
147
148        let mut file = File::open(filepath).map_err(|e| {
149            let msg = format!("Error opening file {}: {}", filepath, e);
150            error!("{}", msg);
151            ApiError::new(&msg)
152        })?;
153        let mut vec = Vec::new();
154        let _reader = file.read_to_end(&mut vec);
155        let mime = get_mime_type(filepath)?;
156        let part = Part::stream(vec).mime_str(mime).map_err(|e| {
157            let msg = format!(
158                "Error applying content type {} to form part for file {}: {}",
159                mime, filepath, e
160            );
161            error!("{}", msg);
162            ApiError::new(&msg)
163        })?;
164
165        let form = multipart::Form::new().text("request", request).part("file", part);
166
167        let response = self.client.put(url).multipart(form).send().await.map_err(|e| {
168            let msg = format!("Error putting to {}: {}", url, e);
169            error!("{}", msg);
170            ApiError::new(&msg)
171        })?;
172        Ok(HttpResponse::new(response))
173    }
174
175    /// Sends an HTTP DELETE
176    pub async fn delete(&self, url: &str) -> Result<HttpResponse, ApiError> {
177        let response = self.retry_client.delete(url).send().await.map_err(|e| {
178            let msg = format!("Error putting to {}: {}", url, e);
179            error!("{}", msg);
180            ApiError::new(&msg)
181        })?;
182        Ok(HttpResponse::new(response))
183    }
184}
185
186fn create_retry_policy(retry_configuration: &RetryConfiguration) -> ExponentialBackoff {
187    let mut retry_policy =
188        ExponentialBackoff::builder().build_with_max_retries(retry_configuration.retries_count);
189    retry_policy.max_retry_interval = retry_configuration.max_retry_interval;
190    retry_policy.min_retry_interval = retry_configuration.min_retry_interval;
191    retry_policy.backoff_exponent = retry_configuration.backoff_exponent;
192    retry_policy
193}
194
195/// Tries to determine the file's MIME type and returns it as a str
196fn get_mime_type(filepath: &str) -> Result<&str, ApiError> {
197    let kind = infer::get_from_path(filepath).map_err(|e| {
198        let msg = format!("Error reading file {}: {}", filepath, e);
199        error!("{}", msg);
200        ApiError::new(&msg)
201    })?;
202
203    match kind {
204        Some(kind) => Ok(kind.mime_type()),
205        None => {
206            let msg = format!("Error determining mime type for file {}", filepath);
207            error!("{}", msg);
208            Err(ApiError::new(&msg))
209        }
210    }
211}
212
213#[cfg(test)]
214mod tests {
215    use crate::http::client::{HttpClient, HttpClientConfiguration};
216
217    #[test]
218    fn try_new_ok() {
219        let client = HttpClient::try_new(&HttpClientConfiguration::default());
220        assert!(client.is_ok());
221    }
222}