1use crate::{Error, Result};
2use reqwest::{Client, Method};
3use rustc_hash::FxHashMap;
4use schemars::JsonSchema;
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone)]
8pub struct HttpHandler {
9 pub endpoint: String,
10 pub method: HttpMethod,
11 pub headers: FxHashMap<String, String>,
12 pub auth: Option<AuthConfig>,
13 pub timeout_ms: Option<u64>,
14 client: Client,
15}
16
17#[derive(Debug, Clone)]
18pub enum HttpMethod {
19 Get,
20 Post,
21 Put,
22 Delete,
23 Patch,
24}
25
26#[derive(Debug, Clone)]
27pub enum AuthConfig {
28 Bearer { token: String },
29 Basic { username: String, password: String },
30 ApiKey { key: String, header: String },
31}
32
33#[derive(Debug, Deserialize, JsonSchema)]
34pub struct HttpInput {
35 #[serde(default)]
36 pub body: Option<serde_json::Value>,
37 #[serde(default)]
38 pub query: FxHashMap<String, String>,
39}
40
41#[derive(Debug, Serialize, JsonSchema)]
42pub struct HttpOutput {
43 pub status: u16,
44 pub body: serde_json::Value,
45 pub headers: FxHashMap<String, String>,
46}
47
48impl HttpHandler {
49 pub fn new(
50 endpoint: String,
51 method: HttpMethod,
52 headers: FxHashMap<String, String>,
53 auth: Option<AuthConfig>,
54 timeout_ms: Option<u64>,
55 ) -> Self {
56 let client = if let Some(timeout) = timeout_ms {
58 Client::builder()
59 .timeout(std::time::Duration::from_millis(timeout))
60 .build()
61 .unwrap_or_else(|_| Client::new())
62 } else {
63 Client::new()
64 };
65
66 Self {
67 endpoint,
68 method,
69 headers,
70 auth,
71 timeout_ms,
72 client,
73 }
74 }
75
76 pub async fn execute(&self, input: HttpInput) -> Result<HttpOutput> {
77 let method = match self.method {
78 HttpMethod::Get => Method::GET,
79 HttpMethod::Post => Method::POST,
80 HttpMethod::Put => Method::PUT,
81 HttpMethod::Delete => Method::DELETE,
82 HttpMethod::Patch => Method::PATCH,
83 };
84
85 let mut request = self.client.request(method, &self.endpoint);
86
87 for (k, v) in &self.headers {
89 request = request.header(k, v);
90 }
91
92 if let Some(auth) = &self.auth {
94 request = match auth {
95 AuthConfig::Bearer { token } => request.bearer_auth(token),
96 AuthConfig::Basic { username, password } => {
97 request.basic_auth(username, Some(password))
98 }
99 AuthConfig::ApiKey { key, header } => request.header(header, key),
100 };
101 }
102
103 if !input.query.is_empty() {
105 request = request.query(&input.query);
106 }
107
108 if let Some(body) = input.body {
110 request = request.json(&body);
111 }
112
113 let response = request
115 .send()
116 .await
117 .map_err(|e| Error::Http(format!("Request failed: {}", e)))?;
118
119 let status = response.status().as_u16();
120
121 let mut headers = FxHashMap::default();
123 for (k, v) in response.headers() {
124 if let Ok(v_str) = v.to_str() {
125 headers.insert(k.to_string(), v_str.to_string());
126 }
127 }
128
129 let body = response
131 .json::<serde_json::Value>()
132 .await
133 .unwrap_or(serde_json::json!({}));
134
135 Ok(HttpOutput {
136 status,
137 body,
138 headers,
139 })
140 }
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146
147 #[test]
148 fn test_http_handler_new() {
149 let handler = HttpHandler::new(
150 "https://api.example.com".to_string(),
151 HttpMethod::Get,
152 FxHashMap::default(),
153 None,
154 None,
155 );
156
157 assert_eq!(handler.endpoint, "https://api.example.com");
158 assert!(handler.headers.is_empty());
159 assert!(handler.auth.is_none());
160 assert!(handler.timeout_ms.is_none());
161 }
162
163 #[test]
164 fn test_http_handler_new_with_auth() {
165 let mut headers = FxHashMap::default();
166 headers.insert("Content-Type".to_string(), "application/json".to_string());
167
168 let auth = Some(AuthConfig::Bearer {
169 token: "test_token".to_string(),
170 });
171
172 let handler = HttpHandler::new(
173 "https://api.example.com".to_string(),
174 HttpMethod::Post,
175 headers.clone(),
176 auth,
177 Some(30000), );
179
180 assert_eq!(handler.endpoint, "https://api.example.com");
181 assert_eq!(handler.headers.len(), 1);
182 assert!(handler.auth.is_some());
183 assert_eq!(handler.timeout_ms, Some(30000));
184 }
185
186 #[test]
187 fn test_http_input_with_body() {
188 let json = r#"{"body": {"key": "value"}, "query": {}}"#;
189 let input: HttpInput = serde_json::from_str(json).unwrap();
190
191 assert!(input.body.is_some());
192 assert_eq!(input.body.unwrap()["key"], "value");
193 }
194
195 #[test]
196 fn test_http_input_with_query() {
197 let json = r#"{"body": null, "query": {"param": "value"}}"#;
198 let input: HttpInput = serde_json::from_str(json).unwrap();
199
200 assert!(input.body.is_none());
201 assert_eq!(input.query.get("param"), Some(&"value".to_string()));
202 }
203
204 #[test]
205 fn test_http_output_serialization() {
206 let mut headers = FxHashMap::default();
207 headers.insert("content-type".to_string(), "application/json".to_string());
208
209 let output = HttpOutput {
210 status: 200,
211 body: serde_json::json!({"result": "success"}),
212 headers,
213 };
214
215 let json = serde_json::to_string(&output).unwrap();
216 assert!(json.contains("\"status\":200"));
217 assert!(json.contains("\"result\":\"success\""));
218 }
219
220 #[tokio::test]
221 async fn test_execute_get_request() {
222 let mut server = mockito::Server::new_async().await;
223 let mock = server
224 .mock("GET", "/test")
225 .with_status(200)
226 .with_header("content-type", "application/json")
227 .with_body(r#"{"message": "success"}"#)
228 .create_async()
229 .await;
230
231 let handler = HttpHandler::new(
232 format!("{}/test", server.url()),
233 HttpMethod::Get,
234 FxHashMap::default(),
235 None,
236 None,
237 );
238
239 let input = HttpInput {
240 body: None,
241 query: FxHashMap::default(),
242 };
243
244 let output = handler.execute(input).await.unwrap();
245
246 assert_eq!(output.status, 200);
247 assert_eq!(output.body["message"], "success");
248 mock.assert_async().await;
249 }
250
251 #[tokio::test]
252 async fn test_execute_post_request_with_body() {
253 let mut server = mockito::Server::new_async().await;
254 let mock = server
255 .mock("POST", "/api/data")
256 .match_header("content-type", "application/json")
257 .match_body(mockito::Matcher::JsonString(
258 r#"{"key":"value"}"#.to_string(),
259 ))
260 .with_status(201)
261 .with_body(r#"{"id": "123"}"#)
262 .create_async()
263 .await;
264
265 let handler = HttpHandler::new(
266 format!("{}/api/data", server.url()),
267 HttpMethod::Post,
268 FxHashMap::default(),
269 None,
270 None,
271 );
272
273 let input = HttpInput {
274 body: Some(serde_json::json!({"key": "value"})),
275 query: FxHashMap::default(),
276 };
277
278 let output = handler.execute(input).await.unwrap();
279
280 assert_eq!(output.status, 201);
281 assert_eq!(output.body["id"], "123");
282 mock.assert_async().await;
283 }
284
285 #[tokio::test]
286 async fn test_execute_with_query_params() {
287 let mut server = mockito::Server::new_async().await;
288 let mock = server
289 .mock("GET", "/search")
290 .match_query(mockito::Matcher::AllOf(vec![
291 mockito::Matcher::UrlEncoded("q".to_string(), "rust".to_string()),
292 mockito::Matcher::UrlEncoded("limit".to_string(), "10".to_string()),
293 ]))
294 .with_status(200)
295 .with_body(r#"{"results": []}"#)
296 .create_async()
297 .await;
298
299 let handler = HttpHandler::new(
300 format!("{}/search", server.url()),
301 HttpMethod::Get,
302 FxHashMap::default(),
303 None,
304 None,
305 );
306
307 let mut query = FxHashMap::default();
308 query.insert("q".to_string(), "rust".to_string());
309 query.insert("limit".to_string(), "10".to_string());
310
311 let input = HttpInput { body: None, query };
312
313 let output = handler.execute(input).await.unwrap();
314
315 assert_eq!(output.status, 200);
316 mock.assert_async().await;
317 }
318
319 #[tokio::test]
320 async fn test_execute_with_bearer_auth() {
321 let mut server = mockito::Server::new_async().await;
322 let mock = server
323 .mock("GET", "/protected")
324 .match_header("authorization", "Bearer secret_token")
325 .with_status(200)
326 .with_body(r#"{"authorized": true}"#)
327 .create_async()
328 .await;
329
330 let handler = HttpHandler::new(
331 format!("{}/protected", server.url()),
332 HttpMethod::Get,
333 FxHashMap::default(),
334 Some(AuthConfig::Bearer {
335 token: "secret_token".to_string(),
336 }),
337 None,
338 );
339
340 let input = HttpInput {
341 body: None,
342 query: FxHashMap::default(),
343 };
344
345 let output = handler.execute(input).await.unwrap();
346
347 assert_eq!(output.status, 200);
348 assert_eq!(output.body["authorized"], true);
349 mock.assert_async().await;
350 }
351
352 #[tokio::test]
353 async fn test_execute_with_basic_auth() {
354 let mut server = mockito::Server::new_async().await;
355 let mock = server
356 .mock("GET", "/admin")
357 .match_header("authorization", "Basic dXNlcjpwYXNz")
358 .with_status(200)
359 .with_body(r#"{"admin": true}"#)
360 .create_async()
361 .await;
362
363 let handler = HttpHandler::new(
364 format!("{}/admin", server.url()),
365 HttpMethod::Get,
366 FxHashMap::default(),
367 Some(AuthConfig::Basic {
368 username: "user".to_string(),
369 password: "pass".to_string(),
370 }),
371 None,
372 );
373
374 let input = HttpInput {
375 body: None,
376 query: FxHashMap::default(),
377 };
378
379 let output = handler.execute(input).await.unwrap();
380
381 assert_eq!(output.status, 200);
382 assert_eq!(output.body["admin"], true);
383 mock.assert_async().await;
384 }
385
386 #[tokio::test]
387 async fn test_execute_with_api_key() {
388 let mut server = mockito::Server::new_async().await;
389 let mock = server
390 .mock("GET", "/api")
391 .match_header("x-api-key", "my_api_key")
392 .with_status(200)
393 .with_body(r#"{"valid": true}"#)
394 .create_async()
395 .await;
396
397 let handler = HttpHandler::new(
398 format!("{}/api", server.url()),
399 HttpMethod::Get,
400 FxHashMap::default(),
401 Some(AuthConfig::ApiKey {
402 key: "my_api_key".to_string(),
403 header: "x-api-key".to_string(),
404 }),
405 None,
406 );
407
408 let input = HttpInput {
409 body: None,
410 query: FxHashMap::default(),
411 };
412
413 let output = handler.execute(input).await.unwrap();
414
415 assert_eq!(output.status, 200);
416 assert_eq!(output.body["valid"], true);
417 mock.assert_async().await;
418 }
419
420 #[tokio::test]
421 async fn test_execute_with_custom_headers() {
422 let mut server = mockito::Server::new_async().await;
423 let mock = server
424 .mock("GET", "/headers")
425 .match_header("x-custom", "custom_value")
426 .match_header("x-request-id", "123")
427 .with_status(200)
428 .with_body(r#"{"ok": true}"#)
429 .create_async()
430 .await;
431
432 let mut headers = FxHashMap::default();
433 headers.insert("x-custom".to_string(), "custom_value".to_string());
434 headers.insert("x-request-id".to_string(), "123".to_string());
435
436 let handler = HttpHandler::new(
437 format!("{}/headers", server.url()),
438 HttpMethod::Get,
439 headers,
440 None,
441 None,
442 );
443
444 let input = HttpInput {
445 body: None,
446 query: FxHashMap::default(),
447 };
448
449 let output = handler.execute(input).await.unwrap();
450
451 assert_eq!(output.status, 200);
452 mock.assert_async().await;
453 }
454
455 #[tokio::test]
456 async fn test_execute_put_request() {
457 let mut server = mockito::Server::new_async().await;
458 let mock = server
459 .mock("PUT", "/update")
460 .with_status(200)
461 .with_body(r#"{"updated": true}"#)
462 .create_async()
463 .await;
464
465 let handler = HttpHandler::new(
466 format!("{}/update", server.url()),
467 HttpMethod::Put,
468 FxHashMap::default(),
469 None,
470 None,
471 );
472
473 let input = HttpInput {
474 body: Some(serde_json::json!({"data": "new_value"})),
475 query: FxHashMap::default(),
476 };
477
478 let output = handler.execute(input).await.unwrap();
479
480 assert_eq!(output.status, 200);
481 assert_eq!(output.body["updated"], true);
482 mock.assert_async().await;
483 }
484
485 #[tokio::test]
486 async fn test_execute_delete_request() {
487 let mut server = mockito::Server::new_async().await;
488 let mock = server
489 .mock("DELETE", "/resource/123")
490 .with_status(204)
491 .with_body("")
492 .create_async()
493 .await;
494
495 let handler = HttpHandler::new(
496 format!("{}/resource/123", server.url()),
497 HttpMethod::Delete,
498 FxHashMap::default(),
499 None,
500 None,
501 );
502
503 let input = HttpInput {
504 body: None,
505 query: FxHashMap::default(),
506 };
507
508 let output = handler.execute(input).await.unwrap();
509
510 assert_eq!(output.status, 204);
511 mock.assert_async().await;
512 }
513
514 #[tokio::test]
515 async fn test_execute_patch_request() {
516 let mut server = mockito::Server::new_async().await;
517 let mock = server
518 .mock("PATCH", "/partial")
519 .with_status(200)
520 .with_body(r#"{"patched": true}"#)
521 .create_async()
522 .await;
523
524 let handler = HttpHandler::new(
525 format!("{}/partial", server.url()),
526 HttpMethod::Patch,
527 FxHashMap::default(),
528 None,
529 None,
530 );
531
532 let input = HttpInput {
533 body: Some(serde_json::json!({"field": "value"})),
534 query: FxHashMap::default(),
535 };
536
537 let output = handler.execute(input).await.unwrap();
538
539 assert_eq!(output.status, 200);
540 assert_eq!(output.body["patched"], true);
541 mock.assert_async().await;
542 }
543
544 #[tokio::test]
545 async fn test_execute_error_handling() {
546 let handler = HttpHandler::new(
547 "http://localhost:1/nonexistent".to_string(),
548 HttpMethod::Get,
549 FxHashMap::default(),
550 None,
551 None,
552 );
553
554 let input = HttpInput {
555 body: None,
556 query: FxHashMap::default(),
557 };
558
559 let result = handler.execute(input).await;
560 assert!(result.is_err());
561 }
562}