1use 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 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}