1use futures::stream::StreamExt;
7use reqwest::header::HeaderMap;
8use reqwest::multipart::{self, Part};
9use reqwest::{redirect::Policy, ClientBuilder};
10use std::collections::HashMap;
11use std::time::Duration;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum HttpMethod {
16 Get,
17 Post,
18 Put,
19 Delete,
20 Patch,
21}
22
23impl std::fmt::Display for HttpMethod {
24 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25 match self {
26 HttpMethod::Get => write!(f, "GET"),
27 HttpMethod::Post => write!(f, "POST"),
28 HttpMethod::Put => write!(f, "PUT"),
29 HttpMethod::Delete => write!(f, "DELETE"),
30 HttpMethod::Patch => write!(f, "PATCH"),
31 }
32 }
33}
34
35impl From<crate::models::collection::Method> for HttpMethod {
36 fn from(method: crate::models::collection::Method) -> Self {
37 match method {
38 crate::models::collection::Method::Get => HttpMethod::Get,
39 crate::models::collection::Method::Post => HttpMethod::Post,
40 crate::models::collection::Method::Put => HttpMethod::Put,
41 crate::models::collection::Method::Delete => HttpMethod::Delete,
42 crate::models::collection::Method::Patch => HttpMethod::Patch,
43 }
44 }
45}
46
47pub type HttpResult<T> = Result<T, HttpError>;
49
50#[derive(Debug)]
52pub enum HttpError {
53 Timeout,
55 ConnectionError(String),
57 RedirectError(String),
59 RequestError(String),
61 ResponseError(String),
63 Other(String),
65}
66
67impl std::fmt::Display for HttpError {
68 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69 match self {
70 HttpError::Timeout => write!(f, "Request timed out"),
71 HttpError::ConnectionError(msg) => write!(f, "Connection error: {}", msg),
72 HttpError::RedirectError(msg) => write!(f, "Redirect error: {}", msg),
73 HttpError::RequestError(msg) => write!(f, "Request error: {}", msg),
74 HttpError::ResponseError(msg) => write!(f, "Response error: {}", msg),
75 HttpError::Other(msg) => write!(f, "{}", msg),
76 }
77 }
78}
79
80impl std::error::Error for HttpError {}
81
82impl From<reqwest::Error> for HttpError {
83 fn from(err: reqwest::Error) -> Self {
84 if err.is_timeout() {
85 HttpError::Timeout
86 } else if err.is_connect() {
87 HttpError::ConnectionError(err.to_string())
88 } else if err.is_redirect() {
89 HttpError::RedirectError(err.to_string())
90 } else {
91 HttpError::Other(err.to_string())
92 }
93 }
94}
95
96#[derive(Debug, Clone)]
98pub struct HttpResponse {
99 pub status: u16,
101 pub status_text: String,
103 pub headers: HashMap<String, String>,
105 pub body: String,
107 pub body_bytes: Vec<u8>,
109 pub elapsed_ms: u128,
111 pub url: String,
113}
114
115impl HttpResponse {
116 pub fn is_success(&self) -> bool {
118 (200..300).contains(&self.status)
119 }
120
121 pub fn is_redirect(&self) -> bool {
123 (300..400).contains(&self.status)
124 }
125
126 pub fn is_client_error(&self) -> bool {
128 (400..500).contains(&self.status)
129 }
130
131 pub fn is_server_error(&self) -> bool {
133 (500..600).contains(&self.status)
134 }
135
136 pub fn json<T: serde::de::DeserializeOwned>(&self) -> Result<T, serde_json::Error> {
138 serde_json::from_str(&self.body)
139 }
140}
141
142#[derive(Debug, Clone)]
144pub struct HttpRequest {
145 url: String,
146 method: HttpMethod,
147 headers: Vec<(String, String)>,
148 body: Option<String>,
149 body_bytes: Option<Vec<u8>>,
150 timeout: Option<Duration>,
151 follow_redirects: bool,
152}
153
154impl HttpRequest {
155 pub fn new(method: HttpMethod, url: &str) -> Self {
157 Self {
158 url: url.to_string(),
159 method,
160 headers: Vec::new(),
161 body: None,
162 body_bytes: None,
163 timeout: None,
164 follow_redirects: false,
165 }
166 }
167
168 pub fn headers(mut self, headers: Vec<(String, String)>) -> Self {
170 self.headers = headers;
171 self
172 }
173
174 pub fn header(mut self, key: &str, value: &str) -> Self {
176 self.headers.push((key.to_string(), value.to_string()));
177 self
178 }
179
180 pub fn body(mut self, body: &str) -> Self {
182 self.body = Some(body.to_string());
183 self
184 }
185
186 pub fn body_bytes(mut self, bytes: Vec<u8>) -> Self {
188 self.body_bytes = Some(bytes);
189 self
190 }
191
192 pub fn timeout(mut self, timeout: Duration) -> Self {
194 self.timeout = Some(timeout);
195 self
196 }
197
198 pub fn follow_redirects(mut self, follow: bool) -> Self {
200 self.follow_redirects = follow;
201 self
202 }
203
204 pub async fn send(self) -> HttpResult<HttpResponse> {
206 let client_builder = ClientBuilder::new();
207
208 let client_builder = if self.follow_redirects {
209 client_builder.redirect(Policy::default())
210 } else {
211 client_builder.redirect(Policy::none())
212 };
213
214 let client_builder = if let Some(timeout) = self.timeout {
215 client_builder.timeout(timeout)
216 } else {
217 client_builder
218 };
219
220 let client = client_builder
221 .build()
222 .map_err(|e| HttpError::RequestError(e.to_string()))?;
223
224 let header_map = build_header_map(&self.headers);
225
226 let method = match self.method {
227 HttpMethod::Get => reqwest::Method::GET,
228 HttpMethod::Post => reqwest::Method::POST,
229 HttpMethod::Put => reqwest::Method::PUT,
230 HttpMethod::Delete => reqwest::Method::DELETE,
231 HttpMethod::Patch => reqwest::Method::PATCH,
232 };
233
234 let start = std::time::Instant::now();
235
236 let request_builder = client.request(method, &self.url).headers(header_map);
237
238 let request_builder = if let Some(bytes) = self.body_bytes {
239 request_builder.body(bytes)
240 } else if let Some(body) = self.body {
241 request_builder.body(body)
242 } else {
243 request_builder
244 };
245
246 let response = request_builder.send().await?;
247
248 let elapsed = start.elapsed().as_millis();
249 let status = response.status().as_u16();
250 let status_text = response.status().to_string();
251 let url = response.url().to_string();
252
253 let mut headers = HashMap::new();
254 for (key, value) in response.headers().iter() {
255 if let Ok(v) = value.to_str() {
256 headers.insert(key.to_string(), v.to_string());
257 }
258 }
259
260 let body_bytes = response.bytes().await?.to_vec();
261 let body = String::from_utf8_lossy(&body_bytes).to_string();
262
263 Ok(HttpResponse {
264 status,
265 status_text,
266 headers,
267 body,
268 body_bytes,
269 elapsed_ms: elapsed,
270 url,
271 })
272 }
273
274 pub async fn send_streaming<F>(self, mut on_chunk: F) -> HttpResult<HttpResponse>
276 where
277 F: FnMut(&[u8]) -> Result<(), Box<dyn std::error::Error>> + Send,
278 {
279 let client_builder = ClientBuilder::new();
280
281 let client_builder = if self.follow_redirects {
282 client_builder.redirect(Policy::default())
283 } else {
284 client_builder.redirect(Policy::none())
285 };
286
287 let client_builder = if let Some(timeout) = self.timeout {
288 client_builder.timeout(timeout)
289 } else {
290 client_builder
291 };
292
293 let client = client_builder
294 .build()
295 .map_err(|e| HttpError::RequestError(e.to_string()))?;
296
297 let header_map = build_header_map(&self.headers);
298
299 let method = match self.method {
300 HttpMethod::Get => reqwest::Method::GET,
301 HttpMethod::Post => reqwest::Method::POST,
302 HttpMethod::Put => reqwest::Method::PUT,
303 HttpMethod::Delete => reqwest::Method::DELETE,
304 HttpMethod::Patch => reqwest::Method::PATCH,
305 };
306
307 let start = std::time::Instant::now();
308
309 let request_builder = client.request(method, &self.url).headers(header_map);
310
311 let request_builder = if let Some(bytes) = self.body_bytes {
312 request_builder.body(bytes)
313 } else if let Some(body) = self.body {
314 request_builder.body(body)
315 } else {
316 request_builder
317 };
318
319 let response = request_builder.send().await?;
320
321 let status = response.status().as_u16();
322 let status_text = response.status().to_string();
323 let url = response.url().to_string();
324
325 let mut headers = HashMap::new();
326 for (key, value) in response.headers().iter() {
327 if let Ok(v) = value.to_str() {
328 headers.insert(key.to_string(), v.to_string());
329 }
330 }
331
332 let mut stream = response.bytes_stream();
333 let mut all_bytes = Vec::new();
334
335 while let Some(chunk) = stream.next().await {
336 let chunk = chunk.map_err(|e| HttpError::ResponseError(e.to_string()))?;
337 all_bytes.extend_from_slice(&chunk);
338 on_chunk(&chunk).map_err(|e| HttpError::Other(e.to_string()))?;
339 }
340
341 let elapsed = start.elapsed().as_millis();
342 let body = String::from_utf8_lossy(&all_bytes).to_string();
343
344 Ok(HttpResponse {
345 status,
346 status_text,
347 headers,
348 body,
349 body_bytes: all_bytes,
350 elapsed_ms: elapsed,
351 url,
352 })
353 }
354}
355
356#[derive(Debug, Clone, Default)]
358pub struct HttpClient {
359 default_headers: Vec<(String, String)>,
360 timeout: Option<Duration>,
361 follow_redirects: bool,
362}
363
364impl HttpClient {
365 pub fn new() -> Self {
367 Self::default()
368 }
369
370 pub fn with_default_headers(mut self, headers: Vec<(String, String)>) -> Self {
372 self.default_headers = headers;
373 self
374 }
375
376 pub fn with_timeout(mut self, timeout: Duration) -> Self {
378 self.timeout = Some(timeout);
379 self
380 }
381
382 pub fn with_follow_redirects(mut self, follow: bool) -> Self {
384 self.follow_redirects = follow;
385 self
386 }
387
388 pub fn get(&self, url: &str) -> HttpRequest {
390 self.request(HttpMethod::Get, url)
391 }
392
393 pub fn post(&self, url: &str) -> HttpRequest {
395 self.request(HttpMethod::Post, url)
396 }
397
398 pub fn put(&self, url: &str) -> HttpRequest {
400 self.request(HttpMethod::Put, url)
401 }
402
403 pub fn delete(&self, url: &str) -> HttpRequest {
405 self.request(HttpMethod::Delete, url)
406 }
407
408 pub fn patch(&self, url: &str) -> HttpRequest {
410 self.request(HttpMethod::Patch, url)
411 }
412
413 pub fn request(&self, method: HttpMethod, url: &str) -> HttpRequest {
415 let mut request = HttpRequest::new(method, url)
416 .headers(self.default_headers.clone())
417 .follow_redirects(self.follow_redirects);
418
419 if let Some(timeout) = self.timeout {
420 request = request.timeout(timeout);
421 }
422
423 request
424 }
425
426 pub async fn execute_endpoint(
428 &self,
429 manager: &crate::core::collection_manager::CollectionManager,
430 collection: &str,
431 endpoint: &str,
432 ) -> HttpResult<HttpResponse> {
433 let col = manager
434 .get_collection(collection)
435 .map_err(|e| HttpError::Other(e.to_string()))?;
436 let req = manager
437 .get_endpoint(collection, endpoint)
438 .map_err(|e| HttpError::Other(e.to_string()))?;
439
440 let url = format!("{}{}", col.url, req.endpoint);
441 let headers = manager
442 .get_endpoint_headers(collection, endpoint)
443 .map_err(|e| HttpError::Other(e.to_string()))?;
444
445 let method: HttpMethod = req.method.into();
446
447 let mut request = HttpRequest::new(method, &url)
448 .headers(headers)
449 .follow_redirects(self.follow_redirects);
450
451 if let Some(body) = &req.body {
452 request = request.body(body);
453 }
454
455 if let Some(timeout) = self.timeout {
456 request = request.timeout(timeout);
457 }
458
459 request.send().await
460 }
461}
462
463pub fn build_header_map(headers: &[(String, String)]) -> HeaderMap {
465 let mut header_map = HeaderMap::new();
466 for (key, value) in headers {
467 if let Ok(header_name) = key.parse::<reqwest::header::HeaderName>() {
468 if let Ok(header_value) = value.parse() {
469 header_map.insert(header_name, header_value);
470 }
471 }
472 }
473 header_map
474}
475
476pub async fn execute_multipart_request(
478 url: &str,
479 method: HttpMethod,
480 headers: &[(String, String)],
481 file_bytes: Vec<u8>,
482 file_name: &str,
483 mime_type: &str,
484) -> HttpResult<HttpResponse> {
485 let client = ClientBuilder::new()
486 .redirect(Policy::none())
487 .build()
488 .map_err(|e| HttpError::RequestError(e.to_string()))?;
489
490 let header_map = build_header_map(headers);
491
492 let method = match method {
493 HttpMethod::Get => reqwest::Method::GET,
494 HttpMethod::Post => reqwest::Method::POST,
495 HttpMethod::Put => reqwest::Method::PUT,
496 HttpMethod::Delete => reqwest::Method::DELETE,
497 HttpMethod::Patch => reqwest::Method::PATCH,
498 };
499
500 let part = Part::bytes(file_bytes)
501 .file_name(file_name.to_string())
502 .mime_str(mime_type)
503 .map_err(|e| HttpError::RequestError(e.to_string()))?;
504
505 let form = multipart::Form::new().part("file", part);
506
507 let start = std::time::Instant::now();
508
509 let response = client
510 .request(method, url)
511 .headers(header_map)
512 .multipart(form)
513 .send()
514 .await?;
515
516 let elapsed = start.elapsed().as_millis();
517 let status = response.status().as_u16();
518 let status_text = response.status().to_string();
519 let url = response.url().to_string();
520
521 let mut headers = HashMap::new();
522 for (key, value) in response.headers().iter() {
523 if let Ok(v) = value.to_str() {
524 headers.insert(key.to_string(), v.to_string());
525 }
526 }
527
528 let body_bytes = response.bytes().await?.to_vec();
529 let body = String::from_utf8_lossy(&body_bytes).to_string();
530
531 Ok(HttpResponse {
532 status,
533 status_text,
534 headers,
535 body,
536 body_bytes,
537 elapsed_ms: elapsed,
538 url,
539 })
540}
541
542#[cfg(test)]
543mod tests {
544 use super::*;
545
546 #[test]
547 fn test_http_method_display() {
548 assert_eq!(HttpMethod::Get.to_string(), "GET");
549 assert_eq!(HttpMethod::Post.to_string(), "POST");
550 assert_eq!(HttpMethod::Put.to_string(), "PUT");
551 assert_eq!(HttpMethod::Delete.to_string(), "DELETE");
552 assert_eq!(HttpMethod::Patch.to_string(), "PATCH");
553 }
554
555 #[test]
556 fn test_http_response_status_checks() {
557 let response = HttpResponse {
558 status: 200,
559 status_text: "OK".to_string(),
560 headers: HashMap::new(),
561 body: String::new(),
562 body_bytes: Vec::new(),
563 elapsed_ms: 0,
564 url: String::new(),
565 };
566
567 assert!(response.is_success());
568 assert!(!response.is_redirect());
569 assert!(!response.is_client_error());
570 assert!(!response.is_server_error());
571 }
572
573 #[test]
574 fn test_build_header_map() {
575 let headers = vec![
576 ("Content-Type".to_string(), "application/json".to_string()),
577 ("Authorization".to_string(), "Bearer token".to_string()),
578 ];
579
580 let header_map = build_header_map(&headers);
581 assert_eq!(header_map.len(), 2);
582 }
583}