1use std::collections::HashMap;
11
12use crate::value::{RuntimeError, Value, list_from_vec, list_slice};
13
14pub fn register(global: &mut HashMap<String, Value>) {
15 let mut members = HashMap::new();
16 for method in &["get", "head", "delete", "post", "put", "patch"] {
17 members.insert(
18 method.to_string(),
19 Value::Builtin(format!("Http.{}", method)),
20 );
21 }
22 global.insert(
23 "Http".to_string(),
24 Value::Namespace {
25 name: "Http".to_string(),
26 members,
27 },
28 );
29}
30
31pub fn effects(name: &str) -> &'static [&'static str] {
32 match name {
33 "Http.get" | "Http.head" | "Http.delete" | "Http.post" | "Http.put" | "Http.patch" => {
34 &["Http"]
35 }
36 _ => &[],
37 }
38}
39
40pub fn call(name: &str, args: &[Value]) -> Option<Result<Value, RuntimeError>> {
42 match name {
43 "Http.get" | "Http.head" | "Http.delete" => Some(call_simple(name, &args)),
44 "Http.post" | "Http.put" | "Http.patch" => Some(call_with_body(name, &args)),
45 _ => None,
46 }
47}
48
49fn call_simple(name: &str, args: &[Value]) -> Result<Value, RuntimeError> {
52 if args.len() != 1 {
53 return Err(RuntimeError::Error(format!(
54 "Http.{}() takes 1 argument (url), got {}",
55 name.trim_start_matches("Http."),
56 args.len()
57 )));
58 }
59 let url = str_arg(&args[0], "Http: url must be a String")?;
60 let method = name.trim_start_matches("Http.").to_uppercase();
61 let result = ureq::request(&method, &url)
62 .timeout(std::time::Duration::from_secs(10))
63 .call();
64 response_value(result)
65}
66
67fn call_with_body(name: &str, args: &[Value]) -> Result<Value, RuntimeError> {
68 if args.len() != 4 {
69 return Err(RuntimeError::Error(format!(
70 "Http.{}() takes 4 arguments (url, body, contentType, headers), got {}",
71 name.trim_start_matches("Http."),
72 args.len()
73 )));
74 }
75 let url = str_arg(&args[0], "Http: url must be a String")?;
76 let body = str_arg(&args[1], "Http: body must be a String")?;
77 let content_type = str_arg(&args[2], "Http: contentType must be a String")?;
78 let extra_headers = parse_request_headers(&args[3])?;
79
80 let method = name.trim_start_matches("Http.").to_uppercase();
81 let mut req = ureq::request(&method, &url)
82 .timeout(std::time::Duration::from_secs(10))
83 .set("Content-Type", &content_type);
84 for (k, v) in &extra_headers {
85 req = req.set(k, v);
86 }
87 response_value(req.send_string(&body))
88}
89
90fn str_arg(val: &Value, msg: &str) -> Result<String, RuntimeError> {
91 match val {
92 Value::Str(s) => Ok(s.clone()),
93 _ => Err(RuntimeError::Error(msg.to_string())),
94 }
95}
96
97fn parse_request_headers(val: &Value) -> Result<Vec<(String, String)>, RuntimeError> {
98 let items = list_slice(val)
99 .ok_or_else(|| RuntimeError::Error("Http: headers must be a List".to_string()))?;
100 let mut out = Vec::new();
101 for item in items {
102 let fields = match item {
103 Value::Record { fields, .. } => fields,
104 _ => {
105 return Err(RuntimeError::Error(
106 "Http: each header must be a record with 'name' and 'value' String fields"
107 .to_string(),
108 ));
109 }
110 };
111 let get = |key: &str| -> Result<String, RuntimeError> {
112 fields
113 .iter()
114 .find(|(k, _)| k == key)
115 .and_then(|(_, v)| {
116 if let Value::Str(s) = v {
117 Some(s.clone())
118 } else {
119 None
120 }
121 })
122 .ok_or_else(|| {
123 RuntimeError::Error(format!(
124 "Http: header record must have a '{}' String field",
125 key
126 ))
127 })
128 };
129 out.push((get("name")?, get("value")?));
130 }
131 Ok(out)
132}
133
134fn response_value(result: Result<ureq::Response, ureq::Error>) -> Result<Value, RuntimeError> {
135 match result {
136 Ok(resp) => build_response(resp),
137 Err(ureq::Error::Status(_, resp)) => build_response(resp),
138 Err(ureq::Error::Transport(e)) => Ok(Value::Err(Box::new(Value::Str(e.to_string())))),
139 }
140}
141
142fn build_response(resp: ureq::Response) -> Result<Value, RuntimeError> {
143 use std::io::Read;
144 let status = resp.status() as i64;
145 let header_names = resp.headers_names();
146 let headers: Vec<Value> = header_names
147 .iter()
148 .map(|name| {
149 let value = resp.header(name).unwrap_or("").to_string();
150 Value::Record {
151 type_name: "Header".to_string(),
152 fields: vec![
153 ("name".to_string(), Value::Str(name.clone())),
154 ("value".to_string(), Value::Str(value)),
155 ],
156 }
157 })
158 .collect();
159
160 const BODY_LIMIT: u64 = 10 * 1024 * 1024; let mut buf = Vec::new();
162 let bytes_read = resp
163 .into_reader()
164 .take(BODY_LIMIT + 1)
165 .read_to_end(&mut buf)
166 .map_err(|e| RuntimeError::Error(format!("Http: failed to read response body: {}", e)))?;
167 if bytes_read as u64 > BODY_LIMIT {
168 return Ok(Value::Err(Box::new(Value::Str(
169 "Http: response body exceeds 10 MB limit".to_string(),
170 ))));
171 }
172 let body = String::from_utf8_lossy(&buf).into_owned();
173 Ok(Value::Ok(Box::new(Value::Record {
174 type_name: "HttpResponse".to_string(),
175 fields: vec![
176 ("status".to_string(), Value::Int(status)),
177 ("body".to_string(), Value::Str(body)),
178 ("headers".to_string(), list_from_vec(headers)),
179 ],
180 })))
181}