Skip to main content

avila_http/
lib.rs

1//! Avila HTTP - Cliente HTTP nativo
2//! Substitui reqwest - 100% Avila
3
4use avila_error::{Error, ErrorKind, Result};
5use std::collections::HashMap;
6use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader};
7use tokio::net::TcpStream;
8
9pub struct Client {
10    timeout: Option<std::time::Duration>,
11    headers: HashMap<String, String>,
12}
13
14impl Client {
15    pub fn new() -> Self {
16        Self {
17            timeout: Some(std::time::Duration::from_secs(30)),
18            headers: HashMap::new(),
19        }
20    }
21
22    pub fn builder() -> ClientBuilder {
23        ClientBuilder::new()
24    }
25
26    pub async fn get(&self, url: &str) -> Result<Response> {
27        self.request(Method::Get, url).await
28    }
29
30    pub async fn post(&self, url: &str) -> Result<RequestBuilder> {
31        Ok(RequestBuilder::new(self, Method::Post, url))
32    }
33
34    pub async fn put(&self, url: &str) -> Result<RequestBuilder> {
35        Ok(RequestBuilder::new(self, Method::Put, url))
36    }
37
38    pub async fn delete(&self, url: &str) -> Result<Response> {
39        self.request(Method::Delete, url).await
40    }
41
42    async fn request(&self, method: Method, url: &str) -> Result<Response> {
43        let parsed_url = parse_url(url)?;
44        let host = parsed_url.host;
45        let port = parsed_url.port.unwrap_or(80);
46        let path = parsed_url.path;
47
48        let addr = format!("{}:{}", host, port);
49        let mut stream = TcpStream::connect(&addr)
50            .await
51            .map_err(|e| Error::network(format!("Failed to connect: {}", e)))?;
52
53        let request = format!(
54            "{} {} HTTP/1.1\r\nHost: {}\r\nConnection: close\r\n{}\r\n\r\n",
55            method.as_str(),
56            path,
57            host,
58            format_headers(&self.headers)
59        );
60
61        stream
62            .write_all(request.as_bytes())
63            .await
64            .map_err(|e| Error::io(format!("Failed to write: {}", e)))?;
65
66        let mut reader = BufReader::new(stream);
67        parse_response(&mut reader).await
68    }
69}
70
71impl Default for Client {
72    fn default() -> Self {
73        Self::new()
74    }
75}
76
77pub struct ClientBuilder {
78    timeout: Option<std::time::Duration>,
79    headers: HashMap<String, String>,
80}
81
82impl ClientBuilder {
83    pub fn new() -> Self {
84        Self {
85            timeout: Some(std::time::Duration::from_secs(30)),
86            headers: HashMap::new(),
87        }
88    }
89
90    pub fn timeout(mut self, duration: std::time::Duration) -> Self {
91        self.timeout = Some(duration);
92        self
93    }
94
95    pub fn header(mut self, key: &str, value: &str) -> Self {
96        self.headers.insert(key.to_string(), value.to_string());
97        self
98    }
99
100    pub fn build(self) -> Client {
101        Client {
102            timeout: self.timeout,
103            headers: self.headers,
104        }
105    }
106}
107
108impl Default for ClientBuilder {
109    fn default() -> Self {
110        Self::new()
111    }
112}
113
114pub struct RequestBuilder<'a> {
115    client: &'a Client,
116    method: Method,
117    url: String,
118    headers: HashMap<String, String>,
119    body: Option<Vec<u8>>,
120}
121
122impl<'a> RequestBuilder<'a> {
123    fn new(client: &'a Client, method: Method, url: &str) -> Self {
124        Self {
125            client,
126            method,
127            url: url.to_string(),
128            headers: HashMap::new(),
129            body: None,
130        }
131    }
132
133    pub fn header(mut self, key: &str, value: &str) -> Self {
134        self.headers.insert(key.to_string(), value.to_string());
135        self
136    }
137
138    pub fn body(mut self, data: Vec<u8>) -> Self {
139        self.body = Some(data);
140        self
141    }
142
143    pub fn json<T: avila_serde::Serialize>(mut self, data: &T) -> Self {
144        let json = data.to_json();
145        self.headers
146            .insert("Content-Type".to_string(), "application/json".to_string());
147        self.body = Some(json.into_bytes());
148        self
149    }
150
151    pub async fn send(self) -> Result<Response> {
152        // TODO: Implement full request with body
153        self.client.request(self.method, &self.url).await
154    }
155}
156
157#[derive(Clone, Copy)]
158pub enum Method {
159    Get,
160    Post,
161    Put,
162    Delete,
163    Patch,
164    Head,
165    Options,
166}
167
168impl Method {
169    fn as_str(&self) -> &str {
170        match self {
171            Method::Get => "GET",
172            Method::Post => "POST",
173            Method::Put => "PUT",
174            Method::Delete => "DELETE",
175            Method::Patch => "PATCH",
176            Method::Head => "HEAD",
177            Method::Options => "OPTIONS",
178        }
179    }
180}
181
182pub struct Response {
183    status: u16,
184    headers: HashMap<String, String>,
185    body: Vec<u8>,
186}
187
188impl Response {
189    pub fn status(&self) -> u16 {
190        self.status
191    }
192
193    pub fn headers(&self) -> &HashMap<String, String> {
194        &self.headers
195    }
196
197    pub fn body(&self) -> &[u8] {
198        &self.body
199    }
200
201    pub fn text(&self) -> Result<String> {
202        String::from_utf8(self.body.clone())
203            .map_err(|e| Error::parse(format!("Invalid UTF-8: {}", e)))
204    }
205
206    pub fn json<T: avila_serde::Deserialize>(&self) -> Result<T> {
207        let text = self.text()?;
208        T::from_json(&text).map_err(|e| Error::parse(format!("JSON parse error: {}", e)))
209    }
210
211    pub fn is_success(&self) -> bool {
212        self.status >= 200 && self.status < 300
213    }
214
215    pub fn is_error(&self) -> bool {
216        self.status >= 400
217    }
218}
219
220struct ParsedUrl {
221    host: String,
222    port: Option<u16>,
223    path: String,
224}
225
226fn parse_url(url: &str) -> Result<ParsedUrl> {
227    let url = url.trim();
228
229    let (url, _scheme) = if url.starts_with("http://") {
230        (&url[7..], "http")
231    } else if url.starts_with("https://") {
232        (&url[8..], "https")
233    } else {
234        (url, "http")
235    };
236
237    let (host_port, path) = if let Some(idx) = url.find('/') {
238        (&url[..idx], &url[idx..])
239    } else {
240        (url, "/")
241    };
242
243    let (host, port) = if let Some(idx) = host_port.find(':') {
244        let host = &host_port[..idx];
245        let port_str = &host_port[idx + 1..];
246        let port = port_str
247            .parse::<u16>()
248            .map_err(|_| Error::parse("Invalid port"))?;
249        (host.to_string(), Some(port))
250    } else {
251        (host_port.to_string(), None)
252    };
253
254    Ok(ParsedUrl {
255        host,
256        port,
257        path: path.to_string(),
258    })
259}
260
261fn format_headers(headers: &HashMap<String, String>) -> String {
262    headers
263        .iter()
264        .map(|(k, v)| format!("{}: {}", k, v))
265        .collect::<Vec<_>>()
266        .join("\r\n")
267}
268
269async fn parse_response<R: AsyncBufReadExt + Unpin>(reader: &mut R) -> Result<Response> {
270    let mut status_line = String::new();
271    reader
272        .read_line(&mut status_line)
273        .await
274        .map_err(|e| Error::network(format!("Failed to read status: {}", e)))?;
275
276    let parts: Vec<&str> = status_line.split_whitespace().collect();
277    let status = parts
278        .get(1)
279        .and_then(|s| s.parse::<u16>().ok())
280        .ok_or_else(|| Error::parse("Invalid status line"))?;
281
282    let mut headers = HashMap::new();
283    loop {
284        let mut line = String::new();
285        reader
286            .read_line(&mut line)
287            .await
288            .map_err(|e| Error::network(format!("Failed to read header: {}", e)))?;
289
290        let line = line.trim();
291        if line.is_empty() {
292            break;
293        }
294
295        if let Some(idx) = line.find(':') {
296            let key = line[..idx].trim().to_string();
297            let value = line[idx + 1..].trim().to_string();
298            headers.insert(key, value);
299        }
300    }
301
302    let mut body = Vec::new();
303    reader
304        .read_to_end(&mut body)
305        .await
306        .map_err(|e| Error::network(format!("Failed to read body: {}", e)))?;
307
308    Ok(Response {
309        status,
310        headers,
311        body,
312    })
313}
314
315#[cfg(test)]
316mod tests {
317    use super::*;
318
319    #[test]
320    fn test_parse_url() {
321        let url = parse_url("http://example.com/path").unwrap();
322        assert_eq!(url.host, "example.com");
323        assert_eq!(url.port, None);
324        assert_eq!(url.path, "/path");
325
326        let url = parse_url("http://example.com:8080/api").unwrap();
327        assert_eq!(url.host, "example.com");
328        assert_eq!(url.port, Some(8080));
329        assert_eq!(url.path, "/api");
330    }
331}