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 version: String,
101 pub status: u16,
103 pub status_text: String,
105 pub headers: HashMap<String, String>,
107 pub body: String,
109 pub elapsed_ms: u128,
113 pub url: String,
115}
116
117impl HttpResponse {
118 pub fn is_success(&self) -> bool {
120 (200..300).contains(&self.status)
121 }
122
123 pub fn is_redirect(&self) -> bool {
125 (300..400).contains(&self.status)
126 }
127
128 pub fn is_client_error(&self) -> bool {
130 (400..500).contains(&self.status)
131 }
132
133 pub fn is_server_error(&self) -> bool {
135 (500..600).contains(&self.status)
136 }
137
138 pub fn json<T: serde::de::DeserializeOwned>(&self) -> Result<T, serde_json::Error> {
140 serde_json::from_str(&self.body)
141 }
142}
143
144#[derive(Debug, Clone)]
146pub struct HttpRequest {
147 url: String,
148 method: HttpMethod,
149 headers: Vec<(String, String)>,
150 body: Option<String>,
151 body_bytes: Option<Vec<u8>>,
152 timeout: Option<Duration>,
153 follow_redirects: bool,
154}
155
156impl HttpRequest {
157 pub fn new(method: HttpMethod, url: &str) -> Self {
159 Self {
160 url: url.to_string(),
161 method,
162 headers: Vec::new(),
163 body: None,
164 body_bytes: None,
165 timeout: None,
166 follow_redirects: false,
167 }
168 }
169
170 pub fn headers(mut self, headers: Vec<(String, String)>) -> Self {
172 self.headers = headers;
173 self
174 }
175
176 pub fn header(mut self, key: &str, value: &str) -> Self {
178 self.headers.push((key.to_string(), value.to_string()));
179 self
180 }
181
182 pub fn body(mut self, body: &str) -> Self {
184 self.body = Some(body.to_string());
185 self
186 }
187
188 pub fn body_bytes(mut self, bytes: Vec<u8>) -> Self {
190 self.body_bytes = Some(bytes);
191 self
192 }
193
194 pub fn timeout(mut self, timeout: Duration) -> Self {
196 self.timeout = Some(timeout);
197 self
198 }
199
200 pub fn follow_redirects(mut self, follow: bool) -> Self {
202 self.follow_redirects = follow;
203 self
204 }
205
206 pub async fn send(self) -> HttpResult<HttpResponse> {
208 let client_builder = ClientBuilder::new();
209
210 let client_builder = if self.follow_redirects {
211 client_builder.redirect(Policy::default())
212 } else {
213 client_builder.redirect(Policy::none())
214 };
215
216 let client_builder = if let Some(timeout) = self.timeout {
217 client_builder.timeout(timeout)
218 } else {
219 client_builder
220 };
221
222 let client = client_builder
223 .build()
224 .map_err(|e| HttpError::RequestError(e.to_string()))?;
225
226 let header_map = build_header_map(&self.headers);
227
228 let method = match self.method {
229 HttpMethod::Get => reqwest::Method::GET,
230 HttpMethod::Post => reqwest::Method::POST,
231 HttpMethod::Put => reqwest::Method::PUT,
232 HttpMethod::Delete => reqwest::Method::DELETE,
233 HttpMethod::Patch => reqwest::Method::PATCH,
234 };
235
236 let start = std::time::Instant::now();
237
238 let request_builder = client.request(method, &self.url).headers(header_map);
239
240 let request_builder = if let Some(bytes) = self.body_bytes {
241 request_builder.body(bytes)
242 } else if let Some(body) = self.body {
243 request_builder.body(body)
244 } else {
245 request_builder
246 };
247
248 let response = request_builder.send().await?;
249
250 let elapsed = start.elapsed().as_millis();
251 let status = response.status().as_u16();
252 let status_text = response.status().to_string();
253 let url = response.url().to_string();
254 let version = format!("{:?}", response.version());
255
256 let mut headers = HashMap::new();
257 for (key, value) in response.headers().iter() {
258 if let Ok(v) = value.to_str() {
259 headers.insert(key.to_string(), v.to_string());
260 }
261 }
262
263 let body_bytes = response.bytes().await?.to_vec();
264 let body = String::from_utf8_lossy(&body_bytes).to_string();
265
266 Ok(HttpResponse {
267 version,
268 status,
269 status_text,
270 headers,
271 body,
272 elapsed_ms: elapsed,
273 url,
274 })
275 }
276
277 pub async fn send_streaming<F>(self, mut on_chunk: F) -> HttpResult<HttpResponse>
279 where
280 F: FnMut(&[u8]) -> Result<(), Box<dyn std::error::Error>> + Send,
281 {
282 let client_builder = ClientBuilder::new();
283
284 let client_builder = if self.follow_redirects {
285 client_builder.redirect(Policy::default())
286 } else {
287 client_builder.redirect(Policy::none())
288 };
289
290 let client_builder = if let Some(timeout) = self.timeout {
291 client_builder.timeout(timeout)
292 } else {
293 client_builder
294 };
295
296 let client = client_builder
297 .build()
298 .map_err(|e| HttpError::RequestError(e.to_string()))?;
299
300 let header_map = build_header_map(&self.headers);
301
302 let method = match self.method {
303 HttpMethod::Get => reqwest::Method::GET,
304 HttpMethod::Post => reqwest::Method::POST,
305 HttpMethod::Put => reqwest::Method::PUT,
306 HttpMethod::Delete => reqwest::Method::DELETE,
307 HttpMethod::Patch => reqwest::Method::PATCH,
308 };
309
310 let start = std::time::Instant::now();
311
312 let request_builder = client.request(method, &self.url).headers(header_map);
313
314 let request_builder = if let Some(bytes) = self.body_bytes {
315 request_builder.body(bytes)
316 } else if let Some(body) = self.body {
317 request_builder.body(body)
318 } else {
319 request_builder
320 };
321
322 let response = request_builder.send().await?;
323
324 let status = response.status().as_u16();
325 let status_text = response.status().to_string();
326 let url = response.url().to_string();
327 let version = format!("{:?}", response.version());
328
329 let mut headers = HashMap::new();
330 for (key, value) in response.headers().iter() {
331 if let Ok(v) = value.to_str() {
332 headers.insert(key.to_string(), v.to_string());
333 }
334 }
335
336 let mut stream = response.bytes_stream();
337
338 while let Some(chunk) = stream.next().await {
339 let chunk = chunk.map_err(|e| HttpError::ResponseError(e.to_string()))?;
340 on_chunk(&chunk).map_err(|e| HttpError::Other(e.to_string()))?;
341 }
342
343 let elapsed = start.elapsed().as_millis();
344
345 Ok(HttpResponse {
346 version,
347 status,
348 status_text,
349 headers,
350 body: String::new(),
351 elapsed_ms: elapsed,
352 url,
353 })
354 }
355
356 pub async fn send_multipart(self, part: Part) -> HttpResult<HttpResponse> {
357 let client_builder = ClientBuilder::new();
358
359 let client_builder = if self.follow_redirects {
360 client_builder.redirect(Policy::default())
361 } else {
362 client_builder.redirect(Policy::none())
363 };
364
365 let client_builder = if let Some(timeout) = self.timeout {
366 client_builder.timeout(timeout)
367 } else {
368 client_builder
369 };
370
371 let client = client_builder
372 .build()
373 .map_err(|e| HttpError::RequestError(e.to_string()))?;
374
375 let header_map = build_header_map(&self.headers);
376
377 let method = match self.method {
378 HttpMethod::Get => reqwest::Method::GET,
379 HttpMethod::Post => reqwest::Method::POST,
380 HttpMethod::Put => reqwest::Method::PUT,
381 HttpMethod::Delete => reqwest::Method::DELETE,
382 HttpMethod::Patch => reqwest::Method::PATCH,
383 };
384
385 let form = multipart::Form::new().part("file", part);
386
387 let start = std::time::Instant::now();
388
389 let response = client
390 .request(method, &self.url)
391 .headers(header_map)
392 .multipart(form)
393 .send()
394 .await?;
395
396 let elapsed = start.elapsed().as_millis();
397 let status = response.status().as_u16();
398 let status_text = response.status().to_string();
399 let url = response.url().to_string();
400 let version = format!("{:?}", response.version());
401
402 let mut headers = HashMap::new();
403 for (key, value) in response.headers().iter() {
404 if let Ok(v) = value.to_str() {
405 headers.insert(key.to_string(), v.to_string());
406 }
407 }
408
409 let body_bytes = response.bytes().await?.to_vec();
410 let body = String::from_utf8_lossy(&body_bytes).to_string();
411
412 Ok(HttpResponse {
413 version,
414 status,
415 status_text,
416 headers,
417 body,
418 elapsed_ms: elapsed,
419 url,
420 })
421 }
422}
423
424#[derive(Debug, Clone, Default)]
426pub struct HttpClient {
427 default_headers: Vec<(String, String)>,
428 timeout: Option<Duration>,
429 follow_redirects: bool,
430}
431
432impl HttpClient {
433 pub fn new() -> Self {
435 Self::default()
436 }
437
438 pub fn with_default_headers(mut self, headers: Vec<(String, String)>) -> Self {
440 self.default_headers = headers;
441 self
442 }
443
444 pub fn with_timeout(mut self, timeout: Duration) -> Self {
446 self.timeout = Some(timeout);
447 self
448 }
449
450 pub fn with_follow_redirects(mut self, follow: bool) -> Self {
452 self.follow_redirects = follow;
453 self
454 }
455
456 pub fn get(&self, url: &str) -> HttpRequest {
458 self.request(HttpMethod::Get, url)
459 }
460
461 pub fn post(&self, url: &str) -> HttpRequest {
463 self.request(HttpMethod::Post, url)
464 }
465
466 pub fn put(&self, url: &str) -> HttpRequest {
468 self.request(HttpMethod::Put, url)
469 }
470
471 pub fn delete(&self, url: &str) -> HttpRequest {
473 self.request(HttpMethod::Delete, url)
474 }
475
476 pub fn patch(&self, url: &str) -> HttpRequest {
478 self.request(HttpMethod::Patch, url)
479 }
480
481 pub fn request(&self, method: HttpMethod, url: &str) -> HttpRequest {
483 let mut request = HttpRequest::new(method, url)
484 .headers(self.default_headers.clone())
485 .follow_redirects(self.follow_redirects);
486
487 if let Some(timeout) = self.timeout {
488 request = request.timeout(timeout);
489 }
490
491 request
492 }
493
494 pub async fn execute_endpoint(
496 &self,
497 manager: &crate::core::collection_manager::CollectionManager,
498 collection: &str,
499 endpoint: &str,
500 ) -> HttpResult<HttpResponse> {
501 let col = manager
502 .get_collection(collection)
503 .map_err(|e| HttpError::Other(e.to_string()))?;
504 let req = manager
505 .get_endpoint(collection, endpoint)
506 .map_err(|e| HttpError::Other(e.to_string()))?;
507
508 let url = format!("{}{}", col.url, req.endpoint);
509 let headers = manager
510 .get_endpoint_headers(collection, endpoint)
511 .map_err(|e| HttpError::Other(e.to_string()))?;
512
513 let method: HttpMethod = req.method.into();
514
515 let mut request = HttpRequest::new(method, &url)
516 .headers(headers)
517 .follow_redirects(self.follow_redirects);
518
519 if let Some(body) = &req.body {
520 request = request.body(body);
521 }
522
523 if let Some(timeout) = self.timeout {
524 request = request.timeout(timeout);
525 }
526
527 request.send().await
528 }
529}
530
531pub fn build_header_map(headers: &[(String, String)]) -> HeaderMap {
533 let mut header_map = HeaderMap::new();
534 for (key, value) in headers {
535 if let Ok(header_name) = key.parse::<reqwest::header::HeaderName>() {
536 if let Ok(header_value) = value.parse() {
537 header_map.insert(header_name, header_value);
538 }
539 }
540 }
541 header_map
542}
543
544#[cfg(test)]
545mod tests {
546 use super::*;
547
548 #[test]
549 fn test_http_method_display() {
550 assert_eq!(HttpMethod::Get.to_string(), "GET");
551 assert_eq!(HttpMethod::Post.to_string(), "POST");
552 assert_eq!(HttpMethod::Put.to_string(), "PUT");
553 assert_eq!(HttpMethod::Delete.to_string(), "DELETE");
554 assert_eq!(HttpMethod::Patch.to_string(), "PATCH");
555 }
556
557 #[test]
558 fn test_http_response_status_checks() {
559 let response = HttpResponse {
560 version: "HTTP/1.1".to_string(),
561 status: 200,
562 status_text: "OK".to_string(),
563 headers: HashMap::new(),
564 body: String::new(),
565 elapsed_ms: 0,
566 url: String::new(),
567 };
568
569 assert!(response.is_success());
570 assert!(!response.is_redirect());
571 assert!(!response.is_client_error());
572 assert!(!response.is_server_error());
573 }
574
575 #[test]
576 fn test_build_header_map() {
577 let headers = vec![
578 ("Content-Type".to_string(), "application/json".to_string()),
579 ("Authorization".to_string(), "Bearer token".to_string()),
580 ];
581
582 let header_map = build_header_map(&headers);
583 assert_eq!(header_map.len(), 2);
584 }
585}