pforge_runtime/handlers/
http.rs

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