Skip to main content

aver/services/
http.rs

1/// Http service — HTTP client built on `ureq`.
2///
3/// Exposes six methods mirroring the HTTP verb set:
4///   GET / HEAD / DELETE  — `Http.get(url)`, `Http.head(url)`, `Http.delete(url)`
5///   POST / PUT / PATCH   — `Http.post(url, body, contentType, headers)`, etc.
6///
7/// Each method requires its own exact effect (`Http.get`, `Http.post`, etc.). Responses are wrapped in `Ok(HttpResponse)`
8/// for any completed HTTP exchange (including 4xx/5xx). Transport failures return
9/// `Err(String)`. Response bodies are capped at 10 MB.
10use 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
47/// Returns `Some(result)` when `name` is owned by this service, `None` otherwise.
48pub 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 {
136            name: get("name")?,
137            value: get("value")?,
138        });
139    }
140    Ok(AverList::from_vec(out))
141}
142
143fn response_value(result: Result<HttpResponse, String>) -> Result<Value, RuntimeError> {
144    match result {
145        Ok(resp) => Ok(Value::Ok(Box::new(http_response_to_value(resp)))),
146        Err(e) => Ok(Value::Err(Box::new(Value::Str(e)))),
147    }
148}
149
150pub fn register_nv(global: &mut HashMap<String, NanValue>, arena: &mut Arena) {
151    let methods = &["get", "head", "delete", "post", "put", "patch"];
152    let mut members: Vec<(Rc<str>, NanValue)> = Vec::with_capacity(methods.len());
153    for method in methods {
154        let idx = arena.push_builtin(&format!("Http.{}", method));
155        members.push((Rc::from(*method), NanValue::new_builtin(idx)));
156    }
157    let ns_idx = arena.push(crate::nan_value::ArenaEntry::Namespace {
158        name: Rc::from("Http"),
159        members,
160    });
161    global.insert("Http".to_string(), NanValue::new_namespace(ns_idx));
162}
163
164/// Bridge: convert NanValue args to Value, call old implementation, convert result back.
165pub fn call_nv(
166    name: &str,
167    args: &[NanValue],
168    arena: &mut Arena,
169) -> Option<Result<NanValue, RuntimeError>> {
170    // Check if this name is owned by us
171    if !matches!(
172        name,
173        "Http.get" | "Http.head" | "Http.delete" | "Http.post" | "Http.put" | "Http.patch"
174    ) {
175        return None;
176    }
177    let old_args: Vec<Value> = args.iter().map(|nv| nv.to_value(arena)).collect();
178    let result = call(name, &old_args)?;
179    Some(result.map(|v| NanValue::from_value(&v, arena)))
180}
181
182fn http_response_to_value(resp: HttpResponse) -> Value {
183    let headers = resp
184        .headers
185        .into_iter()
186        .map(|header| Value::Record {
187            type_name: "Header".to_string(),
188            fields: vec![
189                ("name".to_string(), Value::Str(header.name)),
190                ("value".to_string(), Value::Str(header.value)),
191            ]
192            .into(),
193        })
194        .collect();
195
196    Value::Record {
197        type_name: "HttpResponse".to_string(),
198        fields: vec![
199            ("status".to_string(), Value::Int(resp.status)),
200            ("body".to_string(), Value::Str(resp.body)),
201            ("headers".to_string(), list_from_vec(headers)),
202        ]
203        .into(),
204    }
205}