1use async_trait::async_trait;
7use std::collections::HashMap;
8
9#[derive(Debug, Clone)]
11pub struct HttpResponse {
12 pub status: u16,
14 pub headers: HashMap<String, String>,
16 pub body: Vec<u8>,
18}
19
20impl HttpResponse {
21 pub fn text(&self) -> Result<String, std::string::FromUtf8Error> {
23 String::from_utf8(self.body.clone())
24 }
25
26 pub fn json<T: serde::de::DeserializeOwned>(&self) -> Result<T, serde_json::Error> {
28 serde_json::from_slice(&self.body)
29 }
30
31 pub fn is_success(&self) -> bool {
33 (200..300).contains(&self.status)
34 }
35}
36
37#[cfg_attr(not(feature = "browser"), async_trait)]
42#[cfg_attr(feature = "browser", async_trait(?Send))]
43pub trait HttpClient: Send + Sync {
44 async fn request(
46 &self,
47 method: &str,
48 url: &str,
49 headers: &HashMap<String, String>,
50 body: Option<&[u8]>,
51 ) -> Result<HttpResponse, Box<dyn std::error::Error + Send + Sync>>;
52
53 async fn get(
55 &self,
56 url: &str,
57 headers: &HashMap<String, String>,
58 ) -> Result<HttpResponse, Box<dyn std::error::Error + Send + Sync>> {
59 self.request("GET", url, headers, None).await
60 }
61
62 async fn post(
64 &self,
65 url: &str,
66 headers: &HashMap<String, String>,
67 body: &[u8],
68 ) -> Result<HttpResponse, Box<dyn std::error::Error + Send + Sync>> {
69 self.request("POST", url, headers, Some(body)).await
70 }
71}
72
73#[cfg(feature = "native")]
75pub struct NativeHttpClient {
76 client: reqwest::Client,
77}
78
79#[cfg(feature = "native")]
80impl NativeHttpClient {
81 pub fn new() -> Self {
86 Self {
87 client: reqwest::Client::builder()
88 .timeout(std::time::Duration::from_secs(60))
89 .pool_idle_timeout(std::time::Duration::from_secs(30))
90 .build()
91 .expect("failed to build reqwest client"),
92 }
93 }
94}
95
96#[cfg(feature = "native")]
97impl Default for NativeHttpClient {
98 fn default() -> Self {
99 Self::new()
100 }
101}
102
103#[cfg(feature = "native")]
104#[async_trait]
105impl HttpClient for NativeHttpClient {
106 async fn request(
107 &self,
108 method: &str,
109 url: &str,
110 headers: &HashMap<String, String>,
111 body: Option<&[u8]>,
112 ) -> Result<HttpResponse, Box<dyn std::error::Error + Send + Sync>> {
113 let reqwest_method = method.parse::<reqwest::Method>()?;
114 let mut builder = self.client.request(reqwest_method, url);
115
116 for (key, value) in headers {
117 builder = builder.header(key.as_str(), value.as_str());
118 }
119
120 if let Some(body_bytes) = body {
121 builder = builder.body(body_bytes.to_vec());
122 }
123
124 let response = builder.send().await?;
125
126 let status = response.status().as_u16();
127 let mut resp_headers = HashMap::new();
128 for (key, value) in response.headers() {
129 if let Ok(v) = value.to_str() {
130 resp_headers.insert(key.as_str().to_string(), v.to_string());
131 }
132 }
133 let resp_body = response.bytes().await?.to_vec();
134
135 Ok(HttpResponse {
136 status,
137 headers: resp_headers,
138 body: resp_body,
139 })
140 }
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146
147 #[test]
148 fn test_http_response_text() {
149 let response = HttpResponse {
150 status: 200,
151 headers: HashMap::new(),
152 body: b"hello world".to_vec(),
153 };
154 assert_eq!(response.text().unwrap(), "hello world");
155 }
156
157 #[test]
158 fn test_http_response_text_invalid_utf8() {
159 let response = HttpResponse {
160 status: 200,
161 headers: HashMap::new(),
162 body: vec![0xFF, 0xFE],
163 };
164 assert!(response.text().is_err());
165 }
166
167 #[test]
168 fn test_http_response_json() {
169 let response = HttpResponse {
170 status: 200,
171 headers: HashMap::new(),
172 body: br#"{"key": "value"}"#.to_vec(),
173 };
174 let parsed: HashMap<String, String> = response.json().unwrap();
175 assert_eq!(parsed.get("key").unwrap(), "value");
176 }
177
178 #[test]
179 fn test_http_response_json_invalid() {
180 let response = HttpResponse {
181 status: 200,
182 headers: HashMap::new(),
183 body: b"not json".to_vec(),
184 };
185 let result: Result<HashMap<String, String>, _> = response.json();
186 assert!(result.is_err());
187 }
188
189 #[test]
190 fn test_http_response_is_success() {
191 for status in [200, 201, 204, 299] {
192 let response = HttpResponse {
193 status,
194 headers: HashMap::new(),
195 body: vec![],
196 };
197 assert!(response.is_success(), "status {status} should be success");
198 }
199 }
200
201 #[test]
202 fn test_http_response_is_not_success() {
203 for status in [100, 301, 400, 404, 500] {
204 let response = HttpResponse {
205 status,
206 headers: HashMap::new(),
207 body: vec![],
208 };
209 assert!(
210 !response.is_success(),
211 "status {status} should not be success"
212 );
213 }
214 }
215
216 #[test]
217 fn test_native_http_client_default() {
218 let _client = NativeHttpClient::default();
219 }
220}