tatara_lisp_script/stdlib/
http.rs1use std::sync::Arc;
18
19use tatara_lisp_eval::{Arity, EvalError, Interpreter, Value};
20
21use crate::script_ctx::ScriptCtx;
22use crate::stdlib::env::str_arg;
23use crate::stdlib::json::json_to_value;
24
25pub fn install(interp: &mut Interpreter<ScriptCtx>) {
26 interp.register_fn(
27 "http-get",
28 Arity::Range(1, 2),
29 |args: &[Value], ctx: &mut ScriptCtx, sp| {
30 let url = str_arg(&args[0], "http-get", sp)?;
31 let headers = headers_from_value(args.get(1), sp)?;
32 let body = do_get(ctx, &url, &headers, sp)?;
33 Ok(Value::Str(Arc::from(body)))
34 },
35 );
36
37 interp.register_fn(
38 "http-get-json",
39 Arity::Range(1, 2),
40 |args: &[Value], ctx: &mut ScriptCtx, sp| {
41 let url = str_arg(&args[0], "http-get-json", sp)?;
42 let headers = headers_from_value(args.get(1), sp)?;
43 let body = do_get(ctx, &url, &headers, sp)?;
44 let parsed: serde_json::Value = serde_json::from_str(&body).map_err(|e| {
45 EvalError::native_fn(
46 "http-get-json",
47 format!("response not JSON: {e} (body={body:.200})"),
48 sp,
49 )
50 })?;
51 Ok(json_to_value(&parsed))
52 },
53 );
54
55 interp.register_fn(
56 "http-post-json",
57 Arity::Exact(3),
58 |args: &[Value], ctx: &mut ScriptCtx, sp| {
59 let url = str_arg(&args[0], "http-post-json", sp)?;
60 let headers = headers_from_value(Some(&args[1]), sp)?;
61 let body_json = crate::stdlib::json::value_to_json(&args[2]);
62 let agent = ctx.http().clone();
63 let mut req = agent.post(&*url);
64 for (k, v) in &headers {
65 req = req.header(&**k, &**v);
66 }
67 let resp_body = req
68 .send(
69 serde_json::to_string(&body_json)
70 .map_err(|e| EvalError::native_fn("http-post-json", e.to_string(), sp))?,
71 )
72 .map_err(|e| EvalError::native_fn("http-post-json", e.to_string(), sp))?
73 .into_body()
74 .read_to_string()
75 .map_err(|e| EvalError::native_fn("http-post-json", e.to_string(), sp))?;
76 let parsed: serde_json::Value = serde_json::from_str(&resp_body).map_err(|e| {
77 EvalError::native_fn("http-post-json", format!("response not JSON: {e}"), sp)
78 })?;
79 Ok(json_to_value(&parsed))
80 },
81 );
82}
83
84fn do_get(
85 ctx: &mut ScriptCtx,
86 url: &str,
87 headers: &[(Arc<str>, Arc<str>)],
88 sp: tatara_lisp::Span,
89) -> Result<String, EvalError> {
90 let agent = ctx.http().clone();
91 let mut req = agent.get(url);
92 for (k, v) in headers {
93 req = req.header(&**k, &**v);
94 }
95 let resp = req
96 .call()
97 .map_err(|e| EvalError::native_fn("http-get", format!("{url}: {e}"), sp))?;
98 resp.into_body()
99 .read_to_string()
100 .map_err(|e| EvalError::native_fn("http-get", format!("{url}: {e}"), sp))
101}
102
103fn headers_from_value(
106 v: Option<&Value>,
107 sp: tatara_lisp::Span,
108) -> Result<Vec<(Arc<str>, Arc<str>)>, EvalError> {
109 let Some(v) = v else { return Ok(vec![]) };
110 match v {
111 Value::Nil => Ok(vec![]),
112 Value::List(entries) => {
113 let mut out = Vec::with_capacity(entries.len());
114 for entry in entries.iter() {
115 let Value::List(pair) = entry else {
116 return Err(EvalError::native_fn(
117 "http",
118 "header entry must be a (KEY VALUE) list",
119 sp,
120 ));
121 };
122 if pair.len() != 2 {
123 return Err(EvalError::native_fn(
124 "http",
125 format!(
126 "header entry must be (KEY VALUE), got {} elements",
127 pair.len()
128 ),
129 sp,
130 ));
131 }
132 let k = match &pair[0] {
133 Value::Str(s) => s.clone(),
134 Value::Symbol(s) | Value::Keyword(s) => s.clone(),
135 other => {
136 return Err(EvalError::native_fn(
137 "http",
138 format!(
139 "header key must be string/symbol/keyword, got {}",
140 other.type_name()
141 ),
142 sp,
143 ))
144 }
145 };
146 let v = match &pair[1] {
147 Value::Str(s) => s.clone(),
148 other => {
149 return Err(EvalError::native_fn(
150 "http",
151 format!("header value must be string, got {}", other.type_name()),
152 sp,
153 ))
154 }
155 };
156 out.push((k, v));
157 }
158 Ok(out)
159 }
160 other => Err(EvalError::native_fn(
161 "http",
162 format!("headers must be a list, got {}", other.type_name()),
163 sp,
164 )),
165 }
166}