1use std::time::Duration;
13
14use reqwest::{
15 Method,
16 header::{CONTENT_TYPE, HeaderMap, HeaderValue},
17};
18use serde::{Serialize, de::DeserializeOwned};
19
20use crate::Error;
21
22#[derive(Debug, Clone)]
23pub struct HttpClientBuilder {
24 timeout: Duration,
25 headers: HeaderMap<HeaderValue>,
26}
27
28#[derive(Debug, Clone)]
29pub struct HttpClient {
30 headers: HeaderMap<HeaderValue>,
31 client: reqwest::Client,
32}
33
34#[derive(Debug, Clone)]
35pub struct HttpRequest {
36 method: Method,
37 url: String,
38 headers: HeaderMap<HeaderValue>,
39 body: Option<String>,
40 client: reqwest::Client,
41}
42
43impl Default for HttpClientBuilder {
44 fn default() -> Self {
45 Self::new()
46 }
47}
48
49impl HttpClientBuilder {
50 pub fn new() -> Self {
51 let mut headers = HeaderMap::new();
52 headers.append(CONTENT_TYPE, HeaderValue::from_static("application/json"));
53
54 Self {
55 timeout: Duration::from_secs(30),
56 headers,
57 }
58 }
59
60 pub fn with_header(mut self, name: &'static str, value: impl AsRef<str>) -> Self {
61 if let Ok(value) = HeaderValue::from_str(value.as_ref()) {
62 self.headers.append(name, value);
63 }
64 self
65 }
66
67 pub fn set_header(mut self, name: &'static str, value: impl AsRef<str>) -> Self {
68 if let Ok(value) = HeaderValue::from_str(value.as_ref()) {
69 self.headers.insert(name, value);
70 }
71 self
72 }
73
74 pub fn with_timeout(mut self, timeout: Option<Duration>) -> Self {
75 if let Some(timeout) = timeout {
76 self.timeout = timeout;
77 }
78 self
79 }
80
81 pub fn build(self) -> HttpClient {
82 let client = reqwest::Client::builder()
83 .timeout(self.timeout)
84 .build()
85 .unwrap_or_default();
86 HttpClient {
87 headers: self.headers,
88 client,
89 }
90 }
91}
92
93impl HttpClient {
94 pub fn request(&self, method: Method, url: impl Into<String>) -> HttpRequest {
95 HttpRequest {
96 method,
97 url: url.into(),
98 headers: self.headers.clone(),
99 body: None,
100 client: self.client.clone(),
101 }
102 }
103
104 pub fn get(&self, url: impl Into<String>) -> HttpRequest {
105 self.request(Method::GET, url)
106 }
107
108 pub fn post(&self, url: impl Into<String>) -> HttpRequest {
109 self.request(Method::POST, url)
110 }
111
112 pub fn put(&self, url: impl Into<String>) -> HttpRequest {
113 self.request(Method::PUT, url)
114 }
115
116 pub fn delete(&self, url: impl Into<String>) -> HttpRequest {
117 self.request(Method::DELETE, url)
118 }
119
120 pub fn patch(&self, url: impl Into<String>) -> HttpRequest {
121 self.request(Method::PATCH, url)
122 }
123}
124
125impl HttpRequest {
126 pub fn with_header(mut self, name: &'static str, value: impl AsRef<str>) -> Self {
127 if let Ok(value) = HeaderValue::from_str(value.as_ref()) {
128 self.headers.append(name, value);
129 }
130 self
131 }
132
133 pub fn set_header(mut self, name: &'static str, value: impl AsRef<str>) -> Self {
134 if let Ok(value) = HeaderValue::from_str(value.as_ref()) {
135 self.headers.insert(name, value);
136 }
137 self
138 }
139
140 pub fn with_body<B: Serialize>(mut self, body: B) -> crate::Result<Self> {
141 match serde_json::to_string(&body) {
142 Ok(body) => {
143 self.body = Some(body);
144 Ok(self)
145 }
146 Err(err) => Err(Error::Serialize(format!(
147 "Failed to serialize request: {err}"
148 ))),
149 }
150 }
151
152 pub fn with_raw_body(mut self, body: String) -> Self {
153 self.body = Some(body);
154 self
155 }
156
157 pub async fn send<T>(self) -> crate::Result<T>
158 where
159 T: DeserializeOwned,
160 {
161 let response = self.send_raw().await?;
162 serde_json::from_slice::<T>(response.as_bytes())
163 .map_err(|err| Error::Serialize(format!("Failed to deserialize response: {err}")))
164 }
165
166 pub async fn send_raw(self) -> crate::Result<String> {
167 self.send_raw_with_headers().await.map(|(body, _)| body)
168 }
169
170 pub async fn send_raw_with_headers(self) -> crate::Result<(String, HeaderMap<HeaderValue>)> {
171 let mut request = self
172 .client
173 .request(self.method, &self.url)
174 .headers(self.headers);
175
176 if let Some(body) = self.body {
177 request = request.body(body);
178 }
179
180 let response = request
181 .send()
182 .await
183 .map_err(|err| Error::Api(format!("Failed to send request to {}: {err}", self.url)))?;
184
185 let code = response.status().as_u16();
186 let headers = response.headers().clone();
187 match code {
188 204 => Ok((String::new(), headers)),
189 200..=299 => response
190 .text()
191 .await
192 .map(|body| (body, headers))
193 .map_err(|err| {
194 Error::Api(format!("Failed to read response from {}: {err}", self.url))
195 }),
196 401 => Err(Error::Unauthorized),
197 404 => Err(Error::NotFound),
198 _ => {
199 let text = response.text().await.unwrap_or_default();
200 Err(Error::Api(http_status_message(code, &text)))
201 }
202 }
203 }
204
205 pub async fn send_with_retry<T>(self, max_retries: u32) -> crate::Result<T>
206 where
207 T: DeserializeOwned,
208 {
209 let mut attempts = 0;
210 let body = self.body;
211 loop {
212 let mut request = self
213 .client
214 .request(self.method.clone(), &self.url)
215 .headers(self.headers.clone());
216
217 if let Some(body) = body.as_ref() {
218 request = request.body(body.clone());
219 }
220
221 let response = request.send().await.map_err(|err| {
222 Error::Api(format!("Failed to send request to {}: {err}", self.url))
223 })?;
224
225 let code = response.status().as_u16();
226 return match code {
227 204 => serde_json::from_str("{}").map_err(|err| {
228 Error::Serialize(format!("Failed to create empty response: {err}"))
229 }),
230 200..=299 => {
231 let text = response.text().await.map_err(|err| {
232 Error::Api(format!("Failed to read response from {}: {err}", self.url))
233 })?;
234 let parse_target = if text.trim().is_empty() { "{}" } else { &text };
235 serde_json::from_str(parse_target).map_err(|err| {
236 Error::Serialize(format!("Failed to deserialize response: {err}"))
237 })
238 }
239 429 if attempts < max_retries => {
240 if let Some(retry_after) = response.headers().get("retry-after")
241 && let Ok(seconds) = retry_after.to_str().unwrap_or("0").parse::<u64>()
242 {
243 tokio::time::sleep(Duration::from_secs(seconds)).await;
244 attempts += 1;
245 continue;
246 }
247 Err(Error::Api("Rate limit exceeded".to_string()))
248 }
249 401 => Err(Error::Unauthorized),
250 404 => Err(Error::NotFound),
251 _ => {
252 let text = response.text().await.unwrap_or_default();
253 Err(Error::Api(http_status_message(code, &text)))
254 }
255 };
256 }
257 }
258}
259
260fn http_status_message(code: u16, body: &str) -> String {
261 let trimmed = body.trim();
262 if code == 400 {
263 if trimmed.is_empty() {
264 "BadRequest".to_string()
265 } else {
266 format!("BadRequest {trimmed}")
267 }
268 } else if trimmed.is_empty() {
269 format!("HTTP {code}")
270 } else {
271 format!("HTTP {code}: {trimmed}")
272 }
273}