square_api_client/http/client/
http_client.rs1use 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#[derive(Clone, Debug)]
21pub struct HttpClient {
22 pub client: reqwest::Client,
24 pub retry_client: ClientWithMiddleware,
25}
26
27impl HttpClient {
28 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 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 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 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 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 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 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 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
195fn 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}