moduforge_rules_engine/handler/function/module/
http.rs

1use 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}