moduforge_rules_engine/handler/function/module/
http.rs1use crate::handler::function::error::ResultExt;
2use crate::handler::function::module::export_default;
3use crate::handler::function::serde::JsValue;
4use reqwest::header::{HeaderMap, HeaderName};
5use reqwest::Method;
6use rquickjs::module::{Declarations, Exports, ModuleDef};
7use rquickjs::prelude::{Async, Func, Opt};
8use rquickjs::{CatchResultExt, Ctx, FromJs, IntoAtom, IntoJs, Object, Value};
9use std::str::FromStr;
10use std::sync::OnceLock;
11use moduforge_rules_expression::variable::Variable;
12
13pub(crate) struct HttpResponse<'js> {
14 data: Value<'js>,
15 headers: Object<'js>,
16 status: u16,
17}
18
19impl<'js> IntoJs<'js> for HttpResponse<'js> {
20 fn into_js(
21 self,
22 ctx: &Ctx<'js>,
23 ) -> rquickjs::Result<Value<'js>> {
24 let object = Object::new(ctx.clone())?;
25 object.set("data", self.data)?;
26 object.set("headers", self.headers)?;
27 object.set("status", self.status)?;
28
29 Ok(object.into_value())
30 }
31}
32
33async fn execute_http<'js>(
34 ctx: Ctx<'js>,
35 method: Method,
36 url: String,
37 data: Option<JsValue>,
38 config: Option<HttpConfig>,
39) -> rquickjs::Result<HttpResponse<'js>> {
40 static HTTP_CLIENT: OnceLock<reqwest::Client> = OnceLock::new();
41
42 let client = HTTP_CLIENT.get_or_init(|| reqwest::Client::new()).clone();
43 let mut builder = client.request(method, url);
44 if let Some(data) = data {
45 builder = builder.json(&data.0);
46 }
47
48 if let Some(config) = config {
49 builder =
50 builder.headers(config.headers).query(config.params.as_slice());
51
52 if let Some(data) = config.data {
53 builder = builder.json(&data.0);
54 }
55 }
56
57 let response = builder.send().await.or_throw(&ctx)?;
58 let status = response.status().as_u16();
59 let header_object = Object::new(ctx.clone()).catch(&ctx).or_throw(&ctx)?;
60 for (key, value) in response.headers() {
61 header_object.set(
62 key.as_str().into_atom(&ctx)?,
63 value.to_str().or_throw(&ctx).into_js(&ctx),
64 )?;
65 }
66
67 let data: Variable = response.json().await.or_throw(&ctx)?;
68
69 Ok(HttpResponse {
70 data: JsValue(data).into_js(&ctx)?,
71 headers: header_object,
72 status,
73 })
74}
75
76#[derive(Default)]
77pub(crate) struct HttpConfig {
78 headers: HeaderMap,
79 params: Vec<(String, String)>,
80 data: Option<JsValue>,
81}
82
83impl<'js> FromJs<'js> for HttpConfig {
84 fn from_js(
85 ctx: &Ctx<'js>,
86 value: Value<'js>,
87 ) -> rquickjs::Result<Self> {
88 let object = value.into_object().or_throw(ctx)?;
89 let headers_obj: Option<Object<'js>> =
90 object.get("headers").or_throw(ctx)?;
91 let headers = if let Some(headers_obj) = headers_obj {
92 let mut header_map = HeaderMap::with_capacity(headers_obj.len());
93 for result in headers_obj.into_iter() {
94 let Ok((key, value)) = result else {
95 continue;
96 };
97
98 let value = JsValue::from_js(ctx, value)?;
99 let str_value = match value.0 {
100 Variable::Bool(b) => Some(b.to_string()),
101 Variable::Number(n) => Some(n.to_string()),
102 Variable::String(s) => Some(s.to_string()),
103 Variable::Null => None,
104 Variable::Array(_) => None,
105 Variable::Object(_) => None,
106 Variable::Dynamic(_) => None,
107 };
108
109 let key_value = key.to_string()?;
110 let key =
111 HeaderName::from_str(key_value.as_str()).or_throw(&ctx)?;
112 if let Some(str_value) = str_value {
113 header_map.insert(key, str_value.parse().or_throw(&ctx)?);
114 }
115 }
116
117 header_map
118 } else {
119 HeaderMap::default()
120 };
121
122 let params_obj: Option<Object<'js>> =
123 object.get("params").or_throw(ctx)?;
124 let params = if let Some(params_obj) = params_obj {
125 let mut params = Vec::with_capacity(params_obj.len());
126 for result in params_obj.into_iter() {
127 let Ok((key, value)) = result else {
128 continue;
129 };
130
131 let value = JsValue::from_js(ctx, value)?;
132 let str_value = match value.0 {
133 Variable::Bool(b) => Some(b.to_string()),
134 Variable::Number(n) => Some(n.to_string()),
135 Variable::String(s) => Some(s.to_string()),
136 Variable::Null => None,
137 Variable::Array(_) => None,
138 Variable::Object(_) => None,
139 Variable::Dynamic(_) => None,
140 };
141
142 let key = key.to_string()?;
143 if let Some(str_value) = str_value {
144 params.push((key, str_value));
145 }
146 }
147
148 params
149 } else {
150 Vec::default()
151 };
152
153 let data_obj: Option<Value<'js>> = object.get("data").ok();
154 let data = if let Some(data_obj) = data_obj {
155 Some(JsValue::from_js(&ctx, data_obj).catch(&ctx).or_throw(&ctx)?)
156 } else {
157 None
158 };
159
160 Ok(Self { headers, params, data })
161 }
162}
163
164async fn get<'js>(
165 ctx: Ctx<'js>,
166 url: String,
167 config: Opt<HttpConfig>,
168) -> rquickjs::Result<HttpResponse<'js>> {
169 execute_http(ctx, Method::GET, url, None, config.0).await
170}
171
172async fn post<'js>(
173 ctx: Ctx<'js>,
174 url: String,
175 data: JsValue,
176 config: Opt<HttpConfig>,
177) -> rquickjs::Result<HttpResponse<'js>> {
178 execute_http(ctx, Method::POST, url, Some(data), config.0).await
179}
180
181async fn patch<'js>(
182 ctx: Ctx<'js>,
183 url: String,
184 data: JsValue,
185 config: Opt<HttpConfig>,
186) -> rquickjs::Result<HttpResponse<'js>> {
187 execute_http(ctx, Method::PATCH, url, Some(data), config.0).await
188}
189
190async fn put<'js>(
191 ctx: Ctx<'js>,
192 url: String,
193 data: JsValue,
194 config: Opt<HttpConfig>,
195) -> rquickjs::Result<HttpResponse<'js>> {
196 execute_http(ctx, Method::PUT, url, Some(data), config.0).await
197}
198
199async fn delete<'js>(
200 ctx: Ctx<'js>,
201 url: String,
202 config: Opt<HttpConfig>,
203) -> rquickjs::Result<HttpResponse<'js>> {
204 execute_http(ctx, Method::DELETE, url, None, config.0).await
205}
206
207async fn head<'js>(
208 ctx: Ctx<'js>,
209 url: String,
210 config: Opt<HttpConfig>,
211) -> rquickjs::Result<HttpResponse<'js>> {
212 execute_http(ctx, Method::DELETE, url, None, config.0).await
213}
214
215pub(crate) struct HttpModule;
216
217impl ModuleDef for HttpModule {
218 fn declare<'js>(decl: &Declarations<'js>) -> rquickjs::Result<()> {
219 decl.declare("get")?;
220 decl.declare("head")?;
221 decl.declare("post")?;
222 decl.declare("patch")?;
223 decl.declare("put")?;
224 decl.declare("delete")?;
225
226 decl.declare("default")?;
227
228 Ok(())
229 }
230
231 fn evaluate<'js>(
232 ctx: &Ctx<'js>,
233 exports: &Exports<'js>,
234 ) -> rquickjs::Result<()> {
235 export_default(ctx, exports, |default| {
236 default.set("get", Func::from(Async(get)))?;
237 default.set("head", Func::from(Async(head)))?;
238 default.set("post", Func::from(Async(post)))?;
239 default.set("patch", Func::from(Async(patch)))?;
240 default.set("put", Func::from(Async(put)))?;
241 default.set("delete", Func::from(Async(delete)))?;
242
243 Ok(())
244 })
245 }
246}