1use crate::module_exports::{ModuleExports, ModuleFunction, ModuleParam};
9use shape_value::ValueWord;
10use std::sync::Arc;
11
12fn build_response(status: u16, headers: Vec<(String, String)>, body: String) -> ValueWord {
15 let mut h_keys = Vec::with_capacity(headers.len());
17 let mut h_values = Vec::with_capacity(headers.len());
18 for (hk, hv) in headers.iter() {
19 h_keys.push(ValueWord::from_string(Arc::new(hk.clone())));
20 h_values.push(ValueWord::from_string(Arc::new(hv.clone())));
21 }
22
23 let keys = vec![
24 ValueWord::from_string(Arc::new("status".to_string())),
25 ValueWord::from_string(Arc::new("headers".to_string())),
26 ValueWord::from_string(Arc::new("body".to_string())),
27 ValueWord::from_string(Arc::new("ok".to_string())),
28 ];
29 let values = vec![
30 ValueWord::from_f64(status as f64),
31 ValueWord::from_hashmap_pairs(h_keys, h_values),
32 ValueWord::from_string(Arc::new(body)),
33 ValueWord::from_bool((200..300).contains(&status)),
34 ];
35 ValueWord::from_hashmap_pairs(keys, values)
36}
37
38fn extract_headers(options: &ValueWord) -> Vec<(String, String)> {
40 if let Some((keys, values, _)) = options.as_hashmap() {
41 for (i, k) in keys.iter().enumerate() {
43 if k.as_str() == Some("headers") {
44 if let Some((hk, hv, _)) = values[i].as_hashmap() {
45 return hk
46 .iter()
47 .zip(hv.iter())
48 .filter_map(|(k, v)| {
49 Some((k.as_str()?.to_string(), v.as_str()?.to_string()))
50 })
51 .collect();
52 }
53 }
54 }
55 }
56 Vec::new()
57}
58
59fn extract_timeout(options: &ValueWord) -> Option<std::time::Duration> {
61 if let Some((keys, values, _)) = options.as_hashmap() {
62 for (i, k) in keys.iter().enumerate() {
63 if k.as_str() == Some("timeout") {
64 if let Some(ms) = values[i].as_number_coerce() {
65 if ms > 0.0 {
66 return Some(std::time::Duration::from_millis(ms as u64));
67 }
68 }
69 }
70 }
71 }
72 None
73}
74
75pub fn create_http_module() -> ModuleExports {
77 let mut module = ModuleExports::new("std::core::http");
78 module.description = "HTTP client for making web requests".to_string();
79
80 let url_param = ModuleParam {
81 name: "url".to_string(),
82 type_name: "string".to_string(),
83 required: true,
84 description: "URL to request".to_string(),
85 ..Default::default()
86 };
87
88 let options_param = ModuleParam {
89 name: "options".to_string(),
90 type_name: "object".to_string(),
91 required: false,
92 description: "Request options: { headers?: HashMap, timeout?: number }".to_string(),
93 ..Default::default()
94 };
95
96 let body_param = ModuleParam {
97 name: "body".to_string(),
98 type_name: "any".to_string(),
99 required: false,
100 description: "Request body (string or value to serialize as JSON)".to_string(),
101 ..Default::default()
102 };
103
104 module.add_async_function_with_schema(
106 "get",
107 |args: Vec<ValueWord>| async move {
108 let url = args
109 .first()
110 .and_then(|a| a.as_str())
111 .map(|s| s.to_string())
112 .ok_or_else(|| "http.get() requires a URL string".to_string())?;
113
114 let mut builder = reqwest::Client::new().get(&url);
115
116 if let Some(options) = args.get(1) {
117 for (k, v) in extract_headers(options) {
118 builder = builder.header(&k, &v);
119 }
120 if let Some(timeout) = extract_timeout(options) {
121 builder = builder.timeout(timeout);
122 }
123 }
124
125 let resp = builder
126 .send()
127 .await
128 .map_err(|e| format!("http.get() failed: {}", e))?;
129
130 let status = resp.status().as_u16();
131 let headers: Vec<(String, String)> = resp
132 .headers()
133 .iter()
134 .map(|(k, v)| (k.as_str().to_string(), v.to_str().unwrap_or("").to_string()))
135 .collect();
136 let body = resp
137 .text()
138 .await
139 .map_err(|e| format!("http.get() body read failed: {}", e))?;
140
141 Ok(ValueWord::from_ok(build_response(status, headers, body)))
142 },
143 ModuleFunction {
144 description: "Perform an HTTP GET request".to_string(),
145 params: vec![url_param.clone(), options_param.clone()],
146 return_type: Some("Result<HttpResponse>".to_string()),
147 },
148 );
149
150 module.add_async_function_with_schema(
152 "post",
153 |args: Vec<ValueWord>| async move {
154 let url = args
155 .first()
156 .and_then(|a| a.as_str())
157 .map(|s| s.to_string())
158 .ok_or_else(|| "http.post() requires a URL string".to_string())?;
159
160 let mut builder = reqwest::Client::new().post(&url);
161
162 if let Some(body_arg) = args.get(1) {
164 if !body_arg.is_none() && !body_arg.is_unit() {
165 if let Some(s) = body_arg.as_str() {
166 builder = builder.body(s.to_string());
167 } else {
168 let json = body_arg.to_json_value();
169 builder = builder
170 .header("Content-Type", "application/json")
171 .body(serde_json::to_string(&json).unwrap_or_default());
172 }
173 }
174 }
175
176 if let Some(options) = args.get(2) {
178 for (k, v) in extract_headers(options) {
179 builder = builder.header(&k, &v);
180 }
181 if let Some(timeout) = extract_timeout(options) {
182 builder = builder.timeout(timeout);
183 }
184 }
185
186 let resp = builder
187 .send()
188 .await
189 .map_err(|e| format!("http.post() failed: {}", e))?;
190
191 let status = resp.status().as_u16();
192 let headers: Vec<(String, String)> = resp
193 .headers()
194 .iter()
195 .map(|(k, v)| (k.as_str().to_string(), v.to_str().unwrap_or("").to_string()))
196 .collect();
197 let body = resp
198 .text()
199 .await
200 .map_err(|e| format!("http.post() body read failed: {}", e))?;
201
202 Ok(ValueWord::from_ok(build_response(status, headers, body)))
203 },
204 ModuleFunction {
205 description: "Perform an HTTP POST request".to_string(),
206 params: vec![url_param.clone(), body_param.clone(), options_param.clone()],
207 return_type: Some("Result<HttpResponse>".to_string()),
208 },
209 );
210
211 module.add_async_function_with_schema(
213 "put",
214 |args: Vec<ValueWord>| async move {
215 let url = args
216 .first()
217 .and_then(|a| a.as_str())
218 .map(|s| s.to_string())
219 .ok_or_else(|| "http.put() requires a URL string".to_string())?;
220
221 let mut builder = reqwest::Client::new().put(&url);
222
223 if let Some(body_arg) = args.get(1) {
224 if !body_arg.is_none() && !body_arg.is_unit() {
225 if let Some(s) = body_arg.as_str() {
226 builder = builder.body(s.to_string());
227 } else {
228 let json = body_arg.to_json_value();
229 builder = builder
230 .header("Content-Type", "application/json")
231 .body(serde_json::to_string(&json).unwrap_or_default());
232 }
233 }
234 }
235
236 if let Some(options) = args.get(2) {
237 for (k, v) in extract_headers(options) {
238 builder = builder.header(&k, &v);
239 }
240 if let Some(timeout) = extract_timeout(options) {
241 builder = builder.timeout(timeout);
242 }
243 }
244
245 let resp = builder
246 .send()
247 .await
248 .map_err(|e| format!("http.put() failed: {}", e))?;
249
250 let status = resp.status().as_u16();
251 let headers: Vec<(String, String)> = resp
252 .headers()
253 .iter()
254 .map(|(k, v)| (k.as_str().to_string(), v.to_str().unwrap_or("").to_string()))
255 .collect();
256 let body = resp
257 .text()
258 .await
259 .map_err(|e| format!("http.put() body read failed: {}", e))?;
260
261 Ok(ValueWord::from_ok(build_response(status, headers, body)))
262 },
263 ModuleFunction {
264 description: "Perform an HTTP PUT request".to_string(),
265 params: vec![url_param.clone(), body_param, options_param.clone()],
266 return_type: Some("Result<HttpResponse>".to_string()),
267 },
268 );
269
270 module.add_async_function_with_schema(
272 "delete",
273 |args: Vec<ValueWord>| async move {
274 let url = args
275 .first()
276 .and_then(|a| a.as_str())
277 .map(|s| s.to_string())
278 .ok_or_else(|| "http.delete() requires a URL string".to_string())?;
279
280 let mut builder = reqwest::Client::new().delete(&url);
281
282 if let Some(options) = args.get(1) {
283 for (k, v) in extract_headers(options) {
284 builder = builder.header(&k, &v);
285 }
286 if let Some(timeout) = extract_timeout(options) {
287 builder = builder.timeout(timeout);
288 }
289 }
290
291 let resp = builder
292 .send()
293 .await
294 .map_err(|e| format!("http.delete() failed: {}", e))?;
295
296 let status = resp.status().as_u16();
297 let headers: Vec<(String, String)> = resp
298 .headers()
299 .iter()
300 .map(|(k, v)| (k.as_str().to_string(), v.to_str().unwrap_or("").to_string()))
301 .collect();
302 let body = resp
303 .text()
304 .await
305 .map_err(|e| format!("http.delete() body read failed: {}", e))?;
306
307 Ok(ValueWord::from_ok(build_response(status, headers, body)))
308 },
309 ModuleFunction {
310 description: "Perform an HTTP DELETE request".to_string(),
311 params: vec![url_param, options_param],
312 return_type: Some("Result<HttpResponse>".to_string()),
313 },
314 );
315
316 module
317}
318
319#[cfg(test)]
320mod tests {
321 use super::*;
322
323 #[test]
324 fn test_http_module_creation() {
325 let module = create_http_module();
326 assert_eq!(module.name, "std::core::http");
327 assert!(module.has_export("get"));
328 assert!(module.has_export("post"));
329 assert!(module.has_export("put"));
330 assert!(module.has_export("delete"));
331 }
332
333 #[test]
334 fn test_http_all_async() {
335 let module = create_http_module();
336 assert!(module.is_async("get"));
337 assert!(module.is_async("post"));
338 assert!(module.is_async("put"));
339 assert!(module.is_async("delete"));
340 }
341
342 #[test]
343 fn test_http_schemas() {
344 let module = create_http_module();
345
346 let get_schema = module.get_schema("get").unwrap();
347 assert_eq!(get_schema.params.len(), 2);
348 assert_eq!(get_schema.params[0].name, "url");
349 assert!(get_schema.params[0].required);
350 assert!(!get_schema.params[1].required);
351 assert_eq!(
352 get_schema.return_type.as_deref(),
353 Some("Result<HttpResponse>")
354 );
355
356 let post_schema = module.get_schema("post").unwrap();
357 assert_eq!(post_schema.params.len(), 3);
358 assert_eq!(post_schema.params[1].name, "body");
359
360 let delete_schema = module.get_schema("delete").unwrap();
361 assert_eq!(delete_schema.params.len(), 2);
362 }
363
364 #[test]
365 fn test_build_response() {
366 let resp = build_response(
367 200,
368 vec![("content-type".to_string(), "text/html".to_string())],
369 "hello".to_string(),
370 );
371 let (keys, values, _) = resp.as_hashmap().expect("should be hashmap");
372
373 let status_idx = keys
375 .iter()
376 .position(|k| k.as_str() == Some("status"))
377 .unwrap();
378 assert_eq!(values[status_idx].as_f64(), Some(200.0));
379
380 let ok_idx = keys.iter().position(|k| k.as_str() == Some("ok")).unwrap();
382 assert_eq!(values[ok_idx].as_bool(), Some(true));
383
384 let body_idx = keys
386 .iter()
387 .position(|k| k.as_str() == Some("body"))
388 .unwrap();
389 assert_eq!(values[body_idx].as_str(), Some("hello"));
390
391 let headers_idx = keys
393 .iter()
394 .position(|k| k.as_str() == Some("headers"))
395 .unwrap();
396 assert!(values[headers_idx].as_hashmap().is_some());
397 }
398
399 #[test]
400 fn test_build_response_error_status() {
401 let resp = build_response(404, vec![], "not found".to_string());
402 let (keys, values, _) = resp.as_hashmap().expect("should be hashmap");
403
404 let ok_idx = keys.iter().position(|k| k.as_str() == Some("ok")).unwrap();
405 assert_eq!(values[ok_idx].as_bool(), Some(false));
406 }
407
408 #[test]
409 fn test_extract_headers_from_options() {
410 let hk = vec![ValueWord::from_string(Arc::new(
411 "Authorization".to_string(),
412 ))];
413 let hv = vec![ValueWord::from_string(Arc::new(
414 "Bearer token123".to_string(),
415 ))];
416 let headers_map = ValueWord::from_hashmap_pairs(hk, hv);
417
418 let ok = vec![ValueWord::from_string(Arc::new("headers".to_string()))];
419 let ov = vec![headers_map];
420 let options = ValueWord::from_hashmap_pairs(ok, ov);
421
422 let extracted = extract_headers(&options);
423 assert_eq!(extracted.len(), 1);
424 assert_eq!(extracted[0].0, "Authorization");
425 assert_eq!(extracted[0].1, "Bearer token123");
426 }
427
428 #[test]
429 fn test_extract_timeout_from_options() {
430 let ok = vec![ValueWord::from_string(Arc::new("timeout".to_string()))];
431 let ov = vec![ValueWord::from_f64(5000.0)];
432 let options = ValueWord::from_hashmap_pairs(ok, ov);
433
434 let timeout = extract_timeout(&options);
435 assert_eq!(timeout, Some(std::time::Duration::from_millis(5000)));
436 }
437
438 #[test]
439 fn test_extract_timeout_none() {
440 let options = ValueWord::empty_hashmap();
441 assert_eq!(extract_timeout(&options), None);
442 }
443}