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