avx_http/
client.rs

1//! HTTP Client implementation
2
3use crate::error::{Error, Result};
4use crate::common;
5use bytes::Bytes;
6use http::{HeaderMap, HeaderName, HeaderValue, Method, StatusCode};
7use std::time::Duration;
8use tokio::io::{AsyncReadExt, AsyncWriteExt};
9use tokio::net::TcpStream;
10
11/// HTTP Client for making requests
12#[derive(Clone)]
13pub struct Client {
14    /// Client configuration
15    pub config: ClientConfig,
16}
17
18/// Client configuration
19#[derive(Clone)]
20pub struct ClientConfig {
21    /// Request timeout
22    pub timeout: Duration,
23    /// Default headers
24    pub default_headers: HeaderMap,
25    /// AVL Platform auth token
26    pub avl_auth: Option<String>,
27    /// Preferred region
28    pub region: Option<String>,
29    /// Enable compression
30    pub compression: bool,
31    /// Maximum redirects
32    pub max_redirects: usize,
33}
34
35impl Default for ClientConfig {
36    fn default() -> Self {
37        Self {
38            timeout: common::DEFAULT_TIMEOUT,
39            default_headers: HeaderMap::new(),
40            avl_auth: None,
41            region: None,
42            compression: false,
43            max_redirects: 5,
44        }
45    }
46}
47
48impl Client {
49    /// Create a new client with default configuration
50    pub fn new() -> Self {
51        Self {
52            config: ClientConfig::default(),
53        }
54    }
55
56    /// Create a client builder for custom configuration
57    pub fn builder() -> ClientBuilder {
58        ClientBuilder::new()
59    }
60
61    /// Make a GET request
62    pub fn get(&self, url: impl Into<String>) -> RequestBuilder {
63        self.request(Method::GET, url)
64    }
65
66    /// Make a POST request
67    pub fn post(&self, url: impl Into<String>) -> RequestBuilder {
68        self.request(Method::POST, url)
69    }
70
71    /// Make a PUT request
72    pub fn put(&self, url: impl Into<String>) -> RequestBuilder {
73        self.request(Method::PUT, url)
74    }
75
76    /// Make a DELETE request
77    pub fn delete(&self, url: impl Into<String>) -> RequestBuilder {
78        self.request(Method::DELETE, url)
79    }
80
81    /// Make a PATCH request
82    pub fn patch(&self, url: impl Into<String>) -> RequestBuilder {
83        self.request(Method::PATCH, url)
84    }
85
86    /// Make a HEAD request
87    pub fn head(&self, url: impl Into<String>) -> RequestBuilder {
88        self.request(Method::HEAD, url)
89    }
90
91    /// Create a request with custom method
92    pub fn request(&self, method: Method, url: impl Into<String>) -> RequestBuilder {
93        RequestBuilder {
94            client: self.clone(),
95            method,
96            url: url.into(),
97            headers: self.config.default_headers.clone(),
98            body: None,
99            query_params: Vec::new(),
100            timeout: Some(self.config.timeout),
101        }
102    }
103}
104
105impl Default for Client {
106    fn default() -> Self {
107        Self::new()
108    }
109}
110
111/// Builder for configuring HTTP client
112pub struct ClientBuilder {
113    config: ClientConfig,
114}
115
116impl ClientBuilder {
117    /// Create a new client builder
118    pub fn new() -> Self {
119        Self {
120            config: ClientConfig::default(),
121        }
122    }
123
124    /// Set request timeout
125    pub fn timeout(mut self, timeout: Duration) -> Self {
126        self.config.timeout = timeout;
127        self
128    }
129
130    /// Add a default header for all requests
131    pub fn default_header(mut self, name: impl AsRef<str>, value: impl AsRef<str>) -> Result<Self> {
132        let name = HeaderName::from_bytes(name.as_ref().as_bytes())
133            .map_err(|_| Error::InvalidHeader {
134                name: name.as_ref().to_string(),
135                value: value.as_ref().to_string(),
136            })?;
137        let value = HeaderValue::from_str(value.as_ref())
138            .map_err(|_| Error::InvalidHeader {
139                name: name.to_string(),
140                value: value.as_ref().to_string(),
141            })?;
142        self.config.default_headers.insert(name, value);
143        Ok(self)
144    }
145
146    /// Set AVL Platform authentication token
147    pub fn avl_auth(mut self, token: impl Into<String>) -> Self {
148        self.config.avl_auth = Some(token.into());
149        self
150    }
151
152    /// Set preferred region for regional routing
153    pub fn region(mut self, region: impl Into<String>) -> Self {
154        self.config.region = Some(region.into());
155        self
156    }
157
158    /// Enable automatic compression
159    pub fn compression(mut self, enabled: bool) -> Self {
160        self.config.compression = enabled;
161        self
162    }
163
164    /// Set maximum number of redirects to follow
165    pub fn max_redirects(mut self, max: usize) -> Self {
166        self.config.max_redirects = max;
167        self
168    }
169
170    /// Build the client
171    pub fn build(self) -> Result<Client> {
172        Ok(Client { config: self.config })
173    }
174}
175
176impl Default for ClientBuilder {
177    fn default() -> Self {
178        Self::new()
179    }
180}
181
182/// Builder for constructing HTTP requests
183pub struct RequestBuilder {
184    client: Client,
185    /// HTTP method
186    pub method: Method,
187    url: String,
188    headers: HeaderMap,
189    body: Option<Bytes>,
190    query_params: Vec<(String, String)>,
191    timeout: Option<Duration>,
192}
193
194impl RequestBuilder {
195    /// Add a header to the request
196    pub fn header(mut self, name: impl AsRef<str>, value: impl AsRef<str>) -> Result<Self> {
197        let name = HeaderName::from_bytes(name.as_ref().as_bytes())
198            .map_err(|_| Error::InvalidHeader {
199                name: name.as_ref().to_string(),
200                value: value.as_ref().to_string(),
201            })?;
202        let value = HeaderValue::from_str(value.as_ref())
203            .map_err(|_| Error::InvalidHeader {
204                name: name.to_string(),
205                value: value.as_ref().to_string(),
206            })?;
207        self.headers.insert(name, value);
208        Ok(self)
209    }
210
211    /// Set request body
212    pub fn body(mut self, body: impl Into<Bytes>) -> Self {
213        self.body = Some(body.into());
214        self
215    }
216
217    /// Set JSON body
218    pub fn json<T: serde::Serialize>(mut self, json: &T) -> Result<Self> {
219        let json_str = serde_json::to_string(json)
220            .map_err(|e| Error::JsonError { source: e.to_string() })?;
221        self.body = Some(Bytes::from(json_str));
222        self = self.header("Content-Type", "application/json")?;
223        Ok(self)
224    }
225
226    /// Add query parameter
227    pub fn query(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
228        self.query_params.push((key.into(), value.into()));
229        self
230    }
231
232    /// Set request timeout
233    pub fn timeout(mut self, timeout: Duration) -> Self {
234        self.timeout = Some(timeout);
235        self
236    }
237
238    /// Send the request
239    pub async fn send(self) -> Result<Response> {
240        // Build full URL with query params
241        let mut full_url = self.url.clone();
242        if !self.query_params.is_empty() {
243            let query_string = self.query_params
244                .iter()
245                .map(|(k, v)| format!("{}={}", urlencoding::encode(k), urlencoding::encode(v)))
246                .collect::<Vec<_>>()
247                .join("&");
248            full_url = format!("{}?{}", full_url, query_string);
249        }
250
251        // Parse URL
252        let (host, port, _is_https) = common::parse_url(&full_url)?;
253
254        // Connect to server
255        let addr = format!("{}:{}", host, port);
256        let mut stream = tokio::time::timeout(
257            self.timeout.unwrap_or(common::DEFAULT_TIMEOUT),
258            TcpStream::connect(&addr)
259        )
260        .await
261        .map_err(|_| Error::Timeout { duration: self.timeout.unwrap_or(common::DEFAULT_TIMEOUT) })?
262        .map_err(|e| Error::ConnectionFailed { addr: addr.clone(), source: e })?;
263
264        // Build HTTP request
265        let path = full_url
266            .find("://")
267            .and_then(|pos| full_url[pos + 3..].find('/'))
268            .map(|pos| &full_url[full_url.find("://").unwrap() + 3 + pos..])
269            .unwrap_or("/");
270
271        let mut request = format!("{} {} HTTP/1.1\r\n", self.method, path);
272        request.push_str(&format!("Host: {}\r\n", host));
273        request.push_str("Connection: close\r\n");
274
275        // Add headers
276        for (name, value) in self.headers.iter() {
277            request.push_str(&format!("{}: {}\r\n", name, value.to_str().unwrap_or("")));
278        }
279
280        // Add AVL auth if configured
281        if let Some(auth) = &self.client.config.avl_auth {
282            request.push_str(&format!("Authorization: Bearer {}\r\n", auth));
283        }
284
285        // Add body
286        if let Some(body) = &self.body {
287            request.push_str(&format!("Content-Length: {}\r\n", body.len()));
288            request.push_str("\r\n");
289        } else {
290            request.push_str("\r\n");
291        }
292
293        // Send request
294        stream.write_all(request.as_bytes()).await?;
295        if let Some(body) = &self.body {
296            stream.write_all(body).await?;
297        }
298
299        // Read response
300        let mut response_data = Vec::new();
301        stream.read_to_end(&mut response_data).await?;
302
303        // Parse response
304        parse_response(response_data)
305    }
306}
307
308/// HTTP Response
309pub struct Response {
310    status: StatusCode,
311    headers: HeaderMap,
312    body: Bytes,
313}
314
315impl Response {
316    /// Get response status code
317    pub fn status(&self) -> StatusCode {
318        self.status
319    }
320
321    /// Get response headers
322    pub fn headers(&self) -> &HeaderMap {
323        &self.headers
324    }
325
326    /// Get response body as bytes
327    pub fn bytes(&self) -> &Bytes {
328        &self.body
329    }
330
331    /// Get response body as text
332    pub async fn text(self) -> Result<String> {
333        String::from_utf8(self.body.to_vec())
334            .map_err(|e| Error::Internal { message: e.to_string() })
335    }
336
337    /// Parse response body as JSON
338    pub async fn json<T: serde::de::DeserializeOwned>(self) -> Result<T> {
339        serde_json::from_slice(&self.body)
340            .map_err(|e| Error::JsonError { source: e.to_string() })
341    }
342
343    /// Check if response status is success (2xx)
344    pub fn is_success(&self) -> bool {
345        self.status.is_success()
346    }
347}
348
349fn parse_response(data: Vec<u8>) -> Result<Response> {
350    // Find the separator between headers and body
351    let separator = b"\r\n\r\n";
352    let mut header_end = 0;
353    
354    for i in 0..data.len().saturating_sub(3) {
355        if &data[i..i + 4] == separator {
356            header_end = i + 4;
357            break;
358        }
359    }
360
361    if header_end == 0 {
362        return Err(Error::Internal {
363            message: "Invalid HTTP response: no header/body separator found".to_string(),
364        });
365    }
366
367    let header_data = &data[..header_end - 4];
368    let header_str = String::from_utf8_lossy(header_data);
369    let mut lines = header_str.lines();
370
371    // Parse status line
372    let status_line = lines.next().ok_or_else(|| Error::Internal {
373        message: "Empty response".to_string(),
374    })?;
375
376    let status_code = status_line
377        .split_whitespace()
378        .nth(1)
379        .and_then(|s| s.parse::<u16>().ok())
380        .ok_or_else(|| Error::Internal {
381            message: format!("Invalid status line: {}", status_line),
382        })?;
383
384    let status = StatusCode::from_u16(status_code)
385        .map_err(|_| Error::Internal {
386            message: format!("Invalid status code: {}", status_code),
387        })?;
388
389    // Parse headers
390    let mut headers = HeaderMap::new();
391
392    for line in lines {
393        if line.is_empty() {
394            break;
395        }
396
397        if let Some(pos) = line.find(':') {
398            let name = &line[..pos].trim();
399            let value = &line[pos + 1..].trim();
400
401            if let (Ok(name), Ok(value)) = (
402                HeaderName::from_bytes(name.as_bytes()),
403                HeaderValue::from_str(value),
404            ) {
405                headers.insert(name, value);
406            }
407        }
408    }
409
410    // Extract body
411    let body = if header_end < data.len() {
412        Bytes::copy_from_slice(&data[header_end..])
413    } else {
414        Bytes::new()
415    };
416
417    Ok(Response {
418        status,
419        headers,
420        body,
421    })
422}
423
424/// Request type (re-export for convenience)
425pub type Request = RequestBuilder;
426
427#[cfg(test)]
428mod tests {
429    use super::*;
430
431    #[test]
432    fn test_client_builder() {
433        let client = Client::builder()
434            .timeout(Duration::from_secs(10))
435            .avl_auth("test-token")
436            .region("br-saopaulo-1")
437            .compression(true)
438            .build()
439            .unwrap();
440
441        assert_eq!(client.config.timeout, Duration::from_secs(10));
442        assert_eq!(client.config.avl_auth, Some("test-token".to_string()));
443        assert_eq!(client.config.region, Some("br-saopaulo-1".to_string()));
444        assert!(client.config.compression);
445    }
446
447    #[test]
448    fn test_request_builder_methods() {
449        let client = Client::new();
450        
451        let get_req = client.get("https://example.com");
452        assert_eq!(get_req.method, Method::GET);
453
454        let post_req = client.post("https://example.com");
455        assert_eq!(post_req.method, Method::POST);
456    }
457
458    #[test]
459    fn test_request_with_query_params() {
460        let client = Client::new();
461        let req = client
462            .get("https://api.example.com/data")
463            .query("limit", "100")
464            .query("offset", "0");
465
466        assert_eq!(req.query_params.len(), 2);
467        assert_eq!(req.query_params[0], ("limit".to_string(), "100".to_string()));
468    }
469
470    #[tokio::test]
471    async fn test_parse_response() {
472        let response_data = b"HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 5\r\n\r\nHello";
473        let response = parse_response(response_data.to_vec()).unwrap();
474
475        assert_eq!(response.status(), StatusCode::OK);
476        assert!(response.is_success());
477        assert_eq!(response.body.len(), 5);
478    }
479}