square_rust/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::Response;
10use reqwest_middleware::{ClientBuilder, ClientWithMiddleware};
11use reqwest_retry::policies::ExponentialBackoff;
12use reqwest_retry::RetryTransientMiddleware;
13use retry_policies::Jitter;
14
15use crate::api::models::objects::api_error::SquareApiError;
16use crate::http::client::config::{RetryConfig, SquareHttpClientConfig};
17
18/// HTTP Client to send HTTP Requests and read the responses.
19#[derive(Clone, Debug)]
20pub struct SquareHttpClient {
21    /// The wrapped lib client
22    pub client: reqwest::Client,
23    pub retry_client: ClientWithMiddleware,
24}
25
26/// HTTP Client to send HTTP Requests and read the responses.
27impl SquareHttpClient {
28    /// Instantiates a new `SquareHttpClient` given the provided `SquareHttpClientConfig`.
29    pub fn try_new(config: &SquareHttpClientConfig) -> Result<Self, SquareApiError> {
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            SquareApiError::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 { client, retry_client })
44    }
45
46    /// Sends an HTTP GET
47    pub async fn get(&self, url: &str) -> Result<Response, SquareApiError> {
48        let response = self.retry_client.get(url).send().await.map_err(|e| {
49            let msg = format!("Error getting {}: {}", url, e);
50            error!("{}", msg);
51            SquareApiError::new(&msg)
52        })?;
53        Ok(response)
54    }
55
56    /// Sends an HTTP POST
57    pub async fn post(&self, url: &str, body: &str) -> Result<Response, SquareApiError> {
58        let body_string = body.to_string();
59        let response = self
60            .retry_client
61            .post(url)
62            .body(body_string)
63            .send()
64            .await
65            .map_err(|e| {
66                let msg = format!("Error posting to {}: {}", url, e);
67                error!("{}", msg);
68                SquareApiError::new(&msg)
69            })?;
70        Ok(response)
71    }
72
73    /// Sends an HTTP POST with multipart form data
74    pub async fn post_multipart(&self, url: &str, body: &str, filepath: &str) -> Result<Response, SquareApiError> {
75        let request = serde_json::to_string(body).map_err(|e| {
76            let msg = format!("Error serializing request body - url: {}, body: {:?}: {}", url, body, e);
77            error!("{}", msg);
78            SquareApiError::new(&msg)
79        })?;
80
81        let mut file = File::open(filepath).map_err(|e| {
82            let msg = format!("Error opening file {}: {}", filepath, e);
83            error!("{}", msg);
84            SquareApiError::new(&msg)
85        })?;
86        let mut vec = Vec::new();
87        let _reader = file.read_to_end(&mut vec);
88        let mime = get_mime_type(filepath)?;
89        let part = Part::stream(vec).mime_str(mime).map_err(|e| {
90            let msg = format!(
91                "Error applying content type {} to form part for file {}: {}",
92                mime, filepath, e
93            );
94            error!("{}", msg);
95            SquareApiError::new(&msg)
96        })?;
97
98        let form = multipart::Form::new().text("request", request).part("file", part);
99
100        let response = self.client.post(url).multipart(form).send().await.map_err(|e| {
101            let msg = format!("Error posting to {}: {}", url, e);
102            error!("{}", msg);
103            SquareApiError::new(&msg)
104        })?;
105        Ok(response)
106    }
107
108    /// Sends an HTTP POST without any body
109    pub async fn empty_post(&self, url: &str) -> Result<Response, SquareApiError> {
110        let response = self.client.post(url).send().await.map_err(|e| {
111            let msg = format!("Error posting to {}: {}", url, e);
112            error!("{}", msg);
113            SquareApiError::new(&msg)
114        })?;
115        Ok(response)
116    }
117
118    /// Sends an HTTP PUT
119    pub async fn put(&self, url: &str, body: &str) -> Result<Response, SquareApiError> {
120        let body_string = body.to_string();
121        let response = self.retry_client.put(url).body(body_string).send().await.map_err(|e| {
122            let msg = format!("Error putting to {}: {}", url, e);
123            error!("{}", msg);
124            SquareApiError::new(&msg)
125        })?;
126        Ok(response)
127    }
128
129    /// Sends an HTTP PUT with multipart form data
130    pub async fn put_multipart(&self, url: &str, body: &str, filepath: &str) -> Result<Response, SquareApiError> {
131        let request = serde_json::to_string(body).map_err(|e| {
132            let msg = format!("Error serializing request body - url: {}, body: {:?}: {}", url, body, e);
133            error!("{}", msg);
134            SquareApiError::new(&msg)
135        })?;
136
137        let mut file = File::open(filepath).map_err(|e| {
138            let msg = format!("Error opening file {}: {}", filepath, e);
139            error!("{}", msg);
140            SquareApiError::new(&msg)
141        })?;
142        let mut vec = Vec::new();
143        let _reader = file.read_to_end(&mut vec);
144        let mime = get_mime_type(filepath)?;
145        let part = Part::stream(vec).mime_str(mime).map_err(|e| {
146            let msg = format!(
147                "Error applying content type {} to form part for file {}: {}",
148                mime, filepath, e
149            );
150            error!("{}", msg);
151            SquareApiError::new(&msg)
152        })?;
153
154        let form = multipart::Form::new().text("request", request).part("file", part);
155
156        let response = self.client.put(url).multipart(form).send().await.map_err(|e| {
157            let msg = format!("Error putting to {}: {}", url, e);
158            error!("{}", msg);
159            SquareApiError::new(&msg)
160        })?;
161        Ok(response)
162    }
163
164    /// Sends an HTTP DELETE
165    pub async fn delete(&self, url: &str) -> Result<Response, SquareApiError> {
166        let response = self.retry_client.delete(url).send().await.map_err(|e| {
167            let msg = format!("Error putting to {}: {}", url, e);
168            error!("{}", msg);
169            SquareApiError::new(&msg)
170        })?;
171        Ok(response)
172    }
173}
174
175/// Creates a retry policy based on the provided `RetryConfig`
176fn create_retry_policy(retry_configuration: &RetryConfig) -> ExponentialBackoff {
177    ExponentialBackoff::builder()
178        .retry_bounds(
179            retry_configuration.min_retry_interval,
180            retry_configuration.max_retry_interval,
181        )
182        .jitter(Jitter::Bounded)
183        .build_with_max_retries(retry_configuration.retries_count)
184}
185
186/// Tries to determine the file's MIME type and returns it as a str
187fn get_mime_type(filepath: &str) -> Result<&str, SquareApiError> {
188    let kind = infer::get_from_path(filepath).map_err(|e| {
189        let msg = format!("Error reading file {}: {}", filepath, e);
190        error!("{}", msg);
191        SquareApiError::new(&msg)
192    })?;
193
194    match kind {
195        Some(kind) => Ok(kind.mime_type()),
196        None => {
197            let msg = format!("Error determining mime type for file {}", filepath);
198            error!("{}", msg);
199            Err(SquareApiError::new(&msg))
200        }
201    }
202}
203
204#[cfg(test)]
205mod tests {
206    use crate::http::client::config::SquareHttpClientConfig;
207    use crate::http::client::http_client::SquareHttpClient;
208
209    #[test]
210    fn try_new_ok() {
211        let client = SquareHttpClient::try_new(&SquareHttpClientConfig::default());
212        assert!(client.is_ok());
213    }
214}