moduforge-rules-engine 0.5.0

moduforge 引擎规则
Documentation
use crate::handler::function::error::ResultExt;
use crate::handler::function::module::export_default;
use crate::handler::function::serde::JsValue;
use reqwest::header::{HeaderMap, HeaderName};
use reqwest::Method;
use rquickjs::module::{Declarations, Exports, ModuleDef};
use rquickjs::prelude::{Async, Func, Opt};
use rquickjs::{CatchResultExt, Ctx, FromJs, IntoAtom, IntoJs, Object, Value};
use std::str::FromStr;
use std::sync::OnceLock;
use mf_expression::variable::Variable;

pub(crate) struct HttpResponse<'js> {
    data: Value<'js>,
    headers: Object<'js>,
    status: u16,
}

impl<'js> IntoJs<'js> for HttpResponse<'js> {
    fn into_js(
        self,
        ctx: &Ctx<'js>,
    ) -> rquickjs::Result<Value<'js>> {
        let object = Object::new(ctx.clone())?;
        object.set("data", self.data)?;
        object.set("headers", self.headers)?;
        object.set("status", self.status)?;

        Ok(object.into_value())
    }
}

async fn execute_http<'js>(
    ctx: Ctx<'js>,
    method: Method,
    url: String,
    data: Option<JsValue>,
    config: Option<HttpConfig>,
) -> rquickjs::Result<HttpResponse<'js>> {
    static HTTP_CLIENT: OnceLock<reqwest::Client> = OnceLock::new();

    let client = HTTP_CLIENT.get_or_init(|| reqwest::Client::new()).clone();
    let mut builder = client.request(method, url);
    if let Some(data) = data {
        builder = builder.json(&data.0);
    }

    if let Some(config) = config {
        builder =
            builder.headers(config.headers).query(config.params.as_slice());

        if let Some(data) = config.data {
            builder = builder.json(&data.0);
        }
    }

    let response = builder.send().await.or_throw(&ctx)?;
    let status = response.status().as_u16();
    let header_object = Object::new(ctx.clone()).catch(&ctx).or_throw(&ctx)?;
    for (key, value) in response.headers() {
        header_object.set(
            key.as_str().into_atom(&ctx)?,
            value.to_str().or_throw(&ctx).into_js(&ctx),
        )?;
    }

    let data: Variable = response.json().await.or_throw(&ctx)?;

    Ok(HttpResponse {
        data: JsValue(data).into_js(&ctx)?,
        headers: header_object,
        status,
    })
}

#[derive(Default)]
pub(crate) struct HttpConfig {
    headers: HeaderMap,
    params: Vec<(String, String)>,
    data: Option<JsValue>,
}

