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