1use std::collections::HashMap;
11use std::rc::Rc;
12
13use aver_rt::{AverList, Header, HttpResponse};
14
15use crate::nan_value::{Arena, NanValue};
16use crate::value::{RuntimeError, Value, list_from_vec, list_view};
17
18pub fn register(global: &mut HashMap<String, Value>) {
19 let mut members = HashMap::new();
20 for method in &["get", "head", "delete", "post", "put", "patch"] {
21 members.insert(
22 method.to_string(),
23 Value::Builtin(format!("Http.{}", method)),
24 );
25 }
26 global.insert(
27 "Http".to_string(),
28 Value::Namespace {
29 name: "Http".to_string(),
30 members,
31 },
32 );
33}
34
35pub fn effects(name: &str) -> &'static [&'static str] {
36 match name {
37 "Http.get" => &["Http.get"],
38 "Http.head" => &["Http.head"],
39 "Http.delete" => &["Http.delete"],
40 "Http.post" => &["Http.post"],
41 "Http.put" => &["Http.put"],
42 "Http.patch" => &["Http.patch"],
43 _ => &[],
44 }
45}
46
47pub fn call(name: &str, args: &[Value]) -> Option<Result<Value, RuntimeError>> {
49 match name {
50 "Http.get" | "Http.head" | "Http.delete" => Some(call_simple(name, args)),
51 "Http.post" | "Http.put" | "Http.patch" => Some(call_with_body(name, args)),
52 _ => None,
53 }
54}
55
56fn call_simple(name: &str, args: &[Value]) -> Result<Value, RuntimeError> {
57 if args.len() != 1 {
58 return Err(RuntimeError::Error(format!(
59 "Http.{}() takes 1 argument (url), got {}",
60 name.trim_start_matches("Http."),
61 args.len()
62 )));
63 }
64 let url = str_arg(&args[0], "Http: url must be a String")?;
65 let result = match name {
66 "Http.get" => aver_rt::http::get(&url),
67 "Http.head" => aver_rt::http::head(&url),
68 "Http.delete" => aver_rt::http::delete(&url),
69 _ => unreachable!(),
70 };
71 response_value(result)
72}
73
74fn call_with_body(name: &str, args: &[Value]) -> Result<Value, RuntimeError> {
75 if args.len() != 4 {
76 return Err(RuntimeError::Error(format!(
77 "Http.{}() takes 4 arguments (url, body, contentType, headers), got {}",
78 name.trim_start_matches("Http."),
79 args.len()
80 )));
81 }
82 let url = str_arg(&args[0], "Http: url must be a String")?;
83 let body = str_arg(&args[1], "Http: body must be a String")?;
84 let content_type = str_arg(&args[2], "Http: contentType must be a String")?;
85 let extra_headers = parse_request_headers(&args[3])?;
86
87 let result = match name {
88 "Http.post" => aver_rt::http::post(&url, &body, &content_type, &extra_headers),
89 "Http.put" => aver_rt::http::put(&url, &body, &content_type, &extra_headers),
90 "Http.patch" => aver_rt::http::patch(&url, &body, &content_type, &extra_headers),
91 _ => unreachable!(),
92 };
93 response_value(result)
94}
95
96fn str_arg(val: &Value, msg: &str) -> Result<String, RuntimeError> {
97 match val {
98 Value::Str(s) => Ok(s.clone()),
99 _ => Err(RuntimeError::Error(msg.to_string())),
100 }
101}
102
103fn parse_request_headers(val: &Value) -> Result<AverList<Header>, RuntimeError> {
104 let items = list_view(val)
105 .ok_or_else(|| RuntimeError::Error("Http: headers must be a List".to_string()))?;
106 let mut out = Vec::new();
107 for item in items.iter() {
108 let fields = match item {
109 Value::Record { fields, .. } => fields,
110 _ => {
111 return Err(RuntimeError::Error(
112 "Http: each header must be a record with 'name' and 'value' String fields"
113 .to_string(),
114 ));
115 }
116 };
117 let get = |key: &str| -> Result<String, RuntimeError> {
118 fields
119 .iter()
120 .find(|(k, _)| k == key)
121 .and_then(|(_, v)| {
122 if let Value::Str(s) = v {
123 Some(s.clone())
124 } else {
125 None
126 }
127 })
128 .ok_or_else(|| {
129 RuntimeError::Error(format!(
130 "Http: header record must have a '{}' String field",
131 key
132 ))
133 })
134 };
135 out.push(Header::from_strings(get("name")?, get("value")?));
136 }
137 Ok(AverList::from_vec(out))
138}
139
140fn response_value(result: Result<HttpResponse, String>) -> Result<Value, RuntimeError> {
141 match result {
142 Ok(resp) => Ok(Value::Ok(Box::new(http_response_to_value(resp)))),
143 Err(e) => Ok(Value::Err(Box::new(Value::Str(e)))),
144 }
145}
146
147pub fn register_nv(global: &mut HashMap<String, NanValue>, arena: &mut Arena) {
148 let methods = &["get", "head", "delete", "post", "put", "patch"];
149 let mut members: Vec<(Rc<str>, NanValue)> = Vec::with_capacity(methods.len());
150 for method in methods {
151 let idx = arena.push_builtin(&format!("Http.{}", method));
152 members.push((Rc::from(*method), NanValue::new_builtin(idx)));
153 }
154 let ns_idx = arena.push(crate::nan_value::ArenaEntry::Namespace {
155 name: Rc::from("Http"),
156 members,
157 });
158 global.insert("Http".to_string(), NanValue::new_namespace(ns_idx));
159}
160
161pub fn call_nv(
163 name: &str,
164 args: &[NanValue],
165 arena: &mut Arena,
166) -> Option<Result<NanValue, RuntimeError>> {
167 if !matches!(
169 name,
170 "Http.get" | "Http.head" | "Http.delete" | "Http.post" | "Http.put" | "Http.patch"
171 ) {
172 return None;
173 }
174 let old_args: Vec<Value> = args.iter().map(|nv| nv.to_value(arena)).collect();
175 let result = call(name, &old_args)?;
176 Some(result.map(|v| NanValue::from_value(&v, arena)))
177}
178
179fn http_response_to_value(resp: HttpResponse) -> Value {
180 let headers = resp
181 .headers
182 .into_iter()
183 .map(|header| Value::Record {
184 type_name: "Header".to_string(),
185 fields: vec![
186 ("name".to_string(), Value::Str(header.name.to_string())),
187 ("value".to_string(), Value::Str(header.value.to_string())),
188 ]
189 .into(),
190 })
191 .collect();
192
193 Value::Record {
194 type_name: "HttpResponse".to_string(),
195 fields: vec![
196 ("status".to_string(), Value::Int(resp.status)),
197 ("body".to_string(), Value::Str(resp.body.to_string())),
198 ("headers".to_string(), list_from_vec(headers)),
199 ]
200 .into(),
201 }
202}