impl<'js> FromJs<'js> for HttpConfig {
    fn from_js(
        ctx: &Ctx<'js>,
        value: Value<'js>,
    ) -> rquickjs::Result<Self> {
        let object = value.into_object().or_throw(ctx)?;
        let headers_obj: Option<Object<'js>> =
            object.get("headers").or_throw(ctx)?;
        let headers = if let Some(headers_obj) = headers_obj {
            let mut header_map = HeaderMap::with_capacity(headers_obj.len());
            for result in headers_obj.into_iter() {
                let Ok((key, value)) = result else {
                    continue;
                };

                let value = JsValue::from_js(ctx, value)?;
                let str_value = match value.0 {
                    Variable::Bool(b) => Some(b.to_string()),
                    Variable::Number(n) => Some(n.to_string()),
                    Variable::String(s) => Some(s.to_string()),
                    Variable::Null => None,
                    Variable::Array(_) => None,
                    Variable::Object(_) => None,
                    Variable::Dynamic(_) => None,
                };

                let key_value = key.to_string()?;
                let key =
                    HeaderName::from_str(key_value.as_str()).or_throw(&ctx)?;
                if let Some(str_value) = str_value {
                    header_map.insert(key, str_value.parse().or_throw(&ctx)?);
                }
            }

            header_map
        } else {
            HeaderMap::default()
        };

        let params_obj: Option<Object<'js>> =
            object.get("params").or_throw(ctx)?;
        let params = if let Some(params_obj) = params_obj {
            let mut params = Vec::with_capacity(params_obj.len());
            for result in params_obj.into_iter() {
                let Ok((key, value)) = result else {
                    continue;
                };

                let value = JsValue::from_js(ctx, value)?;
                let str_value = match value.0 {
                    Variable::Bool(b) => Some(b.to_string()),
                    Variable::Number(n) => Some(n.to_string()),
                    Variable::String(s) => Some(s.to_string()),
                    Variable::Null => None,
                    Variable::Array(_) => None,
                    Variable::Object(_) => None,
                    Variable::Dynamic(_) => None,
                };

                let key = key.to_string()?;
                if let Some(str_value) = str_value {
                    params.push((key, str_value));
                }
            }

            params
        } else {
            Vec::default()
        };

        let data_obj: Option<Value<'js>> = object.get("data").ok();
        let data = if let Some(data_obj) = data_obj {
            Some(JsValue::from_js(&ctx, data_obj).catch(&ctx).or_throw(&ctx)?)
        } else {
            None
        };

        Ok(Self { headers, params, data })
    }
}

async fn get<'js>(
    ctx: Ctx<'js>,
    url: String,
    config: Opt<HttpConfig>,
) -> rquickjs::Result<HttpResponse<'js>> {
    execute_http(ctx, Method::GET, url, None, config.0).await
}

async fn post<'js>(
    ctx: Ctx<'js>,
    url: String,
    data: JsValue,
    config: Opt<HttpConfig>,
) -> rquickjs::Result<HttpResponse<'js>> {
    execute_http(ctx, Method::POST, url, Some(data), config.0).await
}

async fn patch<'js>(
    ctx: Ctx<'js>,
    url: String,
    data: JsValue,
    config: Opt<HttpConfig>,
) -> rquickjs::Result<HttpResponse<'js>> {
    execute_http(ctx, Method::PATCH, url, Some(data), config.0).await
}

async fn put<'js>(
    ctx: Ctx<'js>,
    url: String,
    data: JsValue,
    config: Opt<HttpConfig>,
) -> rquickjs::Result<HttpResponse<'js>> {
    execute_http(ctx, Method::PUT, url, Some(data), config.0).await
}

async fn delete<'js>(
    ctx: Ctx<'js>,
    url: String,
    config: Opt<HttpConfig>,
) -> rquickjs::Result<HttpResponse<'js>> {
    execute_http(ctx, Method::DELETE, url, None, config.0).await
}

async fn head<'js>(
    ctx: Ctx<'js>,
    url: String,
    config: Opt<HttpConfig>,
) -> rquickjs::Result<HttpResponse<'js>> {
    execute_http(ctx, Method::DELETE, url, None, config.0).await
}

pub(crate) struct HttpModule;

impl ModuleDef for HttpModule {
    fn declare<'js>(decl: &Declarations<'js>) -> rquickjs::Result<()> {
        decl.declare("get")?;
        decl.declare("head")?;
        decl.declare("post")?;
        decl.declare("patch")?;
        decl.declare("put")?;
        decl.declare("delete")?;

        decl.declare("default")?;

        Ok(())
    }

    fn evaluate<'js>(
        ctx: &Ctx<'js>,
        exports: &Exports<'js>,
    ) -> rquickjs::Result<()> {
        export_default(ctx, exports, |default| {
            default.set("get", Func::from(Async(get)))?;
            default.set("head", Func::from(Async(head)))?;
            default.set("post", Func::from(Async(post)))?;
            default.set("patch", Func::from(Async(patch)))?;
            default.set("put", Func::from(Async(put)))?;
            default.set("delete", Func::from(Async(delete)))?;

            Ok(())
        })
    }
}