coman/core/
http_client.rs

1//! HTTP Client - Core HTTP request functionality
2//!
3//! This module provides a clean, library-friendly HTTP client API
4//! without any CLI dependencies (no progress bars, colored output, etc.)
5
6use 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/// HTTP methods supported by the client
14#[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
47/// Result type for HTTP operations
48pub type HttpResult<T> = Result<T, HttpError>;
49
50/// Errors that can occur during HTTP operations
51#[derive(Debug)]
52pub enum HttpError {
53    /// Request timed out
54    Timeout,
55    /// Connection error
56    ConnectionError(String),
57    /// Redirect error
58    RedirectError(String),
59    /// Request building error
60    RequestError(String),
61    /// Response error
62    ResponseError(String),
63    /// Generic error
64    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/// HTTP Response
97#[derive(Debug, Clone)]
98pub struct HttpResponse {
99    /// HTTP Version
100    pub version: String,
101    /// Response status code
102    pub status: u16,
103    /// Response status text
104    pub status_text: String,
105    /// Response headers
106    pub headers: HashMap<String, String>,
107    /// Response body as string
108    pub body: String,
109    /// Response body as bytes (for binary data)
110    // pub body_bytes: Vec<u8>,
111    /// Request duration in milliseconds
112    pub elapsed_ms: u128,
113    /// Final URL (after redirects)
114    pub url: String,
115}
116
117impl HttpResponse {
118    /// Check if the response status is successful (2xx)
119    pub fn is_success(&self) -> bool {
120        (200..300).contains(&self.status)
121    }
122
123    /// Check if the response status is a redirect (3xx)
124    pub fn is_redirect(&self) -> bool {
125        (300..400).contains(&self.status)
126    }
127
128    /// Check if the response status is a client error (4xx)
129    pub fn is_client_error(&self) -> bool {
130        (400..500).contains(&self.status)
131    }
132
133    /// Check if the response status is a server error (5xx)
134    pub fn is_server_error(&self) -> bool {
135        (500..600).contains(&self.status)
136    }
137
138    /// Try to parse the response body as JSON
139    pub fn json<T: serde::de::DeserializeOwned>(&self) -> Result<T, serde_json::Error> {
140        serde_json::from_str(&self.body)
141    }
142}
143
144/// HTTP Request Builder
145#[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    /// Create a new HTTP request
158    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    /// Set request headers
171    pub fn headers(mut self, headers: Vec<(String, String)>) -> Self {
172        self.headers = headers;
173        self
174    }
175
176    /// Add a single header
177    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    /// Set request body as string
183    pub fn body(mut self, body: &str) -> Self {
184        self.body = Some(body.to_string());
185        self
186    }
187
188    /// Set request body as bytes
189    pub fn body_bytes(mut self, bytes: Vec<u8>) -> Self {
190        self.body_bytes = Some(bytes);
191        self
192    }
193
194    /// Set request timeout
195    pub fn timeout(mut self, timeout: Duration) -> Self {
196        self.timeout = Some(timeout);
197        self
198    }
199
200    /// Enable following redirects
201    pub fn follow_redirects(mut self, follow: bool) -> Self {
202        self.follow_redirects = follow;
203        self
204    }
205
206    /// Execute the request
207    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    /// Execute the request and stream the response
278    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/// HTTP Client with convenience methods
425#[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    /// Create a new HTTP client
434    pub fn new() -> Self {
435        Self::default()
436    }
437
438    /// Set default headers for all requests
439    pub fn with_default_headers(mut self, headers: Vec<(String, String)>) -> Self {
440        self.default_headers = headers;
441        self
442    }
443
444    /// Set default timeout for all requests
445    pub fn with_timeout(mut self, timeout: Duration) -> Self {
446        self.timeout = Some(timeout);
447        self
448    }
449
450    /// Enable following redirects by default
451    pub fn with_follow_redirects(mut self, follow: bool) -> Self {
452        self.follow_redirects = follow;
453        self
454    }
455
456    /// Create a GET request
457    pub fn get(&self, url: &str) -> HttpRequest {
458        self.request(HttpMethod::Get, url)
459    }
460
461    /// Create a POST request
462    pub fn post(&self, url: &str) -> HttpRequest {
463        self.request(HttpMethod::Post, url)
464    }
465
466    /// Create a PUT request
467    pub fn put(&self, url: &str) -> HttpRequest {
468        self.request(HttpMethod::Put, url)
469    }
470
471    /// Create a DELETE request
472    pub fn delete(&self, url: &str) -> HttpRequest {
473        self.request(HttpMethod::Delete, url)
474    }
475
476    /// Create a PATCH request
477    pub fn patch(&self, url: &str) -> HttpRequest {
478        self.request(HttpMethod::Patch, url)
479    }
480
481    /// Create a request with a specific method
482    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    /// Execute a request from a collection endpoint
495    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
531/// Build a HeaderMap from a vector of key-value pairs
532pub 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}