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