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    /// Response status code
100    pub status: u16,
101    /// Response status text
102    pub status_text: String,
103    /// Response headers
104    pub headers: HashMap<String, String>,
105    /// Response body as string
106    pub body: String,
107    /// Response body as bytes (for binary data)
108    pub body_bytes: Vec<u8>,
109    /// Request duration in milliseconds
110    pub elapsed_ms: u128,
111    /// Final URL (after redirects)
112    pub url: String,
113}
114
115impl HttpResponse {
116    /// Check if the response status is successful (2xx)
117    pub fn is_success(&self) -> bool {
118        (200..300).contains(&self.status)
119    }
120
121    /// Check if the response status is a redirect (3xx)
122    pub fn is_redirect(&self) -> bool {
123        (300..400).contains(&self.status)
124    }
125
126    /// Check if the response status is a client error (4xx)
127    pub fn is_client_error(&self) -> bool {
128        (400..500).contains(&self.status)
129    }
130
131    /// Check if the response status is a server error (5xx)
132    pub fn is_server_error(&self) -> bool {
133        (500..600).contains(&self.status)
134    }
135
136    /// Try to parse the response body as JSON
137    pub fn json<T: serde::de::DeserializeOwned>(&self) -> Result<T, serde_json::Error> {
138        serde_json::from_str(&self.body)
139    }
140}
141
142/// HTTP Request Builder
143#[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    /// Create a new HTTP request
156    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    /// Set request headers
169    pub fn headers(mut self, headers: Vec<(String, String)>) -> Self {
170        self.headers = headers;
171        self
172    }
173
174    /// Add a single header
175    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    /// Set request body as string
181    pub fn body(mut self, body: &str) -> Self {
182        self.body = Some(body.to_string());
183        self
184    }
185
186    /// Set request body as bytes
187    pub fn body_bytes(mut self, bytes: Vec<u8>) -> Self {
188        self.body_bytes = Some(bytes);
189        self
190    }
191
192    /// Set request timeout
193    pub fn timeout(mut self, timeout: Duration) -> Self {
194        self.timeout = Some(timeout);
195        self
196    }
197
198    /// Enable following redirects
199    pub fn follow_redirects(mut self, follow: bool) -> Self {
200        self.follow_redirects = follow;
201        self
202    }
203
204    /// Execute the request
205    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    /// Execute the request and stream the response
275    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/// HTTP Client with convenience methods
357#[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    /// Create a new HTTP client
366    pub fn new() -> Self {
367        Self::default()
368    }
369
370    /// Set default headers for all requests
371    pub fn with_default_headers(mut self, headers: Vec<(String, String)>) -> Self {
372        self.default_headers = headers;
373        self
374    }
375
376    /// Set default timeout for all requests
377    pub fn with_timeout(mut self, timeout: Duration) -> Self {
378        self.timeout = Some(timeout);
379        self
380    }
381
382    /// Enable following redirects by default
383    pub fn with_follow_redirects(mut self, follow: bool) -> Self {
384        self.follow_redirects = follow;
385        self
386    }
387
388    /// Create a GET request
389    pub fn get(&self, url: &str) -> HttpRequest {
390        self.request(HttpMethod::Get, url)
391    }
392
393    /// Create a POST request
394    pub fn post(&self, url: &str) -> HttpRequest {
395        self.request(HttpMethod::Post, url)
396    }
397
398    /// Create a PUT request
399    pub fn put(&self, url: &str) -> HttpRequest {
400        self.request(HttpMethod::Put, url)
401    }
402
403    /// Create a DELETE request
404    pub fn delete(&self, url: &str) -> HttpRequest {
405        self.request(HttpMethod::Delete, url)
406    }
407
408    /// Create a PATCH request
409    pub fn patch(&self, url: &str) -> HttpRequest {
410        self.request(HttpMethod::Patch, url)
411    }
412
413    /// Create a request with a specific method
414    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    /// Execute a request from a collection endpoint
427    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
463/// Build a HeaderMap from a vector of key-value pairs
464pub 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
476/// Execute a multipart form request
477pub 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}