1use rhai::{def_package, plugin::*};
2
3#[derive(Default, Clone, serde::Deserialize)]
4#[serde(rename_all = "snake_case")]
5pub enum Output {
6 #[default]
7 Text,
8 Json,
9}
10
11#[derive(Clone, serde::Deserialize)]
12pub struct Parameters {
13 method: String,
14 url: String,
15 #[serde(default)]
16 headers: rhai::Array,
17 #[serde(default)]
18 body: rhai::Dynamic,
19 #[serde(default)]
20 output: Output,
21}
22
23#[export_module]
24pub mod api {
25 use std::str::FromStr;
26
27 pub type Client = reqwest::blocking::Client;
31
32 #[rhai_fn(return_raw)]
47 pub fn client() -> Result<Client, Box<rhai::EvalAltResult>> {
48 reqwest::blocking::Client::builder()
49 .build()
50 .map_err(|error| error.to_string().into())
51 }
52
53 #[rhai_fn(global, pure, return_raw)]
85 pub fn request(
86 client: &mut Client,
87 parameters: rhai::Map,
88 ) -> Result<rhai::Dynamic, Box<rhai::EvalAltResult>> {
89 let Parameters {
90 method,
91 url,
92 headers,
93 body,
94 output,
95 } = rhai::serde::from_dynamic::<Parameters>(¶meters.into())?;
96
97 let method = reqwest::Method::from_str(&method)
98 .map_err::<Box<rhai::EvalAltResult>, _>(|error| error.to_string().into())?;
99
100 client
101 .request(method, url)
102 .headers(
103 headers
104 .iter()
105 .map(|header| {
106 if let Some((name, value)) = header.to_string().split_once(':') {
107 let name = name.trim();
108 let value = value.trim();
109
110 let name = reqwest::header::HeaderName::from_str(name).map_err::<Box<
111 EvalAltResult,
112 >, _>(
113 |error| error.to_string().into(),
114 )?;
115 let value = reqwest::header::HeaderValue::from_str(value)
116 .map_err::<Box<EvalAltResult>, _>(|error| {
117 error.to_string().into()
118 })?;
119
120 Ok((name, value))
121 } else {
122 Err(format!("'{header}' is not a valid header").into())
123 }
124 })
125 .collect::<Result<reqwest::header::HeaderMap, Box<EvalAltResult>>>()?,
126 )
127 .body(body.to_string())
129 .send()
130 .and_then(|response| match output {
131 Output::Text => response.text().map(rhai::Dynamic::from),
132 Output::Json => response.json::<rhai::Map>().map(rhai::Dynamic::from),
133 })
134 .map_err(|error| format!("{error:?}").into())
135 }
136}
137
138def_package! {
139 pub HttpPackage(_module) {} |> |engine| {
141 engine.register_static_module("http", rhai::exported_module!(api).into());
144 }
145}
146
147#[cfg(test)]
148pub mod test {
149 use crate::HttpPackage;
150 use rhai::packages::Package;
151
152 #[test]
153 fn simple_query() {
154 let mut engine = rhai::Engine::new();
155
156 HttpPackage::new().register_into_engine(&mut engine);
157
158 let body: String = engine
159 .eval(
160 r#"
161let client = http::client();
162
163client.request(#{ method: "GET", url: "http://example.com" })"#,
164 )
165 .unwrap();
166
167 assert_eq!(
168 body,
169 concat!(
170 r#"<!doctype html><html lang="en"><head><title>Example Domain</title><meta name="viewport" content="width=device-width, initial-scale=1"><style>body{background:#eee;width:60vw;margin:15vh auto;font-family:system-ui,sans-serif}h1{font-size:1.5em}div{opacity:0.8}a:link,a:visited{color:#348}</style><body><div><h1>Example Domain</h1><p>This domain is for use in documentation examples without needing permission. Avoid use in operations.<p><a href="https://iana.org/domains/example">Learn more</a></div></body></html>"#,
171 "\n"
172 )
173 );
174 }
175
176 #[test]
177 fn simple_query_headers() {
178 let mut engine = rhai::Engine::new();
179
180 HttpPackage::new().register_into_engine(&mut engine);
181
182 let body: rhai::Map = engine
183 .eval(
184 r#"
185let client = http::client();
186
187client.request(#{
188 "method": "GET",
189 "url": "https://pro-api.coinmarketcap.com/v1/cryptocurrency/quotes/latest?slug=bitcoin&convert=EUR",
190 "headers": [
191 "X-CMC_PRO_API_KEY: xxx",
192 "Accept: application/json"
193 ],
194 "output": "json",
195})
196"#,
197 )
198 .unwrap();
199
200 println!("{body:#?}");
201 }
202}