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 crate::core::errors::HttpError;
7use crate::core::http_request::HttpRequest;
8use crate::core::http_response::HttpResponse;
9use crate::CollectionManager;
10use std::time::Duration;
11
12/// HTTP methods supported by the client
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum HttpMethod {
15    Get,
16    Post,
17    Put,
18    Delete,
19    Patch,
20}
21
22impl std::fmt::Display for HttpMethod {
23    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
24        match self {
25            HttpMethod::Get => write!(f, "GET"),
26            HttpMethod::Post => write!(f, "POST"),
27            HttpMethod::Put => write!(f, "PUT"),
28            HttpMethod::Delete => write!(f, "DELETE"),
29            HttpMethod::Patch => write!(f, "PATCH"),
30        }
31    }
32}
33
34impl From<crate::models::collection::Method> for HttpMethod {
35    fn from(method: crate::models::collection::Method) -> Self {
36        match method {
37            crate::models::collection::Method::Get => HttpMethod::Get,
38            crate::models::collection::Method::Post => HttpMethod::Post,
39            crate::models::collection::Method::Put => HttpMethod::Put,
40            crate::models::collection::Method::Delete => HttpMethod::Delete,
41            crate::models::collection::Method::Patch => HttpMethod::Patch,
42        }
43    }
44}
45
46/// Result type for HTTP operations
47pub type HttpResult<T> = Result<T, HttpError>;
48
49/// HTTP Client with convenience methods
50#[derive(Debug, Clone, Default)]
51pub struct HttpClient {
52    default_headers: Vec<(String, String)>,
53    timeout: Option<Duration>,
54    follow_redirects: bool,
55}
56
57impl HttpClient {
58    /// Create a new HTTP client
59    pub fn new() -> Self {
60        Self::default()
61    }
62
63    /// Set default headers for all requests
64    pub fn with_default_headers(mut self, headers: Vec<(String, String)>) -> Self {
65        self.default_headers = headers;
66        self
67    }
68
69    /// Set default timeout for all requests
70    pub fn with_timeout(mut self, timeout: Duration) -> Self {
71        self.timeout = Some(timeout);
72        self
73    }
74
75    /// Enable following redirects by default
76    pub fn with_follow_redirects(mut self, follow: bool) -> Self {
77        self.follow_redirects = follow;
78        self
79    }
80
81    /// Create a GET request
82    pub fn get(&self, url: &str) -> HttpRequest {
83        self.request(HttpMethod::Get, url)
84    }
85
86    /// Create a POST request
87    pub fn post(&self, url: &str) -> HttpRequest {
88        self.request(HttpMethod::Post, url)
89    }
90
91    /// Create a PUT request
92    pub fn put(&self, url: &str) -> HttpRequest {
93        self.request(HttpMethod::Put, url)
94    }
95
96    /// Create a DELETE request
97    pub fn delete(&self, url: &str) -> HttpRequest {
98        self.request(HttpMethod::Delete, url)
99    }
100
101    /// Create a PATCH request
102    pub fn patch(&self, url: &str) -> HttpRequest {
103        self.request(HttpMethod::Patch, url)
104    }
105
106    /// Create a request with a specific method
107    pub fn request(&self, method: HttpMethod, url: &str) -> HttpRequest {
108        let mut request = HttpRequest::new(method, url)
109            .headers(self.default_headers.clone())
110            .follow_redirects(self.follow_redirects);
111
112        if let Some(timeout) = self.timeout {
113            request = request.timeout(timeout);
114        }
115
116        request
117    }
118
119    /// Execute a request from a collection endpoint
120    pub async fn execute_endpoint(
121        &self,
122        manager: CollectionManager,
123        col_name: &str,
124        ep_name: &str,
125    ) -> HttpResult<HttpResponse> {
126        let col = manager
127            .get_collection_imutable(col_name)
128            .map_err(|e| HttpError::Other(e.to_string()))?;
129        let req = col.get_request(ep_name).ok_or_else(|| {
130            HttpError::Other(format!(
131                "Endpoint '{}' not found in collection '{}'",
132                ep_name, col_name
133            ))
134        })?;
135
136        let url = format!("{}{}", col.url, req.endpoint);
137        let headers = req.headers;
138
139        let method: HttpMethod = req.method.into();
140
141        let mut request = HttpRequest::new(method, &url)
142            .headers(headers)
143            .follow_redirects(self.follow_redirects);
144
145        if let Some(body) = &req.body {
146            request = request.body(body);
147        }
148
149        if let Some(timeout) = self.timeout {
150            request = request.timeout(timeout);
151        }
152
153        request.send().await
154    }
155}
156
157#[cfg(test)]
158mod tests {
159    use std::collections::HashMap;
160
161    use crate::core::utils::build_header_map;
162
163    use super::*;
164
165    #[test]
166    fn test_http_method_display() {
167        assert_eq!(HttpMethod::Get.to_string(), "GET");
168        assert_eq!(HttpMethod::Post.to_string(), "POST");
169        assert_eq!(HttpMethod::Put.to_string(), "PUT");
170        assert_eq!(HttpMethod::Delete.to_string(), "DELETE");
171        assert_eq!(HttpMethod::Patch.to_string(), "PATCH");
172    }
173
174    #[test]
175    fn test_http_response_status_checks() {
176        let response = HttpResponse {
177            version: "HTTP/1.1".to_string(),
178            status: 200,
179            status_text: "OK".to_string(),
180            headers: HashMap::new(),
181            body: String::new(),
182            elapsed_ms: 0,
183            url: String::new(),
184        };
185
186        assert!(response.is_success());
187        assert!(!response.is_redirect());
188        assert!(!response.is_client_error());
189        assert!(!response.is_server_error());
190    }
191
192    #[test]
193    fn test_build_header_map() {
194        let headers = vec![
195            ("Content-Type".to_string(), "application/json".to_string()),
196            ("Authorization".to_string(), "Bearer token".to_string()),
197        ];
198
199        let header_map = build_header_map(&headers);
200        assert_eq!(header_map.len(), 2);
201    }
202}