Skip to main content

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(col_name)
128            .await
129            .map_err(|e| HttpError::Other(e.to_string()))?
130            .ok_or_else(|| HttpError::Other(format!("Collection '{}' not found", col_name)))?;
131
132        let req = col.get_request(ep_name).ok_or_else(|| {
133            HttpError::Other(format!(
134                "Endpoint '{}' not found in collection '{}'",
135                ep_name, col_name
136            ))
137        })?;
138
139        let url = format!("{}{}", col.url, req.endpoint);
140        let headers = req.headers;
141
142        let method: HttpMethod = req.method.into();
143
144        let mut request = HttpRequest::new(method, &url)
145            .headers(headers)
146            .follow_redirects(self.follow_redirects);
147
148        if let Some(body) = &req.body {
149            request = request.body(body);
150        }
151
152        if let Some(timeout) = self.timeout {
153            request = request.timeout(timeout);
154        }
155
156        request.send().await
157    }
158}
159
160#[cfg(test)]
161mod tests {
162
163    use crate::core::utils::build_header_map;
164
165    use super::*;
166
167    #[test]
168    fn test_http_method_display() {
169        assert_eq!(HttpMethod::Get.to_string(), "GET");
170        assert_eq!(HttpMethod::Post.to_string(), "POST");
171        assert_eq!(HttpMethod::Put.to_string(), "PUT");
172        assert_eq!(HttpMethod::Delete.to_string(), "DELETE");
173        assert_eq!(HttpMethod::Patch.to_string(), "PATCH");
174    }
175
176    #[test]
177    fn test_http_response_status_checks() {
178        let response = HttpResponse {
179            version: "HTTP/1.1".to_string(),
180            status: 200,
181            status_text: "OK".to_string(),
182            headers: Vec::new(),
183            body: String::new(),
184            elapsed_ms: 0,
185            url: String::new(),
186        };
187
188        assert!(response.is_success());
189        assert!(!response.is_redirect());
190        assert!(!response.is_client_error());
191        assert!(!response.is_server_error());
192    }
193
194    #[test]
195    fn test_build_header_map() {
196        let headers = vec![
197            ("Content-Type".to_string(), "application/json".to_string()),
198            ("Authorization".to_string(), "Bearer token".to_string()),
199        ];
200
201        let header_map = build_header_map(&headers);
202        assert_eq!(header_map.len(), 2);
203    }
204}