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/// All methods require `! [Http]`. 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;
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
45/// Returns `Some(result)` when `name` is owned by this service, `None` otherwise.
46pub 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}