1pub mod ghola;
25pub mod native;
26
27pub use ghola::GholaHttpClient;
28pub use native::NativeHttpClient;
29
30use async_trait::async_trait;
31use std::collections::HashMap;
32
33use crate::error::ScopeError;
34
35#[derive(Debug, Clone)]
37pub struct Request {
38 pub url: String,
39 pub method: String,
40 pub headers: HashMap<String, String>,
41 pub body: Option<String>,
42}
43
44impl Request {
45 pub fn get(url: &str) -> Self {
47 Self {
48 url: url.to_string(),
49 method: "GET".to_string(),
50 headers: HashMap::new(),
51 body: None,
52 }
53 }
54
55 pub fn post_json(url: &str, body: impl Into<String>) -> Self {
57 let mut headers = HashMap::new();
58 headers.insert("Content-Type".to_string(), "application/json".to_string());
59 Self {
60 url: url.to_string(),
61 method: "POST".to_string(),
62 headers,
63 body: Some(body.into()),
64 }
65 }
66
67 pub fn with_header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
69 self.headers.insert(key.into(), value.into());
70 self
71 }
72}
73
74#[derive(Debug, Clone)]
76pub struct Response {
77 pub status_code: u16,
78 pub headers: HashMap<String, String>,
79 pub body: String,
80}
81
82impl Response {
83 pub fn is_success(&self) -> bool {
85 (200..300).contains(&self.status_code)
86 }
87
88 pub fn json<T: serde::de::DeserializeOwned>(&self) -> serde_json::Result<T> {
90 serde_json::from_str(&self.body)
91 }
92}
93
94#[async_trait]
98pub trait HttpClient: Send + Sync {
99 async fn send(&self, request: Request) -> Result<Response, ScopeError>;
101}
102
103#[cfg(test)]
108mod tests {
109 use super::*;
110
111 #[test]
112 fn test_request_get() {
113 let req = Request::get("https://example.com");
114 assert_eq!(req.method, "GET");
115 assert_eq!(req.url, "https://example.com");
116 assert!(req.body.is_none());
117 assert!(req.headers.is_empty());
118 }
119
120 #[test]
121 fn test_request_post_json() {
122 let req = Request::post_json("https://example.com/api", r#"{"key":"value"}"#);
123 assert_eq!(req.method, "POST");
124 assert_eq!(req.body.as_deref(), Some(r#"{"key":"value"}"#));
125 assert_eq!(
126 req.headers.get("Content-Type").map(String::as_str),
127 Some("application/json")
128 );
129 }
130
131 #[test]
132 fn test_request_with_header() {
133 let req = Request::get("https://example.com")
134 .with_header("Authorization", "Bearer token123")
135 .with_header("Accept", "application/json");
136 assert_eq!(req.headers.len(), 2);
137 assert_eq!(
138 req.headers.get("Authorization").map(String::as_str),
139 Some("Bearer token123")
140 );
141 }
142
143 #[test]
144 fn test_response_is_success() {
145 assert!(
146 Response {
147 status_code: 200,
148 headers: HashMap::new(),
149 body: String::new()
150 }
151 .is_success()
152 );
153 assert!(
154 Response {
155 status_code: 299,
156 headers: HashMap::new(),
157 body: String::new()
158 }
159 .is_success()
160 );
161 assert!(
162 !Response {
163 status_code: 404,
164 headers: HashMap::new(),
165 body: String::new()
166 }
167 .is_success()
168 );
169 assert!(
170 !Response {
171 status_code: 500,
172 headers: HashMap::new(),
173 body: String::new()
174 }
175 .is_success()
176 );
177 }
178
179 #[test]
180 fn test_response_json() {
181 let resp = Response {
182 status_code: 200,
183 headers: HashMap::new(),
184 body: r#"{"name":"test","value":42}"#.to_string(),
185 };
186 let parsed: serde_json::Value = resp.json().unwrap();
187 assert_eq!(parsed["name"], "test");
188 assert_eq!(parsed["value"], 42);
189 }
190
191 #[test]
192 fn test_response_json_error() {
193 let resp = Response {
194 status_code: 200,
195 headers: HashMap::new(),
196 body: "not json".to_string(),
197 };
198 let result: serde_json::Result<serde_json::Value> = resp.json();
199 assert!(result.is_err());
200 }
201
202 #[test]
203 fn test_request_debug_formatting() {
204 let req = Request::get("https://example.com");
205 let debug = format!("{:?}", req);
206 assert!(debug.contains("GET"));
207 assert!(debug.contains("example.com"));
208 }
209
210 #[test]
211 fn test_request_clone() {
212 let req = Request::post_json("https://example.com", r#"{"a":1}"#)
213 .with_header("X-Test", "yes");
214 let cloned = req.clone();
215 assert_eq!(cloned.method, "POST");
216 assert_eq!(cloned.url, "https://example.com");
217 assert_eq!(cloned.body, Some(r#"{"a":1}"#.to_string()));
218 assert_eq!(
219 cloned.headers.get("X-Test").map(String::as_str),
220 Some("yes")
221 );
222 assert_eq!(
223 cloned.headers.get("Content-Type").map(String::as_str),
224 Some("application/json")
225 );
226 }
227
228 #[test]
229 fn test_response_debug_formatting() {
230 let resp = Response {
231 status_code: 404,
232 headers: HashMap::new(),
233 body: "not found".to_string(),
234 };
235 let debug = format!("{:?}", resp);
236 assert!(debug.contains("404"));
237 assert!(debug.contains("not found"));
238 }
239
240 #[test]
241 fn test_response_clone() {
242 let mut headers = HashMap::new();
243 headers.insert("content-type".to_string(), "text/plain".to_string());
244 let resp = Response {
245 status_code: 200,
246 headers,
247 body: "hello".to_string(),
248 };
249 let cloned = resp.clone();
250 assert_eq!(cloned.status_code, 200);
251 assert_eq!(cloned.body, "hello");
252 assert_eq!(
253 cloned.headers.get("content-type").map(String::as_str),
254 Some("text/plain")
255 );
256 }
257
258 #[test]
259 fn test_response_boundary_success() {
260 assert!(
261 !Response {
262 status_code: 199,
263 headers: HashMap::new(),
264 body: String::new()
265 }
266 .is_success()
267 );
268 assert!(
269 Response {
270 status_code: 200,
271 headers: HashMap::new(),
272 body: String::new()
273 }
274 .is_success()
275 );
276 assert!(
277 !Response {
278 status_code: 300,
279 headers: HashMap::new(),
280 body: String::new()
281 }
282 .is_success()
283 );
284 }
285
286 #[test]
287 fn test_request_header_overwrite() {
288 let req = Request::get("https://example.com")
289 .with_header("X-Key", "first")
290 .with_header("X-Key", "second");
291 assert_eq!(
292 req.headers.get("X-Key").map(String::as_str),
293 Some("second")
294 );
295 }
296
297 #[test]
298 fn test_response_json_typed() {
299 #[derive(serde::Deserialize, Debug, PartialEq)]
300 struct TestData {
301 name: String,
302 count: u32,
303 }
304
305 let resp = Response {
306 status_code: 200,
307 headers: HashMap::new(),
308 body: r#"{"name":"test","count":5}"#.to_string(),
309 };
310 let parsed: TestData = resp.json().unwrap();
311 assert_eq!(
312 parsed,
313 TestData {
314 name: "test".to_string(),
315 count: 5
316 }
317 );
318 }
319
320 #[test]
321 fn test_post_json_empty_body() {
322 let req = Request::post_json("https://example.com", "");
323 assert_eq!(req.body, Some(String::new()));
324 assert_eq!(req.method, "POST");
325 }
326}