mirror of
https://github.com/gorules/zen.git
synced 2026-10-07 16:02:26 +00:00
feat: function v2 (#212)
* feat: function v2 * expose evaluate * remove async trait, add caching * fix naming * add reverse compatibility * add http module * update impl * resolve issues with reqwest * fix * fix * fix ssl * remove unused deps * fix: tests * fix
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
use std::fmt::{Display, Formatter};
|
||||
|
||||
use rquickjs::{CaughtError, Ctx, Error, Exception};
|
||||
|
||||
pub type FunctionResult<Ok = ()> = Result<Ok, FunctionError>;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum FunctionError {
|
||||
Caught(String),
|
||||
Runtime(Error),
|
||||
}
|
||||
|
||||
impl<'js> From<CaughtError<'js>> for FunctionError {
|
||||
fn from(value: CaughtError<'js>) -> Self {
|
||||
Self::Caught(value.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Error> for FunctionError {
|
||||
fn from(value: Error) -> Self {
|
||||
Self::Runtime(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for FunctionError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
FunctionError::Caught(c) => f.write_str(c.as_str()),
|
||||
FunctionError::Runtime(rt) => rt.fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ResultExt<T> {
|
||||
#[allow(dead_code)]
|
||||
fn or_throw_msg(self, ctx: &Ctx, msg: &str) -> rquickjs::Result<T>;
|
||||
fn or_throw(self, ctx: &Ctx) -> rquickjs::Result<T>;
|
||||
}
|
||||
|
||||
impl<T, E: Display> ResultExt<T> for Result<T, E> {
|
||||
fn or_throw_msg(self, ctx: &Ctx, msg: &str) -> rquickjs::Result<T> {
|
||||
self.map_err(|_| {
|
||||
let mut message = String::with_capacity(100);
|
||||
message.push_str(msg);
|
||||
message.push_str(".");
|
||||
Exception::throw_message(ctx, &message)
|
||||
})
|
||||
}
|
||||
|
||||
fn or_throw(self, ctx: &Ctx) -> rquickjs::Result<T> {
|
||||
self.map_err(|err| Exception::throw_message(ctx, &err.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> ResultExt<T> for Option<T> {
|
||||
fn or_throw_msg(self, ctx: &Ctx, msg: &str) -> rquickjs::Result<T> {
|
||||
self.ok_or(Exception::throw_message(ctx, msg))
|
||||
}
|
||||
|
||||
fn or_throw(self, ctx: &Ctx) -> rquickjs::Result<T> {
|
||||
self.ok_or(Exception::throw_message(ctx, "Value is not present"))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use rquickjs::promise::MaybePromise;
|
||||
use rquickjs::{async_with, AsyncContext, AsyncRuntime, CatchResultExt, Ctx, Module};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::handler::function::error::{FunctionError, FunctionResult, ResultExt};
|
||||
use crate::handler::function::listener::{RuntimeEvent, RuntimeListener};
|
||||
use crate::handler::function::module::console::{Console, Log};
|
||||
use crate::handler::function::module::ModuleLoader;
|
||||
use crate::handler::function::serde::JsValue;
|
||||
|
||||
pub struct FunctionConfig {
|
||||
pub(crate) listeners: Option<Vec<Box<dyn RuntimeListener>>>,
|
||||
}
|
||||
|
||||
pub struct Function {
|
||||
rt: Arc<AsyncRuntime>,
|
||||
ctx: AsyncContext,
|
||||
listeners: Vec<Box<dyn RuntimeListener>>,
|
||||
module_loader: ModuleLoader,
|
||||
}
|
||||
|
||||
impl Function {
|
||||
pub async fn create<'js>(config: FunctionConfig) -> FunctionResult<Self> {
|
||||
let module_loader = ModuleLoader::new();
|
||||
let rt = Arc::new(AsyncRuntime::new()?);
|
||||
|
||||
rt.set_loader(module_loader.clone(), module_loader.clone())
|
||||
.await;
|
||||
|
||||
let ctx = AsyncContext::full(&rt).await?;
|
||||
let this = Self {
|
||||
rt,
|
||||
ctx,
|
||||
module_loader,
|
||||
listeners: config.listeners.unwrap_or_default(),
|
||||
};
|
||||
|
||||
this.dispatch_event(RuntimeEvent::Startup).await?;
|
||||
Ok(this)
|
||||
}
|
||||
|
||||
async fn dispatch_event_inner(&self, ctx: &Ctx<'_>, event: RuntimeEvent) -> FunctionResult {
|
||||
for listener in &self.listeners {
|
||||
if let Err(err) = listener.on_event(ctx.clone(), event.clone()).await {
|
||||
return Err(err.into());
|
||||
};
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
async fn dispatch_event(&self, event: RuntimeEvent) -> FunctionResult {
|
||||
async_with!(&self.ctx => |ctx| {
|
||||
self.dispatch_event_inner(&ctx, event).await
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub fn context(&self) -> &AsyncContext {
|
||||
&self.ctx
|
||||
}
|
||||
|
||||
pub fn runtime(&self) -> &AsyncRuntime {
|
||||
&self.rt
|
||||
}
|
||||
|
||||
pub fn suggest_module_name<'a>(&self, id: &str, name: &str) -> String {
|
||||
let declarative_name = format!("node:{name}");
|
||||
|
||||
if self.module_loader.has_module(&declarative_name) {
|
||||
format!("node:{id}")
|
||||
} else {
|
||||
declarative_name
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn register_module(&self, name: &str, source: &str) -> FunctionResult {
|
||||
let maybe_error: Option<FunctionError> = async_with!(&self.ctx => |ctx| {
|
||||
if let Err(err) = Module::declare(ctx.clone(), name.as_bytes().to_vec(), source.as_bytes().to_vec()).catch(&ctx) {
|
||||
return Some(err.into())
|
||||
}
|
||||
|
||||
return None;
|
||||
}).await;
|
||||
if let Some(err) = maybe_error {
|
||||
return Err(err);
|
||||
}
|
||||
|
||||
self.module_loader.add_module(name.to_string());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn call_handler(
|
||||
&self,
|
||||
name: &str,
|
||||
data: JsValue,
|
||||
) -> FunctionResult<HandlerResponse> {
|
||||
let k: FunctionResult<HandlerResponse> = async_with!(&self.ctx => |ctx| {
|
||||
self.dispatch_event_inner(&ctx, RuntimeEvent::SoftReset).await?;
|
||||
|
||||
let m: rquickjs::Object = Module::import(&ctx, name).catch(&ctx)?.into_future().await.catch(&ctx)?;
|
||||
let handler: rquickjs::Function = m.get("handler").catch(&ctx)?;
|
||||
|
||||
let handler_promise: MaybePromise = handler.call((data, 5)).catch(&ctx)?;
|
||||
let handler_result = handler_promise.into_future::<JsValue>().await.catch(&ctx)?;
|
||||
|
||||
let console = Console::from_context(&ctx).or_throw(&ctx)?;
|
||||
let logs = console.logs.into_inner();
|
||||
|
||||
Ok(HandlerResponse { data: handler_result.0, logs })
|
||||
})
|
||||
.await;
|
||||
|
||||
Ok(k?)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct HandlerResponse {
|
||||
pub logs: Vec<Log>,
|
||||
pub data: Value,
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
|
||||
use rquickjs::Ctx;
|
||||
|
||||
use crate::handler::function::error::FunctionResult;
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub(crate) enum RuntimeEvent {
|
||||
Startup,
|
||||
SoftReset,
|
||||
}
|
||||
|
||||
pub(crate) trait RuntimeListener {
|
||||
fn on_event<'js>(
|
||||
&self,
|
||||
ctx: Ctx<'js>,
|
||||
event: RuntimeEvent,
|
||||
) -> Pin<Box<dyn Future<Output = FunctionResult> + 'js>>;
|
||||
}
|
||||
@@ -1,48 +1,102 @@
|
||||
use std::time::{Duration, Instant};
|
||||
use std::rc::Rc;
|
||||
use std::time::Duration;
|
||||
|
||||
use ::serde::{Deserialize, Serialize};
|
||||
use anyhow::anyhow;
|
||||
use rquickjs::Runtime;
|
||||
use rquickjs::{async_with, CatchResultExt, Object};
|
||||
use serde_json::json;
|
||||
|
||||
use crate::handler::function::script::Script;
|
||||
use crate::handler::function::error::FunctionResult;
|
||||
use crate::handler::function::function::{Function, HandlerResponse};
|
||||
use crate::handler::function::serde::JsValue;
|
||||
use crate::handler::node::{NodeRequest, NodeResponse, NodeResult};
|
||||
use crate::model::DecisionNodeKind;
|
||||
use crate::model::{DecisionNodeKind, FunctionNodeContent};
|
||||
|
||||
mod js_value;
|
||||
pub(crate) mod runtime;
|
||||
mod script;
|
||||
pub(crate) mod error;
|
||||
pub(crate) mod function;
|
||||
pub(crate) mod listener;
|
||||
pub(crate) mod module;
|
||||
pub(crate) mod serde;
|
||||
|
||||
pub struct FunctionHandler {
|
||||
trace: bool,
|
||||
runtime: Runtime,
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct FunctionResponse {
|
||||
performance: String,
|
||||
data: Option<HandlerResponse>,
|
||||
}
|
||||
|
||||
static MAX_DURATION: Duration = Duration::from_millis(500);
|
||||
pub struct FunctionHandler {
|
||||
function: Rc<Function>,
|
||||
trace: bool,
|
||||
iteration: u8,
|
||||
max_depth: u8,
|
||||
}
|
||||
|
||||
static MAX_DURATION: Duration = Duration::from_millis(5_000);
|
||||
|
||||
impl FunctionHandler {
|
||||
pub fn new(trace: bool, runtime: Runtime) -> Self {
|
||||
Self { trace, runtime }
|
||||
pub fn new(function: Rc<Function>, trace: bool, iteration: u8, max_depth: u8) -> Self {
|
||||
Self {
|
||||
function,
|
||||
trace,
|
||||
iteration,
|
||||
max_depth,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn handle(&self, request: &NodeRequest<'_>) -> NodeResult {
|
||||
let content = match &request.node.kind {
|
||||
DecisionNodeKind::FunctionNode { content } => Ok(content),
|
||||
DecisionNodeKind::FunctionNode { content } => match content {
|
||||
FunctionNodeContent::Version2(content) => Ok(content),
|
||||
_ => Err(anyhow!("Unexpected node type")),
|
||||
},
|
||||
_ => Err(anyhow!("Unexpected node type")),
|
||||
}?;
|
||||
let start = std::time::Instant::now();
|
||||
|
||||
let start = Instant::now();
|
||||
let module_name = self
|
||||
.function
|
||||
.suggest_module_name(request.node.id.as_str(), request.node.name.as_str());
|
||||
let interrupt_handler = Box::new(move || start.elapsed() > MAX_DURATION);
|
||||
self.runtime.set_interrupt_handler(Some(interrupt_handler));
|
||||
self.function
|
||||
.runtime()
|
||||
.set_interrupt_handler(Some(interrupt_handler))
|
||||
.await;
|
||||
|
||||
let mut script = Script::new(self.runtime.clone());
|
||||
let result_response = script.call(content, &request.input).await;
|
||||
self.attach_globals()
|
||||
.await
|
||||
.map_err(|e| anyhow!(e.to_string()))?;
|
||||
|
||||
self.runtime.set_interrupt_handler(None);
|
||||
self.function
|
||||
.register_module(&module_name, content.source.as_str())
|
||||
.await
|
||||
.map_err(|e| anyhow!(e.to_string()))?;
|
||||
|
||||
let response = self
|
||||
.function
|
||||
.call_handler(&module_name, JsValue(request.input.clone()))
|
||||
.await
|
||||
.map_err(|e| anyhow!(e.to_string()))?;
|
||||
|
||||
self.function.runtime().set_interrupt_handler(None).await;
|
||||
|
||||
let response = result_response?;
|
||||
Ok(NodeResponse {
|
||||
output: response.output,
|
||||
trace_data: self.trace.then(|| json!({ "log": response.log })),
|
||||
output: response.data,
|
||||
trace_data: self.trace.then(|| json!({ "log": response.logs })),
|
||||
})
|
||||
}
|
||||
|
||||
async fn attach_globals(&self) -> FunctionResult {
|
||||
async_with!(self.function.context() => |ctx| {
|
||||
let config = Object::new(ctx.clone()).catch(&ctx)?;
|
||||
|
||||
config.prop("iteration", self.iteration).catch(&ctx)?;
|
||||
config.prop("maxDepth", self.max_depth).catch(&ctx)?;
|
||||
config.prop("trace", self.trace).catch(&ctx)?;
|
||||
|
||||
ctx.globals().set("config", config).catch(&ctx)?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
use std::cell::RefCell;
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use crate::handler::function::error::{FunctionResult, ResultExt};
|
||||
use crate::handler::function::listener::{RuntimeEvent, RuntimeListener};
|
||||
use rquickjs::prelude::Rest;
|
||||
use rquickjs::{Ctx, Object, Value};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub(crate) struct ConsoleListener;
|
||||
|
||||
impl RuntimeListener for ConsoleListener {
|
||||
fn on_event<'js>(
|
||||
&self,
|
||||
ctx: Ctx<'js>,
|
||||
event: RuntimeEvent,
|
||||
) -> Pin<Box<dyn Future<Output = FunctionResult> + 'js>> {
|
||||
Box::pin(async move {
|
||||
match event {
|
||||
RuntimeEvent::Startup => Console::init(&ctx)?,
|
||||
RuntimeEvent::SoftReset => Console::init(&ctx)?,
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Log {
|
||||
lines: Vec<String>,
|
||||
ms_since_run: usize,
|
||||
}
|
||||
|
||||
#[derive(rquickjs::class::Trace, Clone)]
|
||||
#[rquickjs::class]
|
||||
pub struct Console {
|
||||
#[qjs(skip_trace)]
|
||||
pub logs: RefCell<Vec<Log>>,
|
||||
#[qjs(skip_trace)]
|
||||
created_at: Instant,
|
||||
}
|
||||
|
||||
#[rquickjs::methods(rename_all = "camelCase")]
|
||||
impl Console {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
logs: Default::default(),
|
||||
created_at: Instant::now(),
|
||||
}
|
||||
}
|
||||
|
||||
#[qjs(skip)]
|
||||
pub fn init(ctx: &Ctx) -> rquickjs::Result<()> {
|
||||
ctx.globals().set("console", Self::new())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[qjs(skip)]
|
||||
pub fn from_context(ctx: &Ctx) -> rquickjs::Result<Self> {
|
||||
let obj: Self = ctx.globals().get("console")?;
|
||||
Ok(obj)
|
||||
}
|
||||
|
||||
pub fn log<'js>(&self, ctx: Ctx<'js>, args: Rest<Value<'js>>) -> rquickjs::Result<()> {
|
||||
let config: Object = ctx.globals().get("config").or_throw(&ctx)?;
|
||||
let trace: bool = config.get("trace").or_throw(&ctx)?;
|
||||
if !trace {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let step1 = args
|
||||
.0
|
||||
.into_iter()
|
||||
.map(|arg| ctx.json_stringify(arg))
|
||||
.collect::<Result<Vec<Option<rquickjs::String<'js>>>, _>>()?;
|
||||
|
||||
let step2 = step1
|
||||
.into_iter()
|
||||
.map(|s| s.map(|i| i.to_string()).transpose())
|
||||
.collect::<Result<Vec<Option<String>>, _>>()?;
|
||||
|
||||
let step3 = step2
|
||||
.into_iter()
|
||||
.map(|s| s.unwrap_or_default())
|
||||
.collect::<Vec<String>>();
|
||||
|
||||
let mut logs = self.logs.borrow_mut();
|
||||
logs.push(Log {
|
||||
lines: step3,
|
||||
ms_since_run: self.created_at.elapsed().as_millis() as usize,
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn sleep(&self, duration_ms: u64) -> rquickjs::Result<()> {
|
||||
tokio::time::sleep(Duration::from_millis(duration_ms)).await;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
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 crate::handler::function::error::ResultExt;
|
||||
use crate::handler::function::module::export_default;
|
||||
use crate::handler::function::serde::JsValue;
|
||||
|
||||
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> {
|
||||
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: serde_json::Value = 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 {
|
||||
serde_json::Value::Null => None,
|
||||
serde_json::Value::Bool(b) => Some(b.to_string()),
|
||||
serde_json::Value::Number(n) => Some(n.to_string()),
|
||||
serde_json::Value::String(s) => Some(s),
|
||||
serde_json::Value::Array(_) => None,
|
||||
serde_json::Value::Object(_) => 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 {
|
||||
serde_json::Value::Null => None,
|
||||
serde_json::Value::Bool(b) => Some(b.to_string()),
|
||||
serde_json::Value::Number(n) => Some(n.to_string()),
|
||||
serde_json::Value::String(s) => Some(s),
|
||||
serde_json::Value::Array(_) => None,
|
||||
serde_json::Value::Object(_) => 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> {
|
||||
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> {
|
||||
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> {
|
||||
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> {
|
||||
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> {
|
||||
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> {
|
||||
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(())
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashSet;
|
||||
use std::ops::DerefMut;
|
||||
use std::rc::Rc;
|
||||
|
||||
use rquickjs::loader::{Bundle, Loader, ModuleLoader as MDLoader, Resolver};
|
||||
use rquickjs::module::{Declared, Exports};
|
||||
use rquickjs::{embed, Ctx, Error, Module, Object};
|
||||
|
||||
use crate::handler::function::module::http::HttpModule;
|
||||
use crate::handler::function::module::zen::ZenModule;
|
||||
|
||||
pub(crate) mod console;
|
||||
pub(crate) mod http;
|
||||
pub(crate) mod zen;
|
||||
|
||||
static JS_BUNDLE: Bundle = embed! {
|
||||
"dayjs": "js/dayjs.mjs",
|
||||
"big.js": "js/big.mjs",
|
||||
"zod": "js/zod.mjs"
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ModuleLoader(Rc<RefCell<BaseModuleLoader>>);
|
||||
|
||||
impl ModuleLoader {
|
||||
pub fn new() -> Self {
|
||||
Self(Rc::new(RefCell::new(BaseModuleLoader::new())))
|
||||
}
|
||||
|
||||
pub fn add_module(&self, module: String) {
|
||||
let reference = self.0.borrow_mut();
|
||||
reference.add_module(module);
|
||||
}
|
||||
|
||||
pub fn has_module(&self, module: &str) -> bool {
|
||||
let reference = self.0.borrow();
|
||||
reference.has_module(module)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolver for ModuleLoader {
|
||||
fn resolve<'js>(&mut self, ctx: &Ctx<'js>, base: &str, name: &str) -> rquickjs::Result<String> {
|
||||
let mut inner = self.0.borrow_mut();
|
||||
inner.deref_mut().resolve(ctx, base, name)
|
||||
}
|
||||
}
|
||||
|
||||
impl Loader for ModuleLoader {
|
||||
fn load<'js>(&mut self, ctx: &Ctx<'js>, name: &str) -> rquickjs::Result<Module<'js, Declared>> {
|
||||
let mut inner = self.0.borrow_mut();
|
||||
inner.deref_mut().load(ctx, name)
|
||||
}
|
||||
}
|
||||
|
||||
struct BaseModuleLoader {
|
||||
bundle: Bundle,
|
||||
defined_modules: RefCell<HashSet<String>>,
|
||||
md_loader: MDLoader,
|
||||
}
|
||||
|
||||
impl BaseModuleLoader {
|
||||
pub fn new() -> Self {
|
||||
let mut hs = HashSet::from(["zen".to_string(), "http".to_string()]);
|
||||
|
||||
JS_BUNDLE.iter().for_each(|(key, _)| {
|
||||
hs.insert(key.to_string());
|
||||
});
|
||||
|
||||
Self {
|
||||
bundle: JS_BUNDLE,
|
||||
defined_modules: RefCell::new(hs),
|
||||
md_loader: MDLoader::default()
|
||||
.with_module("zen", ZenModule)
|
||||
.with_module("http", HttpModule),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_module(&self, value: String) {
|
||||
let mut modules = self.defined_modules.borrow_mut();
|
||||
modules.insert(value);
|
||||
}
|
||||
|
||||
pub fn has_module(&self, value: &str) -> bool {
|
||||
let modules = self.defined_modules.borrow();
|
||||
modules.contains(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolver for &mut BaseModuleLoader {
|
||||
fn resolve<'js>(&mut self, ctx: &Ctx<'js>, base: &str, name: &str) -> rquickjs::Result<String> {
|
||||
if let Ok(b) = self.bundle.resolve(ctx, base, name) {
|
||||
return Ok(b);
|
||||
}
|
||||
|
||||
let defined_modules = self.defined_modules.borrow();
|
||||
if defined_modules.contains(name) {
|
||||
return Ok(name.to_string());
|
||||
}
|
||||
|
||||
Err(Error::new_resolving(base, name))
|
||||
}
|
||||
}
|
||||
|
||||
impl Loader for &mut BaseModuleLoader {
|
||||
fn load<'js>(&mut self, ctx: &Ctx<'js>, name: &str) -> rquickjs::Result<Module<'js, Declared>> {
|
||||
self.bundle
|
||||
.load(ctx, name)
|
||||
.or_else(|_| self.md_loader.load(ctx, name))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn export_default<'js, F>(
|
||||
ctx: &Ctx<'js>,
|
||||
exports: &Exports<'js>,
|
||||
f: F,
|
||||
) -> rquickjs::Result<()>
|
||||
where
|
||||
F: FnOnce(&Object<'js>) -> rquickjs::Result<()>,
|
||||
{
|
||||
let default = Object::new(ctx.clone())?;
|
||||
f(&default)?;
|
||||
|
||||
for name in default.keys::<String>() {
|
||||
let name = name?;
|
||||
let value: rquickjs::Value = default.get(&name)?;
|
||||
exports.export(name, value)?;
|
||||
}
|
||||
|
||||
exports.export("default", default)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
|
||||
use rquickjs::module::{Declarations, Exports, ModuleDef};
|
||||
use rquickjs::prelude::{Async, Func, Opt};
|
||||
use rquickjs::{CatchResultExt, Ctx, Function, Object};
|
||||
|
||||
use crate::handler::custom_node_adapter::CustomNodeAdapter;
|
||||
use crate::handler::function::error::{FunctionResult, ResultExt};
|
||||
use crate::handler::function::listener::{RuntimeEvent, RuntimeListener};
|
||||
use crate::handler::function::module::export_default;
|
||||
use crate::handler::function::serde::JsValue;
|
||||
use crate::handler::graph::{DecisionGraph, DecisionGraphConfig};
|
||||
use crate::loader::DecisionLoader;
|
||||
|
||||
pub(crate) struct ZenListener<Loader, Adapter> {
|
||||
pub loader: Arc<Loader>,
|
||||
pub adapter: Arc<Adapter>,
|
||||
}
|
||||
|
||||
impl<Loader: DecisionLoader + 'static, Adapter: CustomNodeAdapter + 'static> RuntimeListener
|
||||
for ZenListener<Loader, Adapter>
|
||||
{
|
||||
fn on_event<'js>(
|
||||
&self,
|
||||
ctx: Ctx<'js>,
|
||||
event: RuntimeEvent,
|
||||
) -> Pin<Box<dyn Future<Output = FunctionResult> + 'js>> {
|
||||
let loader = self.loader.clone();
|
||||
let adapter = self.adapter.clone();
|
||||
|
||||
Box::pin(async move {
|
||||
if event != RuntimeEvent::Startup {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
ctx.globals()
|
||||
.set(
|
||||
"__evaluate",
|
||||
Func::from(Async(
|
||||
move |ctx: Ctx<'js>,
|
||||
key: String,
|
||||
context: JsValue,
|
||||
opts: Opt<Object<'js>>| {
|
||||
let loader = loader.clone();
|
||||
let adapter = adapter.clone();
|
||||
|
||||
async move {
|
||||
let config: Object = ctx.globals().get("config").or_throw(&ctx)?;
|
||||
|
||||
let iteration: u8 = config.get("iteration").or_throw(&ctx)?;
|
||||
let max_depth: u8 = config.get("maxDepth").or_throw(&ctx)?;
|
||||
let trace = opts
|
||||
.0
|
||||
.map(|opt| opt.get::<_, bool>("trace").unwrap_or_default())
|
||||
.unwrap_or_default();
|
||||
|
||||
let load_result = loader.load(key.as_str()).await;
|
||||
let decision_content = load_result.or_throw(&ctx)?;
|
||||
let mut sub_tree = DecisionGraph::try_new(DecisionGraphConfig {
|
||||
content: &decision_content,
|
||||
max_depth,
|
||||
loader,
|
||||
adapter,
|
||||
iteration: iteration + 1,
|
||||
trace,
|
||||
})
|
||||
.or_throw(&ctx)?;
|
||||
|
||||
let response =
|
||||
sub_tree.evaluate(&context.0).await.or_throw(&ctx)?;
|
||||
return rquickjs::Result::Ok(JsValue(
|
||||
serde_json::to_value(response).or_throw(&ctx)?,
|
||||
));
|
||||
}
|
||||
},
|
||||
)),
|
||||
)
|
||||
.catch(&ctx)?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn evaluate_expression<'js>(
|
||||
ctx: Ctx<'js>,
|
||||
expression: String,
|
||||
context: JsValue,
|
||||
) -> rquickjs::Result<JsValue> {
|
||||
let s = zen_expression::evaluate_expression(expression.as_str(), &context.0).or_throw(&ctx)?;
|
||||
|
||||
Ok(JsValue(s))
|
||||
}
|
||||
|
||||
fn evaluate_unary_expression<'js>(
|
||||
ctx: Ctx<'js>,
|
||||
expression: String,
|
||||
context: JsValue,
|
||||
) -> rquickjs::Result<bool> {
|
||||
let s = zen_expression::evaluate_unary_expression(expression.as_str(), &context.0)
|
||||
.or_throw(&ctx)?;
|
||||
|
||||
Ok(s)
|
||||
}
|
||||
|
||||
fn evaluate<'js>(
|
||||
ctx: Ctx<'js>,
|
||||
key: String,
|
||||
context: JsValue,
|
||||
opts: Opt<Object<'js>>,
|
||||
) -> rquickjs::Result<rquickjs::Value<'js>> {
|
||||
let s: Function = ctx.globals().get("__evaluate").or_throw(&ctx)?;
|
||||
let result: rquickjs::Value = s.call((key, context, opts)).or_throw(&ctx)?;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub struct ZenModule;
|
||||
|
||||
impl ModuleDef for ZenModule {
|
||||
fn declare<'js>(decl: &Declarations<'js>) -> rquickjs::Result<()> {
|
||||
decl.declare("evaluateExpression")?;
|
||||
decl.declare("evaluateUnaryExpression")?;
|
||||
decl.declare("evaluate")?;
|
||||
|
||||
decl.declare("default")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn evaluate<'js>(ctx: &Ctx<'js>, exports: &Exports<'js>) -> rquickjs::Result<()> {
|
||||
export_default(ctx, exports, |default| {
|
||||
default.set("evaluateExpression", Func::from(evaluate_expression))?;
|
||||
default.set(
|
||||
"evaluateUnaryExpression",
|
||||
Func::from(evaluate_unary_expression),
|
||||
)?;
|
||||
default.set("evaluate", Func::from(evaluate))?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
use anyhow::Context;
|
||||
use rquickjs::loader::Bundle;
|
||||
use rquickjs::{embed, Runtime};
|
||||
|
||||
static JS_BUNDLE: Bundle = embed! {
|
||||
"dayjs": "js/dayjs.js",
|
||||
"big": "js/big.js",
|
||||
"internals": "js/internals.js"
|
||||
};
|
||||
|
||||
pub(crate) fn create_runtime() -> anyhow::Result<Runtime> {
|
||||
let runtime = Runtime::new().context("Failed to create runtime")?;
|
||||
runtime.set_loader(JS_BUNDLE, JS_BUNDLE);
|
||||
|
||||
Ok(runtime)
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
use std::fmt::Debug;
|
||||
|
||||
use anyhow::Context as _;
|
||||
use rquickjs::{Context, Ctx, Error as QError, FromJs, Runtime};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::handler::function::js_value::JsValue;
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct EvaluateResponse {
|
||||
pub output: Value,
|
||||
pub log: Vec<Value>,
|
||||
}
|
||||
|
||||
pub struct Script {
|
||||
runtime: Runtime,
|
||||
}
|
||||
|
||||
impl Script {
|
||||
pub fn new(runtime: Runtime) -> Self {
|
||||
Self { runtime }
|
||||
}
|
||||
|
||||
pub async fn call<P>(&mut self, source: &str, args: &P) -> anyhow::Result<EvaluateResponse>
|
||||
where
|
||||
P: Serialize,
|
||||
{
|
||||
let runtime = &self.runtime;
|
||||
let context = Context::full(&runtime).context("Failed to create context")?;
|
||||
|
||||
let args_str =
|
||||
serde_json::to_string(args).context("Failed to serialize function arguments")?;
|
||||
|
||||
let json_response = context.with(|ctx| -> anyhow::Result<String> {
|
||||
let _ = ctx
|
||||
.clone()
|
||||
.compile("main", "import 'internals'; globalThis.now = Date.now();")
|
||||
.map_err(|e| map_js_error(&ctx, e))?;
|
||||
|
||||
let _ = ctx
|
||||
.globals()
|
||||
.set("log", Vec::<isize>::new())
|
||||
.map_err(|e| map_js_error(&ctx, e))?;
|
||||
|
||||
ctx.eval::<String, _>(format!("{source};main({args_str})"))
|
||||
.map_err(|e| map_js_error(&ctx, e))
|
||||
})?;
|
||||
|
||||
serde_json::from_str(json_response.as_str()).context("Failed to parse function result")
|
||||
}
|
||||
}
|
||||
|
||||
fn map_js_error(ctx: &Ctx, e: QError) -> anyhow::Error {
|
||||
let error = JsValue::from_js(&ctx, ctx.catch())
|
||||
.map(|v| v.0)
|
||||
.unwrap_or(Value::String(e.to_string()));
|
||||
|
||||
anyhow::Error::msg(error.to_string())
|
||||
}
|
||||
+51
-18
@@ -1,5 +1,6 @@
|
||||
use crate::handler::function::error::ResultExt;
|
||||
use itertools::Itertools;
|
||||
use rquickjs::{Ctx, Error as QError, FromJs, Type, Value as QValue};
|
||||
use rquickjs::{Ctx, FromJs, IntoAtom, IntoJs, Type, Value as QValue};
|
||||
use serde_json::{json, Map, Number, Value};
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -9,50 +10,46 @@ impl<'js> FromJs<'js> for JsValue {
|
||||
fn from_js(ctx: &Ctx<'js>, v: QValue<'js>) -> rquickjs::Result<Self> {
|
||||
let computed_value = match v.type_of() {
|
||||
Type::Uninitialized | Type::Undefined | Type::Null => Value::Null,
|
||||
Type::Bool => Value::Bool(
|
||||
v.as_bool()
|
||||
.ok_or_else(|| QError::new_from_js("bool", "bool"))?,
|
||||
),
|
||||
Type::Bool => Value::Bool(v.as_bool().or_throw_msg(ctx, "failed to convert to bool")?),
|
||||
Type::Int => Value::Number(Number::from(
|
||||
v.as_int()
|
||||
.ok_or_else(|| QError::new_from_js("int", "number"))?,
|
||||
v.as_int().or_throw_msg(ctx, "failed to convert to int")?,
|
||||
)),
|
||||
Type::BigInt => Value::Number(Number::from(
|
||||
v.as_big_int()
|
||||
.map(|b| b.clone().to_i64().ok())
|
||||
.flatten()
|
||||
.ok_or_else(|| QError::new_from_js("bigint", "number"))?,
|
||||
.or_throw_msg(ctx, "failed to convert to number")?,
|
||||
)),
|
||||
Type::Float => Value::Number(
|
||||
v.as_float()
|
||||
.map(|n| Number::from_f64(n))
|
||||
.flatten()
|
||||
.ok_or_else(|| QError::new_from_js("float", "number"))?,
|
||||
.or_throw_msg(ctx, "failed to convert to number")?,
|
||||
),
|
||||
Type::String => Value::String(
|
||||
v.as_string()
|
||||
.map(|s| s.to_string().ok())
|
||||
.flatten()
|
||||
.ok_or_else(|| QError::new_from_js("string", "string"))?,
|
||||
.or_throw_msg(ctx, "failed to convert to string")?,
|
||||
),
|
||||
Type::Array => {
|
||||
let arr = v
|
||||
.as_array()
|
||||
.ok_or_else(|| QError::new_from_js("array", "array"))?;
|
||||
.or_throw_msg(ctx, "failed to convert to array")?;
|
||||
|
||||
let js_arr: Vec<Value> = arr
|
||||
.iter::<QValue>()
|
||||
.map_ok(|n| JsValue::from_js(ctx, n.clone()).map(|js_val| js_val.0))
|
||||
.flatten()
|
||||
.try_collect()
|
||||
.map_err(|_| QError::new_from_js("array", "array"))?;
|
||||
.or_throw(ctx)?;
|
||||
|
||||
Value::Array(js_arr)
|
||||
}
|
||||
Type::Object => {
|
||||
let object = v
|
||||
.as_object()
|
||||
.ok_or_else(|| QError::new_from_js("object", "object"))?;
|
||||
.or_throw_msg(ctx, "failed to convert to object")?;
|
||||
|
||||
let js_object: Map<String, Value> = object
|
||||
.props::<String, QValue>()
|
||||
@@ -61,18 +58,16 @@ impl<'js> FromJs<'js> for JsValue {
|
||||
})
|
||||
.flatten()
|
||||
.try_collect()
|
||||
.map_err(|_| QError::new_from_js("object", "object"))?;
|
||||
.or_throw(ctx)?;
|
||||
|
||||
Value::Object(js_object)
|
||||
}
|
||||
Type::Exception => {
|
||||
let exception = v
|
||||
.as_exception()
|
||||
.ok_or_else(|| QError::new_from_js("exception", "object"))?;
|
||||
.or_throw_msg(ctx, "failed to convert to exception")?;
|
||||
|
||||
let message = exception
|
||||
.message()
|
||||
.ok_or_else(|| QError::new_from_js("exception", "object"))?;
|
||||
let message = exception.message().unwrap_or_default();
|
||||
let description = exception.to_string();
|
||||
|
||||
json!({ "message": message, "description": description })
|
||||
@@ -82,8 +77,46 @@ impl<'js> FromJs<'js> for JsValue {
|
||||
Type::Constructor => json!("[Constructor]"),
|
||||
Type::Symbol => json!("[Symbol]"),
|
||||
Type::Unknown => json!("[Unknown]"),
|
||||
Type::Promise => {
|
||||
let promise = v.as_promise().or_throw(ctx)?;
|
||||
let val: JsValue = promise.finish()?;
|
||||
val.0
|
||||
}
|
||||
};
|
||||
|
||||
Ok(JsValue(computed_value))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'js> IntoJs<'js> for JsValue {
|
||||
fn into_js(self, ctx: &Ctx<'js>) -> rquickjs::Result<QValue<'js>> {
|
||||
let res = match self.0 {
|
||||
Value::Null => QValue::new_null(ctx.clone()),
|
||||
Value::Bool(b) => QValue::new_bool(ctx.clone(), b),
|
||||
Value::Number(n) => QValue::new_number(
|
||||
ctx.clone(),
|
||||
n.as_f64()
|
||||
.or_throw_msg(ctx, "failed to convert float to number")?,
|
||||
),
|
||||
Value::String(str) => str.into_js(ctx)?,
|
||||
Value::Array(arr) => {
|
||||
let qarr = rquickjs::Array::new(ctx.clone())?;
|
||||
for (idx, item) in arr.into_iter().enumerate() {
|
||||
qarr.set(idx, JsValue(item))?;
|
||||
}
|
||||
|
||||
qarr.into_value()
|
||||
}
|
||||
Value::Object(map) => {
|
||||
let qmap = rquickjs::Object::new(ctx.clone())?;
|
||||
for (key, value) in map.into_iter() {
|
||||
qmap.set(key.into_atom(ctx)?, JsValue(value))?;
|
||||
}
|
||||
|
||||
qmap.into_value()
|
||||
}
|
||||
};
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user