From cc3d938b2f21bda6b66e7c38b0cfc34df239a5c9 Mon Sep 17 00:00:00 2001 From: stefan-gorules <127550877+stefan-gorules@users.noreply.github.com> Date: Tue, 16 Jul 2024 11:22:51 +0200 Subject: [PATCH] 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 --- .github/workflows/rust.yaml | 2 +- Cargo.toml | 4 +- bindings/c/Cargo.toml | 3 +- bindings/c/src/decision.rs | 6 +- bindings/c/src/engine.rs | 14 +- bindings/c/src/languages/go.rs | 24 +- bindings/c/src/languages/native.rs | 16 +- bindings/c/src/lib.rs | 1 + bindings/c/src/loader.rs | 17 +- bindings/c/src/mt.rs | 18 + bindings/nodejs/Cargo.toml | 9 +- bindings/nodejs/src/decision.rs | 23 +- bindings/nodejs/src/engine.rs | 44 +- bindings/nodejs/src/lib.rs | 1 + bindings/nodejs/src/loader.rs | 15 +- bindings/nodejs/src/mt.rs | 26 + bindings/python/Cargo.toml | 6 +- bindings/python/src/decision.rs | 57 +- bindings/python/src/engine.rs | 82 +- bindings/python/src/lib.rs | 1 + bindings/python/src/loader.rs | 29 +- bindings/python/src/mt.rs | 48 + core/engine/Cargo.toml | 11 +- core/engine/README.md | 37 +- core/engine/benches/engine.rs | 11 +- core/engine/js/big.mjs | 8 + core/engine/js/dayjs.mjs | 8 + core/engine/js/{ => v1}/big.js | 0 core/engine/js/{ => v1}/dayjs.js | 0 core/engine/js/{ => v1}/internals.js | 0 core/engine/js/zod.mjs | 2458 +++++++++++++++++ core/engine/src/decision.rs | 14 +- core/engine/src/engine.rs | 6 +- core/engine/src/handler/decision.rs | 86 +- core/engine/src/handler/function/error.rs | 63 + core/engine/src/handler/function/function.rs | 125 + core/engine/src/handler/function/listener.rs | 20 + core/engine/src/handler/function/mod.rs | 98 +- .../src/handler/function/module/console.rs | 104 + .../src/handler/function/module/http.rs | 241 ++ .../engine/src/handler/function/module/mod.rs | 133 + .../engine/src/handler/function/module/zen.rs | 144 + .../function/{js_value.rs => serde.rs} | 69 +- core/engine/src/handler/function_v1/mod.rs | 49 + .../{function => function_v1}/runtime.rs | 6 +- .../{function => function_v1}/script.rs | 17 +- core/engine/src/handler/graph.rs | 72 +- core/engine/src/handler/mod.rs | 1 + core/engine/src/lib.rs | 8 +- core/engine/src/loader/cached.rs | 37 + core/engine/src/loader/closure.rs | 13 +- core/engine/src/loader/filesystem.rs | 25 +- core/engine/src/loader/memory.rs | 16 +- core/engine/src/loader/mod.rs | 24 +- core/engine/src/loader/noop.rs | 19 +- core/engine/src/model/mod.rs | 18 +- core/engine/tests/decision.rs | 12 +- core/engine/tests/engine.rs | 73 +- 58 files changed, 4093 insertions(+), 379 deletions(-) create mode 100644 bindings/c/src/mt.rs create mode 100644 bindings/nodejs/src/mt.rs create mode 100644 bindings/python/src/mt.rs create mode 100644 core/engine/js/big.mjs create mode 100644 core/engine/js/dayjs.mjs rename core/engine/js/{ => v1}/big.js (100%) rename core/engine/js/{ => v1}/dayjs.js (100%) rename core/engine/js/{ => v1}/internals.js (100%) create mode 100644 core/engine/js/zod.mjs create mode 100644 core/engine/src/handler/function/error.rs create mode 100644 core/engine/src/handler/function/function.rs create mode 100644 core/engine/src/handler/function/listener.rs create mode 100644 core/engine/src/handler/function/module/console.rs create mode 100644 core/engine/src/handler/function/module/http.rs create mode 100644 core/engine/src/handler/function/module/mod.rs create mode 100644 core/engine/src/handler/function/module/zen.rs rename core/engine/src/handler/function/{js_value.rs => serde.rs} (52%) create mode 100644 core/engine/src/handler/function_v1/mod.rs rename core/engine/src/handler/{function => function_v1}/runtime.rs (76%) rename core/engine/src/handler/{function => function_v1}/script.rs (80%) create mode 100644 core/engine/src/loader/cached.rs diff --git a/.github/workflows/rust.yaml b/.github/workflows/rust.yaml index f337f5f8..0c5e9bc6 100644 --- a/.github/workflows/rust.yaml +++ b/.github/workflows/rust.yaml @@ -86,7 +86,7 @@ jobs: steps: - uses: actions/checkout@v3 - name: Install Rust - run: rustup toolchain install nightly-2024-02-11 --component miri && rustup default nightly-2024-02-11 + run: rustup toolchain install nightly-2024-07-13 --component miri && rustup default nightly-2024-07-13 - run: cargo miri test --workspace --all-features env: MIRIFLAGS: -Zmiri-strict-provenance -Zmiri-symbolic-alignment-check -Zmiri-disable-isolation diff --git a/Cargo.toml b/Cargo.toml index 175b4257..eb6581d6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,14 +7,14 @@ members = [ [workspace.dependencies] ahash = "0.8.6" -async-trait = "0.1.77" bincode = "2.0.0-rc.3" bumpalo = "3.14.0" chrono = "0.4.31" criterion = "0.5.1" fastrand = "2.0.1" -futures = "0.3.30" humantime = "2.1.0" +tokio = "1.38.0" +tokio-util = "0.7.11" once_cell = "1.19.0" petgraph = "0.6.4" phf = "0.11.2" diff --git a/bindings/c/Cargo.toml b/bindings/c/Cargo.toml index e982cc3d..342d4a8b 100644 --- a/bindings/c/Cargo.toml +++ b/bindings/c/Cargo.toml @@ -6,13 +6,12 @@ license = "MIT" publish = false [dependencies] -async-trait = { workspace = true } anyhow = { workspace = true } libc = "0.2.155" serde = { workspace = true } serde_json = { workspace = true } strum = { workspace = true, features = ["derive"] } -futures = "0.3.30" +tokio = { workspace = true, features = ["rt"] } zen-engine = { path = "../../core/engine" } zen-expression = { path = "../../core/expression" } zen-tmpl = { path = "../../core/template" } diff --git a/bindings/c/src/decision.rs b/bindings/c/src/decision.rs index 7d4fc513..7f685ada 100644 --- a/bindings/c/src/decision.rs +++ b/bindings/c/src/decision.rs @@ -2,14 +2,13 @@ use std::ffi::{c_char, c_void, CStr, CString}; use std::marker::{PhantomData, PhantomPinned}; use std::ops::{Deref, DerefMut}; -use futures::executor::block_on; - use zen_engine::Decision; use crate::custom_node::DynamicCustomNode; use crate::engine::ZenEngineEvaluationOptions; use crate::error::ZenError; use crate::loader::DynamicDecisionLoader; +use crate::mt::tokio_runtime; use crate::result::ZenResult; #[repr(C)] @@ -75,7 +74,8 @@ pub extern "C" fn zen_decision_evaluate( }; let zen_decision = unsafe { &*(decision as *mut ZenDecision) }; - let maybe_result = block_on(zen_decision.evaluate_with_opts(&context, options.into())); + let maybe_result = + tokio_runtime().block_on(zen_decision.evaluate_with_opts(&context, options.into())); let result = match maybe_result { Ok(r) => r, Err(e) => return ZenResult::from(&e), diff --git a/bindings/c/src/engine.rs b/bindings/c/src/engine.rs index 03de90b8..5678f14e 100644 --- a/bindings/c/src/engine.rs +++ b/bindings/c/src/engine.rs @@ -3,15 +3,14 @@ use std::marker::{PhantomData, PhantomPinned}; use std::ops::{Deref, DerefMut}; use std::sync::Arc; -use futures::executor::block_on; - -use crate::custom_node::DynamicCustomNode; use zen_engine::{DecisionEngine, EvaluationOptions}; +use crate::custom_node::DynamicCustomNode; use crate::decision::{ZenDecision, ZenDecisionStruct}; use crate::error::ZenError; use crate::helper::safe_str_from_ptr; use crate::loader::DynamicDecisionLoader; +use crate::mt::tokio_runtime; use crate::result::ZenResult; pub(crate) struct ZenEngine(DecisionEngine); @@ -128,8 +127,11 @@ pub extern "C" fn zen_engine_evaluate( let zen_engine = unsafe { &*(engine as *mut ZenEngine) }; - let maybe_result = - block_on(zen_engine.evaluate_with_opts(str_key, &val_context, options.into())); + let maybe_result = tokio_runtime().block_on(zen_engine.evaluate_with_opts( + str_key, + &val_context, + options.into(), + )); let result = match maybe_result { Ok(r) => r, Err(e) => return ZenResult::from(&e), @@ -160,7 +162,7 @@ pub extern "C" fn zen_engine_get_decision( }; let zen_engine = unsafe { &*(engine as *mut ZenEngine) }; - let decision = match block_on(zen_engine.get_decision(str_key)) { + let decision = match tokio_runtime().block_on(zen_engine.get_decision(str_key)) { Ok(d) => d, Err(e) => return ZenResult::from(&e), }; diff --git a/bindings/c/src/languages/go.rs b/bindings/c/src/languages/go.rs index 3b057fc0..8e5ecd8e 100644 --- a/bindings/c/src/languages/go.rs +++ b/bindings/c/src/languages/go.rs @@ -1,7 +1,6 @@ -use std::ffi::{c_char, CString}; - use anyhow::anyhow; -use async_trait::async_trait; +use std::ffi::{c_char, CString}; +use std::future::Future; use zen_engine::handler::custom_node_adapter::{CustomNodeAdapter, CustomNodeRequest}; use zen_engine::handler::node::NodeResult; @@ -22,18 +21,19 @@ impl GoDecisionLoader { } } -#[async_trait] impl DecisionLoader for GoDecisionLoader { - async fn load(&self, key: &str) -> LoaderResponse { - let Some(handler) = &self.handler else { - return Err(LoaderError::NotFound(key.to_string()).into()); - }; + fn load<'a>(&'a self, key: &'a str) -> impl Future + 'a { + async move { + let Some(handler) = &self.handler else { + return Err(LoaderError::NotFound(key.to_string()).into()); + }; - let c_key = CString::new(key).unwrap(); - let c_content_ptr = - unsafe { zen_engine_go_loader_callback(handler.clone(), c_key.as_ptr()) }; + let c_key = CString::new(key).unwrap(); + let c_content_ptr = + unsafe { zen_engine_go_loader_callback(handler.clone(), c_key.as_ptr()) }; - c_content_ptr.into_loader_response(key) + c_content_ptr.into_loader_response(key) + } } } diff --git a/bindings/c/src/languages/native.rs b/bindings/c/src/languages/native.rs index e854fdc5..9c9dafad 100644 --- a/bindings/c/src/languages/native.rs +++ b/bindings/c/src/languages/native.rs @@ -1,7 +1,6 @@ -use std::ffi::{c_char, CString}; - use anyhow::anyhow; -use async_trait::async_trait; +use std::ffi::{c_char, CString}; +use std::future::Future; use zen_engine::handler::custom_node_adapter::{CustomNodeAdapter, CustomNodeRequest}; use zen_engine::handler::node::NodeResult; @@ -27,13 +26,14 @@ impl NativeDecisionLoader { } } -#[async_trait] impl DecisionLoader for NativeDecisionLoader { - async fn load(&self, key: &str) -> LoaderResponse { - let c_key = CString::new(key).unwrap(); - let c_content_ptr = (&self.callback)(c_key.as_ptr()); + fn load<'a>(&'a self, key: &'a str) -> impl Future + 'a { + async move { + let c_key = CString::new(key).unwrap(); + let c_content_ptr = (&self.callback)(c_key.as_ptr()); - c_content_ptr.into_loader_response(key) + c_content_ptr.into_loader_response(key) + } } } diff --git a/bindings/c/src/lib.rs b/bindings/c/src/lib.rs index 013b08b5..f3b36004 100644 --- a/bindings/c/src/lib.rs +++ b/bindings/c/src/lib.rs @@ -8,4 +8,5 @@ mod expression; mod helper; mod languages; mod loader; +mod mt; mod result; diff --git a/bindings/c/src/loader.rs b/bindings/c/src/loader.rs index 623bcc7e..0ca36dde 100644 --- a/bindings/c/src/loader.rs +++ b/bindings/c/src/loader.rs @@ -1,8 +1,8 @@ use std::ffi::{c_char, CString}; +use std::future::Future; use std::sync::Arc; use anyhow::anyhow; -use async_trait::async_trait; use zen_engine::loader::{DecisionLoader, LoaderError, LoaderResponse, NoopLoader}; use zen_engine::model::DecisionContent; @@ -23,14 +23,15 @@ impl Default for DynamicDecisionLoader { } } -#[async_trait] impl DecisionLoader for DynamicDecisionLoader { - async fn load(&self, key: &str) -> LoaderResponse { - match self { - DynamicDecisionLoader::Noop(loader) => loader.load(key).await, - DynamicDecisionLoader::Native(loader) => loader.load(key).await, - #[cfg(feature = "go")] - DynamicDecisionLoader::Go(loader) => loader.load(key).await, + fn load<'a>(&'a self, key: &'a str) -> impl Future + 'a { + async move { + match self { + DynamicDecisionLoader::Noop(loader) => loader.load(key).await, + DynamicDecisionLoader::Native(loader) => loader.load(key).await, + #[cfg(feature = "go")] + DynamicDecisionLoader::Go(loader) => loader.load(key).await, + } } } } diff --git a/bindings/c/src/mt.rs b/bindings/c/src/mt.rs new file mode 100644 index 00000000..950607cd --- /dev/null +++ b/bindings/c/src/mt.rs @@ -0,0 +1,18 @@ +use std::sync::{Arc, OnceLock}; + +use tokio::runtime; +use tokio::runtime::Runtime; + +pub(crate) fn tokio_runtime() -> Arc { + static RUNTIME: OnceLock> = OnceLock::new(); + RUNTIME + .get_or_init(|| { + Arc::new( + runtime::Builder::new_current_thread() + .enable_all() + .build() + .expect("Failed to build tokio runtime"), + ) + }) + .clone() +} diff --git a/bindings/nodejs/Cargo.toml b/bindings/nodejs/Cargo.toml index fb75d66a..40f24c8d 100644 --- a/bindings/nodejs/Cargo.toml +++ b/bindings/nodejs/Cargo.toml @@ -9,11 +9,10 @@ publish = false crate-type = ["cdylib"] [dependencies] -async-trait = { workspace = true } -napi = { version = "2.16.2", features = ["serde-json", "error_anyhow", "tokio_rt"] } -napi-derive = "2.16.2" +napi = { version = "2.16", features = ["serde-json", "error_anyhow", "tokio_rt"] } +napi-derive = "2.16" +tokio-util = { workspace = true, features = ["rt"] } serde_json = { workspace = true } -futures = { workspace = true } zen-engine = { path = "../../core/engine" } zen-expression = { path = "../../core/expression" } zen-tmpl = { path = "../../core/template" } @@ -21,4 +20,4 @@ serde = { workspace = true, features = ["derive"] } json_dotpath = { workspace = true } [build-dependencies] -napi-build = "2.1.2" \ No newline at end of file +napi-build = "2.1" \ No newline at end of file diff --git a/bindings/nodejs/src/decision.rs b/bindings/nodejs/src/decision.rs index a5028658..d9cf4ff6 100644 --- a/bindings/nodejs/src/decision.rs +++ b/bindings/nodejs/src/decision.rs @@ -1,10 +1,10 @@ use crate::custom_node::CustomNode; use crate::engine::ZenEvaluateOptions; use crate::loader::DecisionLoader; +use crate::mt::spawn_worker; use crate::safe_result::SafeResult; use crate::types::ZenEngineResponse; use napi::anyhow::anyhow; -use napi::tokio; use napi_derive::napi; use serde_json::Value; use std::sync::Arc; @@ -33,15 +33,20 @@ impl ZenDecision { opts: Option, ) -> napi::Result { let decision = self.0.clone(); - let result = tokio::spawn(async move { + let result = spawn_worker(move || { let options = opts.unwrap_or_default(); - futures::executor::block_on(decision.evaluate_with_opts( - &context, - EvaluationOptions { - max_depth: options.max_depth, - trace: options.trace, - }, - )) + + async move { + decision + .evaluate_with_opts( + &context, + EvaluationOptions { + max_depth: options.max_depth, + trace: options.trace, + }, + ) + .await + } }) .await .map_err(|_| anyhow!("Hook timed out"))? diff --git a/bindings/nodejs/src/engine.rs b/bindings/nodejs/src/engine.rs index 3011b102..78452b38 100644 --- a/bindings/nodejs/src/engine.rs +++ b/bindings/nodejs/src/engine.rs @@ -1,18 +1,22 @@ +use std::sync::Arc; + +use napi::anyhow::{anyhow, Context}; +use napi::bindgen_prelude::{Buffer, Either3}; +use napi::threadsafe_function::{ErrorStrategy, ThreadSafeCallContext, ThreadsafeFunction}; +use napi::{Env, JsFunction, JsObject}; +use napi_derive::napi; +use serde_json::Value; + +use zen_engine::model::DecisionContent; +use zen_engine::{DecisionEngine, EvaluationOptions}; + use crate::content::ZenDecisionContent; use crate::custom_node::CustomNode; use crate::decision::ZenDecision; use crate::loader::DecisionLoader; +use crate::mt::spawn_worker; use crate::safe_result::SafeResult; use crate::types::{ZenEngineHandlerRequest, ZenEngineResponse}; -use napi::anyhow::{anyhow, Context}; -use napi::bindgen_prelude::{Buffer, Either3}; -use napi::threadsafe_function::{ErrorStrategy, ThreadSafeCallContext, ThreadsafeFunction}; -use napi::{tokio, Env, JsFunction, JsObject}; -use napi_derive::napi; -use serde_json::Value; -use std::sync::Arc; -use zen_engine::model::DecisionContent; -use zen_engine::{DecisionEngine, EvaluationOptions}; #[napi] pub struct ZenEngine { @@ -107,17 +111,21 @@ impl ZenEngine { opts: Option, ) -> napi::Result { let graph = self.graph.clone(); - let result = tokio::spawn(async move { + let result = spawn_worker(|| { let options = opts.unwrap_or_default(); - futures::executor::block_on(graph.evaluate_with_opts( - key, - &context, - EvaluationOptions { - max_depth: options.max_depth, - trace: options.trace, - }, - )) + async move { + graph + .evaluate_with_opts( + key, + &context, + EvaluationOptions { + max_depth: options.max_depth, + trace: options.trace, + }, + ) + .await + } }) .await .map_err(|_| anyhow!("Hook timed out"))? diff --git a/bindings/nodejs/src/lib.rs b/bindings/nodejs/src/lib.rs index 24db7d08..e79e0972 100644 --- a/bindings/nodejs/src/lib.rs +++ b/bindings/nodejs/src/lib.rs @@ -5,5 +5,6 @@ mod decision; mod engine; mod expression; mod loader; +mod mt; mod safe_result; mod types; diff --git a/bindings/nodejs/src/loader.rs b/bindings/nodejs/src/loader.rs index d39875f5..59156ea0 100644 --- a/bindings/nodejs/src/loader.rs +++ b/bindings/nodejs/src/loader.rs @@ -1,12 +1,14 @@ +use std::future::Future; use std::sync::Arc; -use async_trait::async_trait; use napi::anyhow::anyhow; use napi::bindgen_prelude::{Buffer, Promise}; use napi::threadsafe_function::{ErrorStrategy, ThreadsafeFunction}; use napi::Either; -use zen_engine::loader::{DecisionLoader as DecisionLoaderTrait, LoaderError, LoaderResult}; +use zen_engine::loader::{ + DecisionLoader as DecisionLoaderTrait, LoaderError, LoaderResponse, LoaderResult, +}; use zen_engine::model::DecisionContent; use crate::content::ZenDecisionContent; @@ -64,10 +66,11 @@ impl DecisionLoader { } } -#[async_trait] impl DecisionLoaderTrait for DecisionLoader { - async fn load(&self, key: &str) -> LoaderResult> { - let decision_content = self.get_key(key).await?; - Ok(decision_content) + fn load<'a>(&'a self, key: &'a str) -> impl Future + 'a { + async move { + let decision_content = self.get_key(key).await?; + Ok(decision_content) + } } } diff --git a/bindings/nodejs/src/mt.rs b/bindings/nodejs/src/mt.rs new file mode 100644 index 00000000..656210dc --- /dev/null +++ b/bindings/nodejs/src/mt.rs @@ -0,0 +1,26 @@ +use napi::tokio::task::JoinHandle; +use std::future::Future; +use std::sync::OnceLock; +use std::thread::available_parallelism; +use tokio_util::task::LocalPoolHandle; + +fn parallelism() -> usize { + available_parallelism().map(Into::into).unwrap_or(1) +} + +pub(crate) fn worker_pool() -> LocalPoolHandle { + static LOCAL_POOL: OnceLock = OnceLock::new(); + LOCAL_POOL + .get_or_init(|| LocalPoolHandle::new(parallelism())) + .clone() +} + +pub(crate) fn spawn_worker(create_task: F) -> JoinHandle +where + F: FnOnce() -> Fut, + F: Send + 'static, + Fut: Future + 'static, + Fut::Output: Send + 'static, +{ + worker_pool().spawn_pinned(create_task) +} diff --git a/bindings/python/Cargo.toml b/bindings/python/Cargo.toml index 01e1e8af..9843f600 100644 --- a/bindings/python/Cargo.toml +++ b/bindings/python/Cargo.toml @@ -10,15 +10,15 @@ name = "zen" crate-type = ["cdylib"] [dependencies] -async-trait = { workspace = true } anyhow = { workspace = true } pyo3 = { version = "0.20", features = ["anyhow", "serde"] } +pyo3-asyncio = { version = "0.20", features = ["tokio-runtime"] } pythonize = "0.20" json_dotpath = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } -futures = { workspace = true } +tokio = { workspace = true, features = ["rt"] } +tokio-util = { workspace = true, features = ["rt"] } zen-engine = { path = "../../core/engine" } zen-expression = { path = "../../core/expression" } zen-tmpl = { path = "../../core/template" } -pyo3-asyncio = { version = "0.20.0", features = ["tokio-runtime"] } diff --git a/bindings/python/src/decision.rs b/bindings/python/src/decision.rs index 3328e346..9be366f3 100644 --- a/bindings/python/src/decision.rs +++ b/bindings/python/src/decision.rs @@ -3,6 +3,7 @@ use std::sync::Arc; use anyhow::{anyhow, Context}; use pyo3::types::PyDict; use pyo3::{pyclass, pymethods, PyAny, PyObject, PyResult, Python, ToPyObject}; +use pyo3_asyncio::tokio; use pythonize::depythonize; use zen_engine::{Decision, EvaluationOptions}; @@ -10,6 +11,7 @@ use zen_engine::{Decision, EvaluationOptions}; use crate::custom_node::PyCustomNode; use crate::engine::PyZenEvaluateOptions; use crate::loader::PyDecisionLoader; +use crate::mt::{spawn_worker, spawn_worker_blocking}; use crate::value::PyValue; #[pyclass] @@ -33,15 +35,19 @@ impl PyZenDecision { }; let decision = self.0.clone(); - let result = futures::executor::block_on(decision.evaluate_with_opts( - &context, - EvaluationOptions { - max_depth: options.max_depth, - trace: options.trace, - }, - )) - .map_err(|e| { - anyhow!(serde_json::to_string(e.as_ref()).unwrap_or_else(|_| e.to_string())) + let result = spawn_worker_blocking(move || async move { + decision + .evaluate_with_opts( + &context, + EvaluationOptions { + max_depth: options.max_depth, + trace: options.trace, + }, + ) + .await + .map_err(|e| { + anyhow!(serde_json::to_string(e.as_ref()).unwrap_or_else(|_| e.to_string())) + }) })?; let value = serde_json::to_value(&result).context("Fail")?; @@ -62,22 +68,27 @@ impl PyZenDecision { }; let decision = self.0.clone(); - pyo3_asyncio::tokio::future_into_py(py, async move { - let result = futures::executor::block_on(decision.evaluate_with_opts( - &context, - EvaluationOptions { - max_depth: options.max_depth, - trace: options.trace, - }, - )) - .map_err(|e| { - anyhow!(serde_json::to_string(e.as_ref()).unwrap_or_else(|_| e.to_string())) - })?; - let value = serde_json::to_value(result).context("Failed to serialize result")?; + tokio::future_into_py( + py, + spawn_worker(move || async move { + let result = decision + .evaluate_with_opts( + &context, + EvaluationOptions { + max_depth: options.max_depth, + trace: options.trace, + }, + ) + .await + .map_err(|e| { + anyhow!(serde_json::to_string(e.as_ref()).unwrap_or_else(|_| e.to_string())) + })?; - Python::with_gil(|py| Ok(PyValue(value).to_object(py))) - }) + let value = serde_json::to_value(result).context("Failed to serialize result")?; + Python::with_gil(|py| Ok(PyValue(value).to_object(py))) + }), + ) } pub fn validate(&self) -> PyResult<()> { diff --git a/bindings/python/src/engine.rs b/bindings/python/src/engine.rs index 2f94657f..5f923a5f 100644 --- a/bindings/python/src/engine.rs +++ b/bindings/python/src/engine.rs @@ -1,16 +1,20 @@ -use crate::custom_node::PyCustomNode; -use crate::decision::PyZenDecision; -use crate::loader::PyDecisionLoader; -use crate::value::PyValue; +use std::sync::Arc; + use anyhow::{anyhow, Context}; use pyo3::types::PyDict; use pyo3::{pyclass, pymethods, PyAny, PyObject, PyResult, Python, ToPyObject}; use pythonize::depythonize; use serde::{Deserialize, Serialize}; -use std::sync::Arc; + use zen_engine::model::DecisionContent; use zen_engine::{DecisionEngine, EvaluationOptions}; +use crate::custom_node::PyCustomNode; +use crate::decision::PyZenDecision; +use crate::loader::PyDecisionLoader; +use crate::mt::{spawn_worker, spawn_worker_blocking}; +use crate::value::PyValue; + #[pyclass] #[pyo3(name = "ZenEngine")] pub struct PyZenEngine { @@ -86,16 +90,20 @@ impl PyZenEngine { }; let graph = self.graph.clone(); - let result = futures::executor::block_on(graph.evaluate_with_opts( - key, - &context, - EvaluationOptions { - max_depth: options.max_depth, - trace: options.trace, - }, - )) - .map_err(|e| { - anyhow!(serde_json::to_string(e.as_ref()).unwrap_or_else(|_| e.to_string())) + let result = spawn_worker_blocking(move || async move { + graph + .evaluate_with_opts( + key, + &context, + EvaluationOptions { + max_depth: options.max_depth, + trace: options.trace, + }, + ) + .await + .map_err(|e| { + anyhow!(serde_json::to_string(e.as_ref()).unwrap_or_else(|_| e.to_string())) + }) })?; let value = serde_json::to_value(&result).context("Failed to serialize result")?; @@ -117,23 +125,28 @@ impl PyZenEngine { }; let graph = self.graph.clone(); - pyo3_asyncio::tokio::future_into_py(py, async move { - let result = futures::executor::block_on(graph.evaluate_with_opts( - key, - &context, - EvaluationOptions { - max_depth: options.max_depth, - trace: options.trace, - }, - )) - .map_err(|e| { - anyhow!(serde_json::to_string(e.as_ref()).unwrap_or_else(|_| e.to_string())) - })?; + pyo3_asyncio::tokio::future_into_py( + py, + spawn_worker(move || async move { + let result = graph + .evaluate_with_opts( + key, + &context, + EvaluationOptions { + max_depth: options.max_depth, + trace: options.trace, + }, + ) + .await + .map_err(|e| { + anyhow!(serde_json::to_string(e.as_ref()).unwrap_or_else(|_| e.to_string())) + })?; - let value = serde_json::to_value(result).context("Failed to serialize result")?; + let value = serde_json::to_value(result).context("Failed to serialize result")?; - Python::with_gil(|py| Ok(PyValue(value).to_object(py))) - }) + Python::with_gil(|py| Ok(PyValue(value).to_object(py))) + }), + ) } pub fn create_decision(&self, content: String) -> PyResult { @@ -145,8 +158,13 @@ impl PyZenEngine { } pub fn get_decision(&self, key: String) -> PyResult { - let decision = futures::executor::block_on(self.graph.get_decision(&key)) - .context("Failed to find decision with given key")?; + let graph = self.graph.clone(); + let decision = spawn_worker_blocking(move || async move { + graph + .get_decision(&key) + .await + .context("Failed to find decision with given key") + })?; Ok(PyZenDecision::from(decision)) } diff --git a/bindings/python/src/lib.rs b/bindings/python/src/lib.rs index 29b377a4..2bca2d2a 100644 --- a/bindings/python/src/lib.rs +++ b/bindings/python/src/lib.rs @@ -9,6 +9,7 @@ mod decision; mod engine; mod expression; mod loader; +mod mt; mod types; mod value; diff --git a/bindings/python/src/loader.rs b/bindings/python/src/loader.rs index 3707502e..704559b9 100644 --- a/bindings/python/src/loader.rs +++ b/bindings/python/src/loader.rs @@ -1,8 +1,10 @@ -use anyhow::anyhow; -use async_trait::async_trait; -use pyo3::{PyObject, Python}; +use std::future::Future; use std::sync::Arc; -use zen_engine::loader::{DecisionLoader, LoaderError, LoaderResult}; + +use anyhow::anyhow; +use pyo3::{PyObject, Python}; + +use zen_engine::loader::{DecisionLoader, LoaderError, LoaderResponse}; use zen_engine::model::DecisionContent; #[derive(Default)] @@ -35,15 +37,16 @@ impl PyDecisionLoader { } } -#[async_trait] impl DecisionLoader for PyDecisionLoader { - async fn load(&self, key: &str) -> LoaderResult> { - self.load_element(key).map_err(|e| { - LoaderError::Internal { - source: e, - key: key.to_string(), - } - .into() - }) + fn load<'a>(&'a self, key: &'a str) -> impl Future + 'a { + async move { + self.load_element(key).map_err(|e| { + LoaderError::Internal { + source: e, + key: key.to_string(), + } + .into() + }) + } } } diff --git a/bindings/python/src/mt.rs b/bindings/python/src/mt.rs new file mode 100644 index 00000000..24103fb8 --- /dev/null +++ b/bindings/python/src/mt.rs @@ -0,0 +1,48 @@ +use std::future::Future; +use std::sync::OnceLock; +use std::thread::available_parallelism; +use tokio::runtime::Handle; +use tokio_util::task::LocalPoolHandle; + +fn parallelism() -> usize { + available_parallelism().map(Into::into).unwrap_or(1) +} + +pub(crate) fn worker_pool() -> LocalPoolHandle { + static LOCAL_POOL: OnceLock = OnceLock::new(); + LOCAL_POOL + .get_or_init(|| LocalPoolHandle::new(parallelism())) + .clone() +} + +pub(crate) fn spawn_worker(create_task: F) -> impl Future +where + F: FnOnce() -> Fut, + F: Send + 'static, + Fut: Future + 'static, + Fut::Output: Send + 'static, +{ + async move { + worker_pool() + .spawn_pinned(create_task) + .await + .expect("Thread panicked") + } +} + +pub(crate) fn spawn_worker_blocking(create_task: F) -> Fut::Output +where + F: FnOnce() -> Fut, + F: Send + 'static, + Fut: Future + 'static, + Fut::Output: Send + 'static, +{ + tokio::task::block_in_place(move || { + Handle::current().block_on(async move { + worker_pool() + .spawn_pinned(create_task) + .await + .expect("Thread panicked") + }) + }) +} diff --git a/core/engine/Cargo.toml b/core/engine/Cargo.toml index 52bac967..4e0f88c8 100644 --- a/core/engine/Cargo.toml +++ b/core/engine/Cargo.toml @@ -11,8 +11,6 @@ repository = "https://github.com/gorules/zen.git" doctest = false [dependencies] -async-trait = { workspace = true } -async-recursion = "1.1.1" anyhow = { workspace = true } thiserror = { workspace = true } bincode = { workspace = true, optional = true } @@ -22,15 +20,16 @@ serde = { workspace = true, features = ["derive"] } once_cell = { workspace = true } json_dotpath = { workspace = true } fixedbitset = "0.4.2" -futures = { workspace = true } -rquickjs = { version = "0.4.3", features = ["macro", "loader", "rust-alloc"] } +tokio = { workspace = true, features = ["sync", "time"] } +reqwest = { version = "0.12", features = ["json", "rustls-tls"], default-features = false } +rquickjs = { version = "0.6.2", features = ["macro", "loader", "rust-alloc", "futures", "either", "properties"] } itertools = { workspace = true } zen-expression = { path = "../expression", version = "0.23.0" } zen-tmpl = { path = "../template", version = "0.23.0" } [dev-dependencies] -tokio = { version = "1.37.0", features = ["rt", "macros"] } -criterion = { workspace = true, features = ["async_futures"] } +tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } +criterion = { workspace = true, features = ["async_tokio"] } [features] bincode = ["dep:bincode"] diff --git a/core/engine/README.md b/core/engine/README.md index b6d5606e..a9c73115 100644 --- a/core/engine/README.md +++ b/core/engine/README.md @@ -15,12 +15,14 @@ and execute JSON Decision Model (JDM) from JSON files. ## Installation Add the following to your Cargo.toml file: + ```toml [dependencies] zen-engine = "0" ``` ## Usage + To execute a simple decision using a Noop (default) loader you can use the code below. ```rust @@ -32,7 +34,7 @@ async fn evaluate() { let decision_content: DecisionContent = serde_json::from_str(include_str!("jdm_graph.json")).unwrap(); let engine = DecisionEngine::default(); let decision = engine.create_decision(decision_content.into()); - + let result = decision.evaluate(&json!({ "input": 12 })).await; } ``` @@ -41,8 +43,10 @@ Alternatively, you may create decision indirectly without constructing the engin `Decision::from` function. ## Loaders + For more advanced use cases where you want to load multiple decisions and utilise graphs you may use one of the following pre-made loaders: + - FilesystemLoader - with a given path as a root it tries to load a decision based on relative path - MemoryLoader - works as a HashMap (key-value store) - ClosureLoader - allows for definition of simple async callback function which takes key as a parameter @@ -51,6 +55,7 @@ may use one of the following pre-made loaders: (mostly existing for streamlining API across languages) ### Filesystem loader + Assuming that you have a folder with decision models (.json files) which is located under /app/decisions, you may use FilesystemLoader in the following way: @@ -64,7 +69,7 @@ async fn evaluate() { keep_in_memory: true, // optionally, keep in memory for increase performance root: "/app/decisions" })); - + let context = json!({ "customer": { "joinedAt": "2022-01-01" } }); // If you plan on using it multiple times, you may cache JDM for minor performance gains // In case of bindings (in other languages, this increase is much greater) @@ -72,7 +77,7 @@ async fn evaluate() { let promotion_decision = engine.get_decision("commercial/promotion.json").await.unwrap(); let result = promotion_decision.evaluate(&context).await.unwrap(); } - + // Or on demand { let result = engine.evaluate("commercial/promotion.json", &context).await.unwrap(); @@ -81,7 +86,8 @@ async fn evaluate() { ``` ### Custom loader -You may create a custom loader for zen engine by implementing `DecisionLoader` trait using async_trait crate. + +You may create a custom loader for zen engine by implementing `DecisionLoader` trait. Here's an example of how MemoryLoader has been implemented. ```rust @@ -97,34 +103,35 @@ pub struct MemoryLoader { impl MemoryLoader { pub fn add(&self, key: K, content: D) - where - K: Into, - D: Into, + where + K: Into, + D: Into, { let mut mref = self.memory_refs.write().unwrap(); mref.insert(key.into(), Arc::new(content.into())); } pub fn get(&self, key: K) -> Option> - where - K: AsRef, + where + K: AsRef, { let mref = self.memory_refs.read().unwrap(); mref.get(key.as_ref()).map(|r| r.clone()) } pub fn remove(&self, key: K) -> bool - where - K: AsRef, + where + K: AsRef, { let mut mref = self.memory_refs.write().unwrap(); mref.remove(key.as_ref()).is_some() } } -#[async_trait] impl DecisionLoader for MemoryLoader { - async fn load(&self, key: &str) -> LoaderResponse { - self.get(&key) - .ok_or_else(|| LoaderError::NotFound(key.to_string())) + fn load<'a>(&'a self, key: &'a str) -> impl Future + 'a { + async move { + self.get(&key) + .ok_or_else(|| LoaderError::NotFound(key.to_string()).into()) + } } } ``` \ No newline at end of file diff --git a/core/engine/benches/engine.rs b/core/engine/benches/engine.rs index 24cde5a6..a11b7885 100644 --- a/core/engine/benches/engine.rs +++ b/core/engine/benches/engine.rs @@ -1,9 +1,8 @@ -use criterion::async_executor::FuturesExecutor; use criterion::{criterion_group, criterion_main, Bencher, Criterion}; -use futures::executor::block_on; use serde_json::{json, Value}; use std::path::Path; use std::sync::Arc; +use tokio::runtime::Runtime; use zen_engine::handler::custom_node_adapter::NoopCustomNode; use zen_engine::loader::{FilesystemLoader, FilesystemLoaderOptions}; use zen_engine::DecisionEngine; @@ -23,18 +22,20 @@ fn create_graph() -> DecisionEngine { } fn bench_decision(b: &mut Bencher, key: &str, context: Value) { + let rt = Runtime::new().unwrap(); let graph = create_graph(); - let decision = block_on(graph.get_decision(key)).unwrap(); - b.to_async(FuturesExecutor).iter(|| async { + let decision = rt.block_on(graph.get_decision(key)).unwrap(); + b.to_async(&rt).iter(|| async { criterion::black_box(decision.evaluate(&context).await.unwrap()); }); } fn bench_loader(b: &mut Bencher, key: &str, context: Value) { + let rt = Runtime::new().unwrap(); let graph = create_graph(); - b.to_async(FuturesExecutor).iter(|| async { + b.to_async(&rt).iter(|| async { criterion::black_box(graph.evaluate(key, &context).await.unwrap()); }); } diff --git a/core/engine/js/big.mjs b/core/engine/js/big.mjs new file mode 100644 index 00000000..77f03c97 --- /dev/null +++ b/core/engine/js/big.mjs @@ -0,0 +1,8 @@ +/** + * Bundled by jsDelivr using Rollup v2.79.1 and Terser v5.19.2. + * Original file: /npm/big.js@6.2.1/big.js + * + * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files + */ +var r,e="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{},t={exports:{}};r=t,function(e){var t,n=1e6,i=1e6,o="[big.js] ",s=o+"Invalid ",c=s+"decimal places",f=s+"rounding mode",u=o+"Division by zero",h={},l=void 0,a=/^-?(\d+(\.\d*)?|\.\d+)(e[+-]?\d+)?$/i;function p(r,e,t,n){var i=r.c;if(t===l&&(t=r.constructor.RM),0!==t&&1!==t&&2!==t&&3!==t)throw Error(f);if(e<1)n=3===t&&(n||!!i[0])||0===e&&(1===t&&i[0]>=5||2===t&&(i[0]>5||5===i[0]&&(n||i[1]!==l))),i.length=1,n?(r.e=r.e-e+1,i[0]=1):i[0]=r.e=0;else if(e=5||2===t&&(i[e]>5||5===i[e]&&(n||i[e+1]!==l||1&i[e-1]))||3===t&&(n||!!i[0]),i.length=e,n)for(;++i[--e]>9;)if(i[e]=0,0===e){++r.e,i.unshift(1);break}for(e=i.length;!i[--e];)i.pop()}return r}function w(r,e,t){var n=r.e,i=r.c.join(""),o=i.length;if(e)i=i.charAt(0)+(o>1?"."+i.slice(1):"")+(n<0?"e":"e+")+n;else if(n<0){for(;++n;)i="0"+i;i="0."+i}else if(n>0)if(++n>o)for(n-=o;n--;)i+="0";else n1&&(i=i.charAt(0)+"."+i.slice(1));return r.s<0&&t?"-"+i:i}h.abs=function(){var r=new this.constructor(this);return r.s=1,r},h.cmp=function(r){var e,t=this,n=t.c,i=(r=new t.constructor(r)).c,o=t.s,s=r.s,c=t.e,f=r.e;if(!n[0]||!i[0])return n[0]?o:i[0]?-s:0;if(o!=s)return o;if(e=o<0,c!=f)return c>f^e?1:-1;for(s=(c=n.length)<(f=i.length)?c:f,o=-1;++oi[o]^e?1:-1;return c==f?0:c>f^e?1:-1},h.div=function(r){var e=this,t=e.constructor,i=e.c,o=(r=new t(r)).c,s=e.s==r.s?1:-1,f=t.DP;if(f!==~~f||f<0||f>n)throw Error(c);if(!o[0])throw Error(u);if(!i[0])return r.s=s,r.c=[r.e=0],r;var h,a,w,g,v,d=o.slice(),E=h=o.length,m=i.length,b=i.slice(0,h),P=b.length,x=r,y=x.c=[],D=0,M=f+(x.e=e.e-r.e)+1;for(x.s=s,s=M<0?0:M,d.unshift(0);P++P?1:-1;else for(v=-1,g=0;++vb[v]?1:-1;break}if(!(g<0))break;for(a=P==h?o:d;P;){if(b[--P]M&&p(x,M,t.RM,b[0]!==l),x},h.eq=function(r){return 0===this.cmp(r)},h.gt=function(r){return this.cmp(r)>0},h.gte=function(r){return this.cmp(r)>-1},h.lt=function(r){return this.cmp(r)<0},h.lte=function(r){return this.cmp(r)<1},h.minus=h.sub=function(r){var e,t,n,i,o=this,s=o.constructor,c=o.s,f=(r=new s(r)).s;if(c!=f)return r.s=-f,o.plus(r);var u=o.c.slice(),h=o.e,l=r.c,a=r.e;if(!u[0]||!l[0])return l[0]?r.s=-f:u[0]?r=new s(o):r.s=1,r;if(c=h-a){for((i=c<0)?(c=-c,n=u):(a=h,n=l),n.reverse(),f=c;f--;)n.push(0);n.reverse()}else for(t=((i=u.length0)for(;f--;)u[e++]=0;for(f=e;t>c;){if(u[--t]0?(f=s,n=u):(e=-e,n=c),n.reverse();e--;)n.push(0);n.reverse()}for(c.length-u.length<0&&(n=u,u=c,c=n),e=u.length,t=0;e;c[e]%=10)t=(c[--e]=c[e]+u[e]+t)/10|0;for(t&&(c.unshift(t),++f),e=c.length;0===c[--e];)c.pop();return r.c=c,r.e=f,r},h.pow=function(r){var e=this,t=new e.constructor("1"),n=t,o=r<0;if(r!==~~r||r<-1e6||r>i)throw Error(s+"exponent");for(o&&(r=-r);1&r&&(n=n.times(e)),r>>=1;)e=e.times(e);return o?t.div(n):n},h.prec=function(r,e){if(r!==~~r||r<1||r>n)throw Error(s+"precision");return p(new this.constructor(this),r,e)},h.round=function(r,e){if(r===l)r=0;else if(r!==~~r||r<-n||r>n)throw Error(c);return p(new this.constructor(this),r+this.e+1,e)},h.sqrt=function(){var r,e,t,n=this,i=n.constructor,s=n.s,c=n.e,f=new i("0.5");if(!n.c[0])return new i(n);if(s<0)throw Error(o+"No square root");0===(s=Math.sqrt(n+""))||s===1/0?((e=n.c.join("")).length+c&1||(e+="0"),c=((c+1)/2|0)-(c<0||1&c),r=new i(((s=Math.sqrt(e))==1/0?"5e":(s=s.toExponential()).slice(0,s.indexOf("e")+1))+c)):r=new i(s+""),c=r.e+(i.DP+=4);do{t=r,r=f.times(t.plus(n.div(t)))}while(t.c.slice(0,c).join("")!==r.c.slice(0,c).join(""));return p(r,(i.DP-=4)+r.e+1,i.RM)},h.times=h.mul=function(r){var e,t=this,n=t.constructor,i=t.c,o=(r=new n(r)).c,s=i.length,c=o.length,f=t.e,u=r.e;if(r.s=t.s==r.s?1:-1,!i[0]||!o[0])return r.c=[r.e=0],r;for(r.e=f+u,sf;)c=e[u]+o[f]*i[u-f-1]+c,e[u--]=c%10,c=c/10|0;e[u]=c}for(c?++r.e:e.shift(),f=e.length;!e[--f];)e.pop();return r.c=e,r},h.toExponential=function(r,e){var t=this,i=t.c[0];if(r!==l){if(r!==~~r||r<0||r>n)throw Error(c);for(t=p(new t.constructor(t),++r,e);t.c.lengthn)throw Error(c);for(r=r+(t=p(new t.constructor(t),r+t.e+1,e)).e+1;t.c.length=e.PE,!!r.c[0])},h.toNumber=function(){var r=Number(w(this,!0,!0));if(!0===this.constructor.strict&&!this.eq(r.toString()))throw Error(o+"Imprecise conversion");return r},h.toPrecision=function(r,e){var t=this,i=t.constructor,o=t.c[0];if(r!==l){if(r!==~~r||r<1||r>n)throw Error(s+"precision");for(t=p(new i(t),r,e);t.c.length=i.PE,!!o)},h.valueOf=function(){var r=this,e=r.constructor;if(!0===e.strict)throw Error(o+"valueOf disallowed");return w(r,r.e<=e.NE||r.e>=e.PE,!0)},t=function r(){function e(t){var n=this;if(!(n instanceof e))return t===l?r():new e(t);if(t instanceof e)n.s=t.s,n.e=t.e,n.c=t.c.slice();else{if("string"!=typeof t){if(!0===e.strict&&"bigint"!=typeof t)throw TypeError(s+"value");t=0===t&&1/t<0?"-0":String(t)}!function(r,e){var t,n,i;if(!a.test(e))throw Error(s+"number");for(r.s="-"==e.charAt(0)?(e=e.slice(1),-1):1,(t=e.indexOf("."))>-1&&(e=e.replace(".","")),(n=e.search(/e/i))>0?(t<0&&(t=n),t+=+e.slice(n+1),e=e.substring(0,n)):t<0&&(t=e.length),i=e.length,n=0;n0&&"0"==e.charAt(--i););for(r.e=t-n-1,r.c=[],t=0;n<=i;)r.c[t++]=+e.charAt(n++)}}(n,t)}n.constructor=e}return e.prototype=h,e.DP=20,e.RM=1,e.NE=-7,e.PE=21,e.strict=!1,e.roundDown=0,e.roundHalfUp=1,e.roundHalfEven=2,e.roundUp=3,e}(),t.default=t.Big=t,r.exports?r.exports=t:e.Big=t}(e);var n=t.exports;export{n as default}; +//# sourceMappingURL=/sm/d5d645c26c6ce952f28329f82f733a72e37c9c4629adab94c729d9987261cde8.map \ No newline at end of file diff --git a/core/engine/js/dayjs.mjs b/core/engine/js/dayjs.mjs new file mode 100644 index 00000000..54d3ef05 --- /dev/null +++ b/core/engine/js/dayjs.mjs @@ -0,0 +1,8 @@ +/** + * Bundled by jsDelivr using Rollup v2.79.1 and Terser v5.19.2. + * Original file: /npm/dayjs@1.11.11/dayjs.min.js + * + * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files + */ +"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self&&self;var t={exports:{}},e=t.exports=function(){var t=1e3,e=6e4,n=36e5,r="millisecond",s="second",i="minute",u="hour",a="day",o="week",c="month",f="quarter",h="year",d="date",l="Invalid Date",$=/^(\d{4})[-/]?(\d{1,2})?[-/]?(\d{0,2})[Tt\s]*(\d{1,2})?:?(\d{1,2})?:?(\d{1,2})?[.:]?(\d+)?$/,y=/\[([^\]]+)]|Y{1,4}|M{1,4}|D{1,2}|d{1,4}|H{1,2}|h{1,2}|a|A|m{1,2}|s{1,2}|Z{1,2}|SSS/g,M={name:"en",weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),ordinal:function(t){var e=["th","st","nd","rd"],n=t%100;return"["+t+(e[(n-20)%10]||e[n]||e[0])+"]"}},g=function(t,e,n){var r=String(t);return!r||r.length>=e?t:""+Array(e+1-r.length).join(n)+t},v={s:g,z:function(t){var e=-t.utcOffset(),n=Math.abs(e),r=Math.floor(n/60),s=n%60;return(e<=0?"+":"-")+g(r,2,"0")+":"+g(s,2,"0")},m:function t(e,n){if(e.date()1)return t(u[0])}else{var a=e.name;D[a]=e,s=a}return!r&&s&&(m=s),s||!r&&m},b=function(t,e){if(S(t))return t.clone();var n="object"==typeof e?e:{};return n.date=t,n.args=arguments,new _(n)},O=v;O.l=w,O.i=S,O.w=function(t,e){return b(t,{locale:e.$L,utc:e.$u,x:e.$x,$offset:e.$offset})};var _=function(){function M(t){this.$L=w(t.locale,null,!0),this.parse(t),this.$x=this.$x||t.x||{},this[p]=!0}var g=M.prototype;return g.parse=function(t){this.$d=function(t){var e=t.date,n=t.utc;if(null===e)return new Date(NaN);if(O.u(e))return new Date;if(e instanceof Date)return new Date(e);if("string"==typeof e&&!/Z$/i.test(e)){var r=e.match($);if(r){var s=r[2]-1||0,i=(r[7]||"0").substring(0,3);return n?new Date(Date.UTC(r[1],s,r[3]||1,r[4]||0,r[5]||0,r[6]||0,i)):new Date(r[1],s,r[3]||1,r[4]||0,r[5]||0,r[6]||0,i)}}return new Date(e)}(t),this.init()},g.init=function(){var t=this.$d;this.$y=t.getFullYear(),this.$M=t.getMonth(),this.$D=t.getDate(),this.$W=t.getDay(),this.$H=t.getHours(),this.$m=t.getMinutes(),this.$s=t.getSeconds(),this.$ms=t.getMilliseconds()},g.$utils=function(){return O},g.isValid=function(){return!(this.$d.toString()===l)},g.isSame=function(t,e){var n=b(t);return this.startOf(e)<=n&&n<=this.endOf(e)},g.isAfter=function(t,e){return b(t) e, e.assertIs = function (e) { + }, e.assertNever = function (e) { + throw new Error + }, e.arrayToEnum = e => { + const t = {}; + for (const a of e) t[a] = a; + return t + }, e.getValidEnumValues = t => { + const a = e.objectKeys(t).filter((e => "number" != typeof t[t[e]])), s = {}; + for (const e of a) s[e] = t[e]; + return e.objectValues(s) + }, e.objectValues = t => e.objectKeys(t).map((function (e) { + return t[e] + })), e.objectKeys = "function" == typeof Object.keys ? e => Object.keys(e) : e => { + const t = []; + for (const a in e) Object.prototype.hasOwnProperty.call(e, a) && t.push(a); + return t + }, e.find = (e, t) => { + for (const a of e) if (t(a)) return a + }, e.isInteger = "function" == typeof Number.isInteger ? e => Number.isInteger(e) : e => "number" == typeof e && isFinite(e) && Math.floor(e) === e, e.joinValues = function (e, t = " | ") { + return e.map((e => "string" == typeof e ? `'${e}'` : e)).join(t) + }, e.jsonStringifyReplacer = (e, t) => "bigint" == typeof t ? t.toString() : t +}(e || (e = {})), function (e) { + e.mergeShapes = (e, t) => ({...e, ...t}) +}(t || (t = {})); +const a = e.arrayToEnum(["string", "nan", "number", "integer", "float", "boolean", "date", "bigint", "symbol", "function", "undefined", "null", "array", "object", "unknown", "promise", "void", "never", "map", "set"]), + s = e => { + switch (typeof e) { + case"undefined": + return a.undefined; + case"string": + return a.string; + case"number": + return isNaN(e) ? a.nan : a.number; + case"boolean": + return a.boolean; + case"function": + return a.function; + case"bigint": + return a.bigint; + case"symbol": + return a.symbol; + case"object": + return Array.isArray(e) ? a.array : null === e ? a.null : e.then && "function" == typeof e.then && e.catch && "function" == typeof e.catch ? a.promise : "undefined" != typeof Map && e instanceof Map ? a.map : "undefined" != typeof Set && e instanceof Set ? a.set : "undefined" != typeof Date && e instanceof Date ? a.date : a.object; + default: + return a.unknown + } + }, + n = e.arrayToEnum(["invalid_type", "invalid_literal", "custom", "invalid_union", "invalid_union_discriminator", "invalid_enum_value", "unrecognized_keys", "invalid_arguments", "invalid_return_type", "invalid_date", "invalid_string", "too_small", "too_big", "invalid_intersection_types", "not_multiple_of", "not_finite"]), + r = e => JSON.stringify(e, null, 2).replace(/"([^"]+)":/g, "$1:"); + +class i extends Error { + constructor(e) { + super(), this.issues = [], this.addIssue = e => { + this.issues = [...this.issues, e] + }, this.addIssues = (e = []) => { + this.issues = [...this.issues, ...e] + }; + const t = new.target.prototype; + Object.setPrototypeOf ? Object.setPrototypeOf(this, t) : this.__proto__ = t, this.name = "ZodError", this.issues = e + } + + get errors() { + return this.issues + } + + format(e) { + const t = e || function (e) { + return e.message + }, a = {_errors: []}, s = e => { + for (const n of e.issues) if ("invalid_union" === n.code) n.unionErrors.map(s); else if ("invalid_return_type" === n.code) s(n.returnTypeError); else if ("invalid_arguments" === n.code) s(n.argumentsError); else if (0 === n.path.length) a._errors.push(t(n)); else { + let e = a, s = 0; + for (; s < n.path.length;) { + const a = n.path[s]; + s === n.path.length - 1 ? (e[a] = e[a] || {_errors: []}, e[a]._errors.push(t(n))) : e[a] = e[a] || {_errors: []}, e = e[a], s++ + } + } + }; + return s(this), a + } + + static assert(e) { + if (!(e instanceof i)) throw new Error(`Not a ZodError: ${e}`) + } + + toString() { + return this.message + } + + get message() { + return JSON.stringify(this.issues, e.jsonStringifyReplacer, 2) + } + + get isEmpty() { + return 0 === this.issues.length + } + + flatten(e = (e => e.message)) { + const t = {}, a = []; + for (const s of this.issues) s.path.length > 0 ? (t[s.path[0]] = t[s.path[0]] || [], t[s.path[0]].push(e(s))) : a.push(e(s)); + return {formErrors: a, fieldErrors: t} + } + + get formErrors() { + return this.flatten() + } +} + +i.create = e => new i(e); +const o = (t, s) => { + let r; + switch (t.code) { + case n.invalid_type: + r = t.received === a.undefined ? "Required" : `Expected ${t.expected}, received ${t.received}`; + break; + case n.invalid_literal: + r = `Invalid literal value, expected ${JSON.stringify(t.expected, e.jsonStringifyReplacer)}`; + break; + case n.unrecognized_keys: + r = `Unrecognized key(s) in object: ${e.joinValues(t.keys, ", ")}`; + break; + case n.invalid_union: + r = "Invalid input"; + break; + case n.invalid_union_discriminator: + r = `Invalid discriminator value. Expected ${e.joinValues(t.options)}`; + break; + case n.invalid_enum_value: + r = `Invalid enum value. Expected ${e.joinValues(t.options)}, received '${t.received}'`; + break; + case n.invalid_arguments: + r = "Invalid function arguments"; + break; + case n.invalid_return_type: + r = "Invalid function return type"; + break; + case n.invalid_date: + r = "Invalid date"; + break; + case n.invalid_string: + "object" == typeof t.validation ? "includes" in t.validation ? (r = `Invalid input: must include "${t.validation.includes}"`, "number" == typeof t.validation.position && (r = `${r} at one or more positions greater than or equal to ${t.validation.position}`)) : "startsWith" in t.validation ? r = `Invalid input: must start with "${t.validation.startsWith}"` : "endsWith" in t.validation ? r = `Invalid input: must end with "${t.validation.endsWith}"` : e.assertNever(t.validation) : r = "regex" !== t.validation ? `Invalid ${t.validation}` : "Invalid"; + break; + case n.too_small: + r = "array" === t.type ? `Array must contain ${t.exact ? "exactly" : t.inclusive ? "at least" : "more than"} ${t.minimum} element(s)` : "string" === t.type ? `String must contain ${t.exact ? "exactly" : t.inclusive ? "at least" : "over"} ${t.minimum} character(s)` : "number" === t.type ? `Number must be ${t.exact ? "exactly equal to " : t.inclusive ? "greater than or equal to " : "greater than "}${t.minimum}` : "date" === t.type ? `Date must be ${t.exact ? "exactly equal to " : t.inclusive ? "greater than or equal to " : "greater than "}${new Date(Number(t.minimum))}` : "Invalid input"; + break; + case n.too_big: + r = "array" === t.type ? `Array must contain ${t.exact ? "exactly" : t.inclusive ? "at most" : "less than"} ${t.maximum} element(s)` : "string" === t.type ? `String must contain ${t.exact ? "exactly" : t.inclusive ? "at most" : "under"} ${t.maximum} character(s)` : "number" === t.type ? `Number must be ${t.exact ? "exactly" : t.inclusive ? "less than or equal to" : "less than"} ${t.maximum}` : "bigint" === t.type ? `BigInt must be ${t.exact ? "exactly" : t.inclusive ? "less than or equal to" : "less than"} ${t.maximum}` : "date" === t.type ? `Date must be ${t.exact ? "exactly" : t.inclusive ? "smaller than or equal to" : "smaller than"} ${new Date(Number(t.maximum))}` : "Invalid input"; + break; + case n.custom: + r = "Invalid input"; + break; + case n.invalid_intersection_types: + r = "Intersection results could not be merged"; + break; + case n.not_multiple_of: + r = `Number must be a multiple of ${t.multipleOf}`; + break; + case n.not_finite: + r = "Number must be finite"; + break; + default: + r = s.defaultError, e.assertNever(t) + } + return {message: r} +}; +let d = o; + +function c(e) { + d = e +} + +function u() { + return d +} + +const l = e => { + const {data: t, path: a, errorMaps: s, issueData: n} = e, r = [...a, ...n.path || []], i = {...n, path: r}; + if (void 0 !== n.message) return {...n, path: r, message: n.message}; + let o = ""; + const d = s.filter((e => !!e)).slice().reverse(); + for (const e of d) o = e(i, {data: t, defaultError: o}).message; + return {...n, path: r, message: o} +}, h = []; + +function p(e, t) { + const a = u(), s = l({ + issueData: t, + data: e.data, + path: e.path, + errorMaps: [e.common.contextualErrorMap, e.schemaErrorMap, a, a === o ? void 0 : o].filter((e => !!e)) + }); + e.common.issues.push(s) +} + +class m { + constructor() { + this.value = "valid" + } + + dirty() { + "valid" === this.value && (this.value = "dirty") + } + + abort() { + "aborted" !== this.value && (this.value = "aborted") + } + + static mergeArray(e, t) { + const a = []; + for (const s of t) { + if ("aborted" === s.status) return f; + "dirty" === s.status && e.dirty(), a.push(s.value) + } + return {status: e.value, value: a} + } + + static async mergeObjectAsync(e, t) { + const a = []; + for (const e of t) { + const t = await e.key, s = await e.value; + a.push({key: t, value: s}) + } + return m.mergeObjectSync(e, a) + } + + static mergeObjectSync(e, t) { + const a = {}; + for (const s of t) { + const {key: t, value: n} = s; + if ("aborted" === t.status) return f; + if ("aborted" === n.status) return f; + "dirty" === t.status && e.dirty(), "dirty" === n.status && e.dirty(), "__proto__" === t.value || void 0 === n.value && !s.alwaysSet || (a[t.value] = n.value) + } + return {status: e.value, value: a} + } +} + +const f = Object.freeze({status: "aborted"}), y = e => ({status: "dirty", value: e}), + _ = e => ({status: "valid", value: e}), v = e => "aborted" === e.status, g = e => "dirty" === e.status, + k = e => "valid" === e.status, b = e => "undefined" != typeof Promise && e instanceof Promise; + +function x(e, t, a, s) { + if ("a" === a && !s) throw new TypeError("Private accessor was defined without a getter"); + if ("function" == typeof t ? e !== t || !s : !t.has(e)) throw new TypeError("Cannot read private member from an object whose class did not declare it"); + return "m" === a ? s : "a" === a ? s.call(e) : s ? s.value : t.get(e) +} + +function w(e, t, a, s, n) { + if ("m" === s) throw new TypeError("Private method is not writable"); + if ("a" === s && !n) throw new TypeError("Private accessor was defined without a setter"); + if ("function" == typeof t ? e !== t || !n : !t.has(e)) throw new TypeError("Cannot write private member to an object whose class did not declare it"); + return "a" === s ? n.call(e, a) : n ? n.value = a : t.set(e, a), a +} + +var Z, T, O; +"function" == typeof SuppressedError && SuppressedError, function (e) { + e.errToObj = e => "string" == typeof e ? {message: e} : e || {}, e.toString = e => "string" == typeof e ? e : null == e ? void 0 : e.message +}(Z || (Z = {})); + +class C { + constructor(e, t, a, s) { + this._cachedPath = [], this.parent = e, this.data = t, this._path = a, this._key = s + } + + get path() { + return this._cachedPath.length || (this._key instanceof Array ? this._cachedPath.push(...this._path, ...this._key) : this._cachedPath.push(...this._path, this._key)), this._cachedPath + } +} + +const N = (e, t) => { + if (k(t)) return {success: !0, data: t.value}; + if (!e.common.issues.length) throw new Error("Validation failed but no issues detected."); + return { + success: !1, get error() { + if (this._error) return this._error; + const t = new i(e.common.issues); + return this._error = t, this._error + } + } +}; + +function S(e) { + if (!e) return {}; + const {errorMap: t, invalid_type_error: a, required_error: s, description: n} = e; + if (t && (a || s)) throw new Error('Can\'t use "invalid_type_error" or "required_error" in conjunction with custom error map.'); + if (t) return {errorMap: t, description: n}; + return { + errorMap: (t, n) => { + var r, i; + const {message: o} = e; + return "invalid_enum_value" === t.code ? {message: null != o ? o : n.defaultError} : void 0 === n.data ? {message: null !== (r = null != o ? o : s) && void 0 !== r ? r : n.defaultError} : "invalid_type" !== t.code ? {message: n.defaultError} : {message: null !== (i = null != o ? o : a) && void 0 !== i ? i : n.defaultError} + }, description: n + } +} + +class E { + constructor(e) { + this.spa = this.safeParseAsync, this._def = e, this.parse = this.parse.bind(this), this.safeParse = this.safeParse.bind(this), this.parseAsync = this.parseAsync.bind(this), this.safeParseAsync = this.safeParseAsync.bind(this), this.spa = this.spa.bind(this), this.refine = this.refine.bind(this), this.refinement = this.refinement.bind(this), this.superRefine = this.superRefine.bind(this), this.optional = this.optional.bind(this), this.nullable = this.nullable.bind(this), this.nullish = this.nullish.bind(this), this.array = this.array.bind(this), this.promise = this.promise.bind(this), this.or = this.or.bind(this), this.and = this.and.bind(this), this.transform = this.transform.bind(this), this.brand = this.brand.bind(this), this.default = this.default.bind(this), this.catch = this.catch.bind(this), this.describe = this.describe.bind(this), this.pipe = this.pipe.bind(this), this.readonly = this.readonly.bind(this), this.isNullable = this.isNullable.bind(this), this.isOptional = this.isOptional.bind(this) + } + + get description() { + return this._def.description + } + + _getType(e) { + return s(e.data) + } + + _getOrReturnCtx(e, t) { + return t || { + common: e.parent.common, + data: e.data, + parsedType: s(e.data), + schemaErrorMap: this._def.errorMap, + path: e.path, + parent: e.parent + } + } + + _processInputParams(e) { + return { + status: new m, + ctx: { + common: e.parent.common, + data: e.data, + parsedType: s(e.data), + schemaErrorMap: this._def.errorMap, + path: e.path, + parent: e.parent + } + } + } + + _parseSync(e) { + const t = this._parse(e); + if (b(t)) throw new Error("Synchronous parse encountered promise."); + return t + } + + _parseAsync(e) { + const t = this._parse(e); + return Promise.resolve(t) + } + + parse(e, t) { + const a = this.safeParse(e, t); + if (a.success) return a.data; + throw a.error + } + + safeParse(e, t) { + var a; + const n = { + common: { + issues: [], + async: null !== (a = null == t ? void 0 : t.async) && void 0 !== a && a, + contextualErrorMap: null == t ? void 0 : t.errorMap + }, + path: (null == t ? void 0 : t.path) || [], + schemaErrorMap: this._def.errorMap, + parent: null, + data: e, + parsedType: s(e) + }, r = this._parseSync({data: e, path: n.path, parent: n}); + return N(n, r) + } + + async parseAsync(e, t) { + const a = await this.safeParseAsync(e, t); + if (a.success) return a.data; + throw a.error + } + + async safeParseAsync(e, t) { + const a = { + common: {issues: [], contextualErrorMap: null == t ? void 0 : t.errorMap, async: !0}, + path: (null == t ? void 0 : t.path) || [], + schemaErrorMap: this._def.errorMap, + parent: null, + data: e, + parsedType: s(e) + }, n = this._parse({data: e, path: a.path, parent: a}), r = await (b(n) ? n : Promise.resolve(n)); + return N(a, r) + } + + refine(e, t) { + const a = e => "string" == typeof t || void 0 === t ? {message: t} : "function" == typeof t ? t(e) : t; + return this._refinement(((t, s) => { + const r = e(t), i = () => s.addIssue({code: n.custom, ...a(t)}); + return "undefined" != typeof Promise && r instanceof Promise ? r.then((e => !!e || (i(), !1))) : !!r || (i(), !1) + })) + } + + refinement(e, t) { + return this._refinement(((a, s) => !!e(a) || (s.addIssue("function" == typeof t ? t(a, s) : t), !1))) + } + + _refinement(e) { + return new Ze({schema: this, typeName: $e.ZodEffects, effect: {type: "refinement", refinement: e}}) + } + + superRefine(e) { + return this._refinement(e) + } + + optional() { + return Te.create(this, this._def) + } + + nullable() { + return Oe.create(this, this._def) + } + + nullish() { + return this.nullable().optional() + } + + array() { + return re.create(this, this._def) + } + + promise() { + return we.create(this, this._def) + } + + or(e) { + return de.create([this, e], this._def) + } + + and(e) { + return he.create(this, e, this._def) + } + + transform(e) { + return new Ze({...S(this._def), schema: this, typeName: $e.ZodEffects, effect: {type: "transform", transform: e}}) + } + + default(e) { + const t = "function" == typeof e ? e : () => e; + return new Ce({...S(this._def), innerType: this, defaultValue: t, typeName: $e.ZodDefault}) + } + + brand() { + return new je({typeName: $e.ZodBranded, type: this, ...S(this._def)}) + } + + catch(e) { + const t = "function" == typeof e ? e : () => e; + return new Ne({...S(this._def), innerType: this, catchValue: t, typeName: $e.ZodCatch}) + } + + describe(e) { + return new (0, this.constructor)({...this._def, description: e}) + } + + pipe(e) { + return Ie.create(this, e) + } + + readonly() { + return Pe.create(this) + } + + isOptional() { + return this.safeParse(void 0).success + } + + isNullable() { + return this.safeParse(null).success + } +} + +const j = /^c[^\s-]{8,}$/i, I = /^[0-9a-z]+$/, P = /^[0-9A-HJKMNP-TV-Z]{26}$/, + R = /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/i, A = /^[a-z0-9_-]{21}$/i, + $ = /^[-+]?P(?!$)(?:(?:[-+]?\d+Y)|(?:[-+]?\d+[.,]\d+Y$))?(?:(?:[-+]?\d+M)|(?:[-+]?\d+[.,]\d+M$))?(?:(?:[-+]?\d+W)|(?:[-+]?\d+[.,]\d+W$))?(?:(?:[-+]?\d+D)|(?:[-+]?\d+[.,]\d+D$))?(?:T(?=[\d+-])(?:(?:[-+]?\d+H)|(?:[-+]?\d+[.,]\d+H$))?(?:(?:[-+]?\d+M)|(?:[-+]?\d+[.,]\d+M$))?(?:[-+]?\d+(?:[.,]\d+)?S)?)??$/, + M = /^(?!\.)(?!.*\.\.)([A-Z0-9_'+\-\.]*)[A-Z0-9_+-]@([A-Z0-9][A-Z0-9\-]*\.)+[A-Z]{2,}$/i; +let L; +const D = /^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])$/, + z = /^(([a-f0-9]{1,4}:){7}|::([a-f0-9]{1,4}:){0,6}|([a-f0-9]{1,4}:){1}:([a-f0-9]{1,4}:){0,5}|([a-f0-9]{1,4}:){2}:([a-f0-9]{1,4}:){0,4}|([a-f0-9]{1,4}:){3}:([a-f0-9]{1,4}:){0,3}|([a-f0-9]{1,4}:){4}:([a-f0-9]{1,4}:){0,2}|([a-f0-9]{1,4}:){5}:([a-f0-9]{1,4}:){0,1})([a-f0-9]{1,4}|(((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2}))\.){3}((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2})))$/, + V = /^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?$/, + U = "((\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-((0[13578]|1[02])-(0[1-9]|[12]\\d|3[01])|(0[469]|11)-(0[1-9]|[12]\\d|30)|(02)-(0[1-9]|1\\d|2[0-8])))", + K = new RegExp(`^${U}$`); + +function B(e) { + let t = "([01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d"; + return e.precision ? t = `${t}\\.\\d{${e.precision}}` : null == e.precision && (t = `${t}(\\.\\d+)?`), t +} + +function W(e) { + let t = `${U}T${B(e)}`; + const a = []; + return a.push(e.local ? "Z?" : "Z"), e.offset && a.push("([+-]\\d{2}:?\\d{2})"), t = `${t}(${a.join("|")})`, new RegExp(`^${t}$`) +} + +class F extends E { + _parse(t) { + this._def.coerce && (t.data = String(t.data)); + if (this._getType(t) !== a.string) { + const e = this._getOrReturnCtx(t); + return p(e, {code: n.invalid_type, expected: a.string, received: e.parsedType}), f + } + const s = new m; + let r; + for (const a of this._def.checks) if ("min" === a.kind) t.data.length < a.value && (r = this._getOrReturnCtx(t, r), p(r, { + code: n.too_small, + minimum: a.value, + type: "string", + inclusive: !0, + exact: !1, + message: a.message + }), s.dirty()); else if ("max" === a.kind) t.data.length > a.value && (r = this._getOrReturnCtx(t, r), p(r, { + code: n.too_big, + maximum: a.value, + type: "string", + inclusive: !0, + exact: !1, + message: a.message + }), s.dirty()); else if ("length" === a.kind) { + const e = t.data.length > a.value, i = t.data.length < a.value; + (e || i) && (r = this._getOrReturnCtx(t, r), e ? p(r, { + code: n.too_big, + maximum: a.value, + type: "string", + inclusive: !0, + exact: !0, + message: a.message + }) : i && p(r, { + code: n.too_small, + minimum: a.value, + type: "string", + inclusive: !0, + exact: !0, + message: a.message + }), s.dirty()) + } else if ("email" === a.kind) M.test(t.data) || (r = this._getOrReturnCtx(t, r), p(r, { + validation: "email", + code: n.invalid_string, + message: a.message + }), s.dirty()); else if ("emoji" === a.kind) L || (L = new RegExp("^(\\p{Extended_Pictographic}|\\p{Emoji_Component})+$", "u")), L.test(t.data) || (r = this._getOrReturnCtx(t, r), p(r, { + validation: "emoji", + code: n.invalid_string, + message: a.message + }), s.dirty()); else if ("uuid" === a.kind) R.test(t.data) || (r = this._getOrReturnCtx(t, r), p(r, { + validation: "uuid", + code: n.invalid_string, + message: a.message + }), s.dirty()); else if ("nanoid" === a.kind) A.test(t.data) || (r = this._getOrReturnCtx(t, r), p(r, { + validation: "nanoid", + code: n.invalid_string, + message: a.message + }), s.dirty()); else if ("cuid" === a.kind) j.test(t.data) || (r = this._getOrReturnCtx(t, r), p(r, { + validation: "cuid", + code: n.invalid_string, + message: a.message + }), s.dirty()); else if ("cuid2" === a.kind) I.test(t.data) || (r = this._getOrReturnCtx(t, r), p(r, { + validation: "cuid2", + code: n.invalid_string, + message: a.message + }), s.dirty()); else if ("ulid" === a.kind) P.test(t.data) || (r = this._getOrReturnCtx(t, r), p(r, { + validation: "ulid", + code: n.invalid_string, + message: a.message + }), s.dirty()); else if ("url" === a.kind) try { + new URL(t.data) + } catch (e) { + r = this._getOrReturnCtx(t, r), p(r, {validation: "url", code: n.invalid_string, message: a.message}), s.dirty() + } else if ("regex" === a.kind) { + a.regex.lastIndex = 0; + a.regex.test(t.data) || (r = this._getOrReturnCtx(t, r), p(r, { + validation: "regex", + code: n.invalid_string, + message: a.message + }), s.dirty()) + } else if ("trim" === a.kind) t.data = t.data.trim(); else if ("includes" === a.kind) t.data.includes(a.value, a.position) || (r = this._getOrReturnCtx(t, r), p(r, { + code: n.invalid_string, + validation: {includes: a.value, position: a.position}, + message: a.message + }), s.dirty()); else if ("toLowerCase" === a.kind) t.data = t.data.toLowerCase(); else if ("toUpperCase" === a.kind) t.data = t.data.toUpperCase(); else if ("startsWith" === a.kind) t.data.startsWith(a.value) || (r = this._getOrReturnCtx(t, r), p(r, { + code: n.invalid_string, + validation: {startsWith: a.value}, + message: a.message + }), s.dirty()); else if ("endsWith" === a.kind) t.data.endsWith(a.value) || (r = this._getOrReturnCtx(t, r), p(r, { + code: n.invalid_string, + validation: {endsWith: a.value}, + message: a.message + }), s.dirty()); else if ("datetime" === a.kind) { + W(a).test(t.data) || (r = this._getOrReturnCtx(t, r), p(r, { + code: n.invalid_string, + validation: "datetime", + message: a.message + }), s.dirty()) + } else if ("date" === a.kind) { + K.test(t.data) || (r = this._getOrReturnCtx(t, r), p(r, { + code: n.invalid_string, + validation: "date", + message: a.message + }), s.dirty()) + } else if ("time" === a.kind) { + new RegExp(`^${B(a)}$`).test(t.data) || (r = this._getOrReturnCtx(t, r), p(r, { + code: n.invalid_string, + validation: "time", + message: a.message + }), s.dirty()) + } else "duration" === a.kind ? $.test(t.data) || (r = this._getOrReturnCtx(t, r), p(r, { + validation: "duration", + code: n.invalid_string, + message: a.message + }), s.dirty()) : "ip" === a.kind ? (i = t.data, ("v4" !== (o = a.version) && o || !D.test(i)) && ("v6" !== o && o || !z.test(i)) && (r = this._getOrReturnCtx(t, r), p(r, { + validation: "ip", + code: n.invalid_string, + message: a.message + }), s.dirty())) : "base64" === a.kind ? V.test(t.data) || (r = this._getOrReturnCtx(t, r), p(r, { + validation: "base64", + code: n.invalid_string, + message: a.message + }), s.dirty()) : e.assertNever(a); + var i, o; + return {status: s.value, value: t.data} + } + + _regex(e, t, a) { + return this.refinement((t => e.test(t)), {validation: t, code: n.invalid_string, ...Z.errToObj(a)}) + } + + _addCheck(e) { + return new F({...this._def, checks: [...this._def.checks, e]}) + } + + email(e) { + return this._addCheck({kind: "email", ...Z.errToObj(e)}) + } + + url(e) { + return this._addCheck({kind: "url", ...Z.errToObj(e)}) + } + + emoji(e) { + return this._addCheck({kind: "emoji", ...Z.errToObj(e)}) + } + + uuid(e) { + return this._addCheck({kind: "uuid", ...Z.errToObj(e)}) + } + + nanoid(e) { + return this._addCheck({kind: "nanoid", ...Z.errToObj(e)}) + } + + cuid(e) { + return this._addCheck({kind: "cuid", ...Z.errToObj(e)}) + } + + cuid2(e) { + return this._addCheck({kind: "cuid2", ...Z.errToObj(e)}) + } + + ulid(e) { + return this._addCheck({kind: "ulid", ...Z.errToObj(e)}) + } + + base64(e) { + return this._addCheck({kind: "base64", ...Z.errToObj(e)}) + } + + ip(e) { + return this._addCheck({kind: "ip", ...Z.errToObj(e)}) + } + + datetime(e) { + var t, a; + return "string" == typeof e ? this._addCheck({ + kind: "datetime", + precision: null, + offset: !1, + local: !1, + message: e + }) : this._addCheck({ + kind: "datetime", + precision: void 0 === (null == e ? void 0 : e.precision) ? null : null == e ? void 0 : e.precision, + offset: null !== (t = null == e ? void 0 : e.offset) && void 0 !== t && t, + local: null !== (a = null == e ? void 0 : e.local) && void 0 !== a && a, ...Z.errToObj(null == e ? void 0 : e.message) + }) + } + + date(e) { + return this._addCheck({kind: "date", message: e}) + } + + time(e) { + return "string" == typeof e ? this._addCheck({ + kind: "time", + precision: null, + message: e + }) : this._addCheck({ + kind: "time", + precision: void 0 === (null == e ? void 0 : e.precision) ? null : null == e ? void 0 : e.precision, ...Z.errToObj(null == e ? void 0 : e.message) + }) + } + + duration(e) { + return this._addCheck({kind: "duration", ...Z.errToObj(e)}) + } + + regex(e, t) { + return this._addCheck({kind: "regex", regex: e, ...Z.errToObj(t)}) + } + + includes(e, t) { + return this._addCheck({ + kind: "includes", + value: e, + position: null == t ? void 0 : t.position, ...Z.errToObj(null == t ? void 0 : t.message) + }) + } + + startsWith(e, t) { + return this._addCheck({kind: "startsWith", value: e, ...Z.errToObj(t)}) + } + + endsWith(e, t) { + return this._addCheck({kind: "endsWith", value: e, ...Z.errToObj(t)}) + } + + min(e, t) { + return this._addCheck({kind: "min", value: e, ...Z.errToObj(t)}) + } + + max(e, t) { + return this._addCheck({kind: "max", value: e, ...Z.errToObj(t)}) + } + + length(e, t) { + return this._addCheck({kind: "length", value: e, ...Z.errToObj(t)}) + } + + nonempty(e) { + return this.min(1, Z.errToObj(e)) + } + + trim() { + return new F({...this._def, checks: [...this._def.checks, {kind: "trim"}]}) + } + + toLowerCase() { + return new F({...this._def, checks: [...this._def.checks, {kind: "toLowerCase"}]}) + } + + toUpperCase() { + return new F({...this._def, checks: [...this._def.checks, {kind: "toUpperCase"}]}) + } + + get isDatetime() { + return !!this._def.checks.find((e => "datetime" === e.kind)) + } + + get isDate() { + return !!this._def.checks.find((e => "date" === e.kind)) + } + + get isTime() { + return !!this._def.checks.find((e => "time" === e.kind)) + } + + get isDuration() { + return !!this._def.checks.find((e => "duration" === e.kind)) + } + + get isEmail() { + return !!this._def.checks.find((e => "email" === e.kind)) + } + + get isURL() { + return !!this._def.checks.find((e => "url" === e.kind)) + } + + get isEmoji() { + return !!this._def.checks.find((e => "emoji" === e.kind)) + } + + get isUUID() { + return !!this._def.checks.find((e => "uuid" === e.kind)) + } + + get isNANOID() { + return !!this._def.checks.find((e => "nanoid" === e.kind)) + } + + get isCUID() { + return !!this._def.checks.find((e => "cuid" === e.kind)) + } + + get isCUID2() { + return !!this._def.checks.find((e => "cuid2" === e.kind)) + } + + get isULID() { + return !!this._def.checks.find((e => "ulid" === e.kind)) + } + + get isIP() { + return !!this._def.checks.find((e => "ip" === e.kind)) + } + + get isBase64() { + return !!this._def.checks.find((e => "base64" === e.kind)) + } + + get minLength() { + let e = null; + for (const t of this._def.checks) "min" === t.kind && (null === e || t.value > e) && (e = t.value); + return e + } + + get maxLength() { + let e = null; + for (const t of this._def.checks) "max" === t.kind && (null === e || t.value < e) && (e = t.value); + return e + } +} + +function q(e, t) { + const a = (e.toString().split(".")[1] || "").length, s = (t.toString().split(".")[1] || "").length, n = a > s ? a : s; + return parseInt(e.toFixed(n).replace(".", "")) % parseInt(t.toFixed(n).replace(".", "")) / Math.pow(10, n) +} + +F.create = e => { + var t; + return new F({ + checks: [], + typeName: $e.ZodString, + coerce: null !== (t = null == e ? void 0 : e.coerce) && void 0 !== t && t, ...S(e) + }) +}; + +class J extends E { + constructor() { + super(...arguments), this.min = this.gte, this.max = this.lte, this.step = this.multipleOf + } + + _parse(t) { + this._def.coerce && (t.data = Number(t.data)); + if (this._getType(t) !== a.number) { + const e = this._getOrReturnCtx(t); + return p(e, {code: n.invalid_type, expected: a.number, received: e.parsedType}), f + } + let s; + const r = new m; + for (const a of this._def.checks) if ("int" === a.kind) e.isInteger(t.data) || (s = this._getOrReturnCtx(t, s), p(s, { + code: n.invalid_type, + expected: "integer", + received: "float", + message: a.message + }), r.dirty()); else if ("min" === a.kind) { + (a.inclusive ? t.data < a.value : t.data <= a.value) && (s = this._getOrReturnCtx(t, s), p(s, { + code: n.too_small, + minimum: a.value, + type: "number", + inclusive: a.inclusive, + exact: !1, + message: a.message + }), r.dirty()) + } else if ("max" === a.kind) { + (a.inclusive ? t.data > a.value : t.data >= a.value) && (s = this._getOrReturnCtx(t, s), p(s, { + code: n.too_big, + maximum: a.value, + type: "number", + inclusive: a.inclusive, + exact: !1, + message: a.message + }), r.dirty()) + } else "multipleOf" === a.kind ? 0 !== q(t.data, a.value) && (s = this._getOrReturnCtx(t, s), p(s, { + code: n.not_multiple_of, + multipleOf: a.value, + message: a.message + }), r.dirty()) : "finite" === a.kind ? Number.isFinite(t.data) || (s = this._getOrReturnCtx(t, s), p(s, { + code: n.not_finite, + message: a.message + }), r.dirty()) : e.assertNever(a); + return {status: r.value, value: t.data} + } + + gte(e, t) { + return this.setLimit("min", e, !0, Z.toString(t)) + } + + gt(e, t) { + return this.setLimit("min", e, !1, Z.toString(t)) + } + + lte(e, t) { + return this.setLimit("max", e, !0, Z.toString(t)) + } + + lt(e, t) { + return this.setLimit("max", e, !1, Z.toString(t)) + } + + setLimit(e, t, a, s) { + return new J({ + ...this._def, + checks: [...this._def.checks, {kind: e, value: t, inclusive: a, message: Z.toString(s)}] + }) + } + + _addCheck(e) { + return new J({...this._def, checks: [...this._def.checks, e]}) + } + + int(e) { + return this._addCheck({kind: "int", message: Z.toString(e)}) + } + + positive(e) { + return this._addCheck({kind: "min", value: 0, inclusive: !1, message: Z.toString(e)}) + } + + negative(e) { + return this._addCheck({kind: "max", value: 0, inclusive: !1, message: Z.toString(e)}) + } + + nonpositive(e) { + return this._addCheck({kind: "max", value: 0, inclusive: !0, message: Z.toString(e)}) + } + + nonnegative(e) { + return this._addCheck({kind: "min", value: 0, inclusive: !0, message: Z.toString(e)}) + } + + multipleOf(e, t) { + return this._addCheck({kind: "multipleOf", value: e, message: Z.toString(t)}) + } + + finite(e) { + return this._addCheck({kind: "finite", message: Z.toString(e)}) + } + + safe(e) { + return this._addCheck({ + kind: "min", + inclusive: !0, + value: Number.MIN_SAFE_INTEGER, + message: Z.toString(e) + })._addCheck({kind: "max", inclusive: !0, value: Number.MAX_SAFE_INTEGER, message: Z.toString(e)}) + } + + get minValue() { + let e = null; + for (const t of this._def.checks) "min" === t.kind && (null === e || t.value > e) && (e = t.value); + return e + } + + get maxValue() { + let e = null; + for (const t of this._def.checks) "max" === t.kind && (null === e || t.value < e) && (e = t.value); + return e + } + + get isInt() { + return !!this._def.checks.find((t => "int" === t.kind || "multipleOf" === t.kind && e.isInteger(t.value))) + } + + get isFinite() { + let e = null, t = null; + for (const a of this._def.checks) { + if ("finite" === a.kind || "int" === a.kind || "multipleOf" === a.kind) return !0; + "min" === a.kind ? (null === t || a.value > t) && (t = a.value) : "max" === a.kind && (null === e || a.value < e) && (e = a.value) + } + return Number.isFinite(t) && Number.isFinite(e) + } +} + +J.create = e => new J({checks: [], typeName: $e.ZodNumber, coerce: (null == e ? void 0 : e.coerce) || !1, ...S(e)}); + +class Y extends E { + constructor() { + super(...arguments), this.min = this.gte, this.max = this.lte + } + + _parse(t) { + this._def.coerce && (t.data = BigInt(t.data)); + if (this._getType(t) !== a.bigint) { + const e = this._getOrReturnCtx(t); + return p(e, {code: n.invalid_type, expected: a.bigint, received: e.parsedType}), f + } + let s; + const r = new m; + for (const a of this._def.checks) if ("min" === a.kind) { + (a.inclusive ? t.data < a.value : t.data <= a.value) && (s = this._getOrReturnCtx(t, s), p(s, { + code: n.too_small, + type: "bigint", + minimum: a.value, + inclusive: a.inclusive, + message: a.message + }), r.dirty()) + } else if ("max" === a.kind) { + (a.inclusive ? t.data > a.value : t.data >= a.value) && (s = this._getOrReturnCtx(t, s), p(s, { + code: n.too_big, + type: "bigint", + maximum: a.value, + inclusive: a.inclusive, + message: a.message + }), r.dirty()) + } else "multipleOf" === a.kind ? t.data % a.value !== BigInt(0) && (s = this._getOrReturnCtx(t, s), p(s, { + code: n.not_multiple_of, + multipleOf: a.value, + message: a.message + }), r.dirty()) : e.assertNever(a); + return {status: r.value, value: t.data} + } + + gte(e, t) { + return this.setLimit("min", e, !0, Z.toString(t)) + } + + gt(e, t) { + return this.setLimit("min", e, !1, Z.toString(t)) + } + + lte(e, t) { + return this.setLimit("max", e, !0, Z.toString(t)) + } + + lt(e, t) { + return this.setLimit("max", e, !1, Z.toString(t)) + } + + setLimit(e, t, a, s) { + return new Y({ + ...this._def, + checks: [...this._def.checks, {kind: e, value: t, inclusive: a, message: Z.toString(s)}] + }) + } + + _addCheck(e) { + return new Y({...this._def, checks: [...this._def.checks, e]}) + } + + positive(e) { + return this._addCheck({kind: "min", value: BigInt(0), inclusive: !1, message: Z.toString(e)}) + } + + negative(e) { + return this._addCheck({kind: "max", value: BigInt(0), inclusive: !1, message: Z.toString(e)}) + } + + nonpositive(e) { + return this._addCheck({kind: "max", value: BigInt(0), inclusive: !0, message: Z.toString(e)}) + } + + nonnegative(e) { + return this._addCheck({kind: "min", value: BigInt(0), inclusive: !0, message: Z.toString(e)}) + } + + multipleOf(e, t) { + return this._addCheck({kind: "multipleOf", value: e, message: Z.toString(t)}) + } + + get minValue() { + let e = null; + for (const t of this._def.checks) "min" === t.kind && (null === e || t.value > e) && (e = t.value); + return e + } + + get maxValue() { + let e = null; + for (const t of this._def.checks) "max" === t.kind && (null === e || t.value < e) && (e = t.value); + return e + } +} + +Y.create = e => { + var t; + return new Y({ + checks: [], + typeName: $e.ZodBigInt, + coerce: null !== (t = null == e ? void 0 : e.coerce) && void 0 !== t && t, ...S(e) + }) +}; + +class H extends E { + _parse(e) { + this._def.coerce && (e.data = Boolean(e.data)); + if (this._getType(e) !== a.boolean) { + const t = this._getOrReturnCtx(e); + return p(t, {code: n.invalid_type, expected: a.boolean, received: t.parsedType}), f + } + return _(e.data) + } +} + +H.create = e => new H({typeName: $e.ZodBoolean, coerce: (null == e ? void 0 : e.coerce) || !1, ...S(e)}); + +class G extends E { + _parse(t) { + this._def.coerce && (t.data = new Date(t.data)); + if (this._getType(t) !== a.date) { + const e = this._getOrReturnCtx(t); + return p(e, {code: n.invalid_type, expected: a.date, received: e.parsedType}), f + } + if (isNaN(t.data.getTime())) { + return p(this._getOrReturnCtx(t), {code: n.invalid_date}), f + } + const s = new m; + let r; + for (const a of this._def.checks) "min" === a.kind ? t.data.getTime() < a.value && (r = this._getOrReturnCtx(t, r), p(r, { + code: n.too_small, + message: a.message, + inclusive: !0, + exact: !1, + minimum: a.value, + type: "date" + }), s.dirty()) : "max" === a.kind ? t.data.getTime() > a.value && (r = this._getOrReturnCtx(t, r), p(r, { + code: n.too_big, + message: a.message, + inclusive: !0, + exact: !1, + maximum: a.value, + type: "date" + }), s.dirty()) : e.assertNever(a); + return {status: s.value, value: new Date(t.data.getTime())} + } + + _addCheck(e) { + return new G({...this._def, checks: [...this._def.checks, e]}) + } + + min(e, t) { + return this._addCheck({kind: "min", value: e.getTime(), message: Z.toString(t)}) + } + + max(e, t) { + return this._addCheck({kind: "max", value: e.getTime(), message: Z.toString(t)}) + } + + get minDate() { + let e = null; + for (const t of this._def.checks) "min" === t.kind && (null === e || t.value > e) && (e = t.value); + return null != e ? new Date(e) : null + } + + get maxDate() { + let e = null; + for (const t of this._def.checks) "max" === t.kind && (null === e || t.value < e) && (e = t.value); + return null != e ? new Date(e) : null + } +} + +G.create = e => new G({checks: [], coerce: (null == e ? void 0 : e.coerce) || !1, typeName: $e.ZodDate, ...S(e)}); + +class X extends E { + _parse(e) { + if (this._getType(e) !== a.symbol) { + const t = this._getOrReturnCtx(e); + return p(t, {code: n.invalid_type, expected: a.symbol, received: t.parsedType}), f + } + return _(e.data) + } +} + +X.create = e => new X({typeName: $e.ZodSymbol, ...S(e)}); + +class Q extends E { + _parse(e) { + if (this._getType(e) !== a.undefined) { + const t = this._getOrReturnCtx(e); + return p(t, {code: n.invalid_type, expected: a.undefined, received: t.parsedType}), f + } + return _(e.data) + } +} + +Q.create = e => new Q({typeName: $e.ZodUndefined, ...S(e)}); + +class ee extends E { + _parse(e) { + if (this._getType(e) !== a.null) { + const t = this._getOrReturnCtx(e); + return p(t, {code: n.invalid_type, expected: a.null, received: t.parsedType}), f + } + return _(e.data) + } +} + +ee.create = e => new ee({typeName: $e.ZodNull, ...S(e)}); + +class te extends E { + constructor() { + super(...arguments), this._any = !0 + } + + _parse(e) { + return _(e.data) + } +} + +te.create = e => new te({typeName: $e.ZodAny, ...S(e)}); + +class ae extends E { + constructor() { + super(...arguments), this._unknown = !0 + } + + _parse(e) { + return _(e.data) + } +} + +ae.create = e => new ae({typeName: $e.ZodUnknown, ...S(e)}); + +class se extends E { + _parse(e) { + const t = this._getOrReturnCtx(e); + return p(t, {code: n.invalid_type, expected: a.never, received: t.parsedType}), f + } +} + +se.create = e => new se({typeName: $e.ZodNever, ...S(e)}); + +class ne extends E { + _parse(e) { + if (this._getType(e) !== a.undefined) { + const t = this._getOrReturnCtx(e); + return p(t, {code: n.invalid_type, expected: a.void, received: t.parsedType}), f + } + return _(e.data) + } +} + +ne.create = e => new ne({typeName: $e.ZodVoid, ...S(e)}); + +class re extends E { + _parse(e) { + const {ctx: t, status: s} = this._processInputParams(e), r = this._def; + if (t.parsedType !== a.array) return p(t, {code: n.invalid_type, expected: a.array, received: t.parsedType}), f; + if (null !== r.exactLength) { + const e = t.data.length > r.exactLength.value, a = t.data.length < r.exactLength.value; + (e || a) && (p(t, { + code: e ? n.too_big : n.too_small, + minimum: a ? r.exactLength.value : void 0, + maximum: e ? r.exactLength.value : void 0, + type: "array", + inclusive: !0, + exact: !0, + message: r.exactLength.message + }), s.dirty()) + } + if (null !== r.minLength && t.data.length < r.minLength.value && (p(t, { + code: n.too_small, + minimum: r.minLength.value, + type: "array", + inclusive: !0, + exact: !1, + message: r.minLength.message + }), s.dirty()), null !== r.maxLength && t.data.length > r.maxLength.value && (p(t, { + code: n.too_big, + maximum: r.maxLength.value, + type: "array", + inclusive: !0, + exact: !1, + message: r.maxLength.message + }), s.dirty()), t.common.async) return Promise.all([...t.data].map(((e, a) => r.type._parseAsync(new C(t, e, t.path, a))))).then((e => m.mergeArray(s, e))); + const i = [...t.data].map(((e, a) => r.type._parseSync(new C(t, e, t.path, a)))); + return m.mergeArray(s, i) + } + + get element() { + return this._def.type + } + + min(e, t) { + return new re({...this._def, minLength: {value: e, message: Z.toString(t)}}) + } + + max(e, t) { + return new re({...this._def, maxLength: {value: e, message: Z.toString(t)}}) + } + + length(e, t) { + return new re({...this._def, exactLength: {value: e, message: Z.toString(t)}}) + } + + nonempty(e) { + return this.min(1, e) + } +} + +function ie(e) { + if (e instanceof oe) { + const t = {}; + for (const a in e.shape) { + const s = e.shape[a]; + t[a] = Te.create(ie(s)) + } + return new oe({...e._def, shape: () => t}) + } + return e instanceof re ? new re({ + ...e._def, + type: ie(e.element) + }) : e instanceof Te ? Te.create(ie(e.unwrap())) : e instanceof Oe ? Oe.create(ie(e.unwrap())) : e instanceof pe ? pe.create(e.items.map((e => ie(e)))) : e +} + +re.create = (e, t) => new re({ + type: e, + minLength: null, + maxLength: null, + exactLength: null, + typeName: $e.ZodArray, ...S(t) +}); + +class oe extends E { + constructor() { + super(...arguments), this._cached = null, this.nonstrict = this.passthrough, this.augment = this.extend + } + + _getCached() { + if (null !== this._cached) return this._cached; + const t = this._def.shape(), a = e.objectKeys(t); + return this._cached = {shape: t, keys: a} + } + + _parse(e) { + if (this._getType(e) !== a.object) { + const t = this._getOrReturnCtx(e); + return p(t, {code: n.invalid_type, expected: a.object, received: t.parsedType}), f + } + const {status: t, ctx: s} = this._processInputParams(e), {shape: r, keys: i} = this._getCached(), o = []; + if (!(this._def.catchall instanceof se && "strip" === this._def.unknownKeys)) for (const e in s.data) i.includes(e) || o.push(e); + const d = []; + for (const e of i) { + const t = r[e], a = s.data[e]; + d.push({key: {status: "valid", value: e}, value: t._parse(new C(s, a, s.path, e)), alwaysSet: e in s.data}) + } + if (this._def.catchall instanceof se) { + const e = this._def.unknownKeys; + if ("passthrough" === e) for (const e of o) d.push({ + key: {status: "valid", value: e}, + value: {status: "valid", value: s.data[e]} + }); else if ("strict" === e) o.length > 0 && (p(s, { + code: n.unrecognized_keys, + keys: o + }), t.dirty()); else if ("strip" !== e) throw new Error("Internal ZodObject error: invalid unknownKeys value.") + } else { + const e = this._def.catchall; + for (const t of o) { + const a = s.data[t]; + d.push({key: {status: "valid", value: t}, value: e._parse(new C(s, a, s.path, t)), alwaysSet: t in s.data}) + } + } + return s.common.async ? Promise.resolve().then((async () => { + const e = []; + for (const t of d) { + const a = await t.key, s = await t.value; + e.push({key: a, value: s, alwaysSet: t.alwaysSet}) + } + return e + })).then((e => m.mergeObjectSync(t, e))) : m.mergeObjectSync(t, d) + } + + get shape() { + return this._def.shape() + } + + strict(e) { + return Z.errToObj, new oe({ + ...this._def, unknownKeys: "strict", ...void 0 !== e ? { + errorMap: (t, a) => { + var s, n, r, i; + const o = null !== (r = null === (n = (s = this._def).errorMap) || void 0 === n ? void 0 : n.call(s, t, a).message) && void 0 !== r ? r : a.defaultError; + return "unrecognized_keys" === t.code ? {message: null !== (i = Z.errToObj(e).message) && void 0 !== i ? i : o} : {message: o} + } + } : {} + }) + } + + strip() { + return new oe({...this._def, unknownKeys: "strip"}) + } + + passthrough() { + return new oe({...this._def, unknownKeys: "passthrough"}) + } + + extend(e) { + return new oe({...this._def, shape: () => ({...this._def.shape(), ...e})}) + } + + merge(e) { + return new oe({ + unknownKeys: e._def.unknownKeys, + catchall: e._def.catchall, + shape: () => ({...this._def.shape(), ...e._def.shape()}), + typeName: $e.ZodObject + }) + } + + setKey(e, t) { + return this.augment({[e]: t}) + } + + catchall(e) { + return new oe({...this._def, catchall: e}) + } + + pick(t) { + const a = {}; + return e.objectKeys(t).forEach((e => { + t[e] && this.shape[e] && (a[e] = this.shape[e]) + })), new oe({...this._def, shape: () => a}) + } + + omit(t) { + const a = {}; + return e.objectKeys(this.shape).forEach((e => { + t[e] || (a[e] = this.shape[e]) + })), new oe({...this._def, shape: () => a}) + } + + deepPartial() { + return ie(this) + } + + partial(t) { + const a = {}; + return e.objectKeys(this.shape).forEach((e => { + const s = this.shape[e]; + t && !t[e] ? a[e] = s : a[e] = s.optional() + })), new oe({...this._def, shape: () => a}) + } + + required(t) { + const a = {}; + return e.objectKeys(this.shape).forEach((e => { + if (t && !t[e]) a[e] = this.shape[e]; else { + let t = this.shape[e]; + for (; t instanceof Te;) t = t._def.innerType; + a[e] = t + } + })), new oe({...this._def, shape: () => a}) + } + + keyof() { + return ke(e.objectKeys(this.shape)) + } +} + +oe.create = (e, t) => new oe({ + shape: () => e, + unknownKeys: "strip", + catchall: se.create(), + typeName: $e.ZodObject, ...S(t) +}), oe.strictCreate = (e, t) => new oe({ + shape: () => e, + unknownKeys: "strict", + catchall: se.create(), + typeName: $e.ZodObject, ...S(t) +}), oe.lazycreate = (e, t) => new oe({ + shape: e, + unknownKeys: "strip", + catchall: se.create(), + typeName: $e.ZodObject, ...S(t) +}); + +class de extends E { + _parse(e) { + const {ctx: t} = this._processInputParams(e), a = this._def.options; + if (t.common.async) return Promise.all(a.map((async e => { + const a = {...t, common: {...t.common, issues: []}, parent: null}; + return {result: await e._parseAsync({data: t.data, path: t.path, parent: a}), ctx: a} + }))).then((function (e) { + for (const t of e) if ("valid" === t.result.status) return t.result; + for (const a of e) if ("dirty" === a.result.status) return t.common.issues.push(...a.ctx.common.issues), a.result; + const a = e.map((e => new i(e.ctx.common.issues))); + return p(t, {code: n.invalid_union, unionErrors: a}), f + })); + { + let e; + const s = []; + for (const n of a) { + const a = {...t, common: {...t.common, issues: []}, parent: null}, + r = n._parseSync({data: t.data, path: t.path, parent: a}); + if ("valid" === r.status) return r; + "dirty" !== r.status || e || (e = {result: r, ctx: a}), a.common.issues.length && s.push(a.common.issues) + } + if (e) return t.common.issues.push(...e.ctx.common.issues), e.result; + const r = s.map((e => new i(e))); + return p(t, {code: n.invalid_union, unionErrors: r}), f + } + } + + get options() { + return this._def.options + } +} + +de.create = (e, t) => new de({options: e, typeName: $e.ZodUnion, ...S(t)}); +const ce = t => t instanceof ve ? ce(t.schema) : t instanceof Ze ? ce(t.innerType()) : t instanceof ge ? [t.value] : t instanceof be ? t.options : t instanceof xe ? e.objectValues(t.enum) : t instanceof Ce ? ce(t._def.innerType) : t instanceof Q ? [void 0] : t instanceof ee ? [null] : t instanceof Te ? [void 0, ...ce(t.unwrap())] : t instanceof Oe ? [null, ...ce(t.unwrap())] : t instanceof je || t instanceof Pe ? ce(t.unwrap()) : t instanceof Ne ? ce(t._def.innerType) : []; + +class ue extends E { + _parse(e) { + const {ctx: t} = this._processInputParams(e); + if (t.parsedType !== a.object) return p(t, {code: n.invalid_type, expected: a.object, received: t.parsedType}), f; + const s = this.discriminator, r = t.data[s], i = this.optionsMap.get(r); + return i ? t.common.async ? i._parseAsync({data: t.data, path: t.path, parent: t}) : i._parseSync({ + data: t.data, + path: t.path, + parent: t + }) : (p(t, {code: n.invalid_union_discriminator, options: Array.from(this.optionsMap.keys()), path: [s]}), f) + } + + get discriminator() { + return this._def.discriminator + } + + get options() { + return this._def.options + } + + get optionsMap() { + return this._def.optionsMap + } + + static create(e, t, a) { + const s = new Map; + for (const a of t) { + const t = ce(a.shape[e]); + if (!t.length) throw new Error(`A discriminator value for key \`${e}\` could not be extracted from all schema options`); + for (const n of t) { + if (s.has(n)) throw new Error(`Discriminator property ${String(e)} has duplicate value ${String(n)}`); + s.set(n, a) + } + } + return new ue({typeName: $e.ZodDiscriminatedUnion, discriminator: e, options: t, optionsMap: s, ...S(a)}) + } +} + +function le(t, n) { + const r = s(t), i = s(n); + if (t === n) return {valid: !0, data: t}; + if (r === a.object && i === a.object) { + const a = e.objectKeys(n), s = e.objectKeys(t).filter((e => -1 !== a.indexOf(e))), r = {...t, ...n}; + for (const e of s) { + const a = le(t[e], n[e]); + if (!a.valid) return {valid: !1}; + r[e] = a.data + } + return {valid: !0, data: r} + } + if (r === a.array && i === a.array) { + if (t.length !== n.length) return {valid: !1}; + const e = []; + for (let a = 0; a < t.length; a++) { + const s = le(t[a], n[a]); + if (!s.valid) return {valid: !1}; + e.push(s.data) + } + return {valid: !0, data: e} + } + return r === a.date && i === a.date && +t == +n ? {valid: !0, data: t} : {valid: !1} +} + +class he extends E { + _parse(e) { + const {status: t, ctx: a} = this._processInputParams(e), s = (e, s) => { + if (v(e) || v(s)) return f; + const r = le(e.value, s.value); + return r.valid ? ((g(e) || g(s)) && t.dirty(), { + status: t.value, + value: r.data + }) : (p(a, {code: n.invalid_intersection_types}), f) + }; + return a.common.async ? Promise.all([this._def.left._parseAsync({ + data: a.data, + path: a.path, + parent: a + }), this._def.right._parseAsync({ + data: a.data, + path: a.path, + parent: a + })]).then((([e, t]) => s(e, t))) : s(this._def.left._parseSync({ + data: a.data, + path: a.path, + parent: a + }), this._def.right._parseSync({data: a.data, path: a.path, parent: a})) + } +} + +he.create = (e, t, a) => new he({left: e, right: t, typeName: $e.ZodIntersection, ...S(a)}); + +class pe extends E { + _parse(e) { + const {status: t, ctx: s} = this._processInputParams(e); + if (s.parsedType !== a.array) return p(s, {code: n.invalid_type, expected: a.array, received: s.parsedType}), f; + if (s.data.length < this._def.items.length) return p(s, { + code: n.too_small, + minimum: this._def.items.length, + inclusive: !0, + exact: !1, + type: "array" + }), f; + !this._def.rest && s.data.length > this._def.items.length && (p(s, { + code: n.too_big, + maximum: this._def.items.length, + inclusive: !0, + exact: !1, + type: "array" + }), t.dirty()); + const r = [...s.data].map(((e, t) => { + const a = this._def.items[t] || this._def.rest; + return a ? a._parse(new C(s, e, s.path, t)) : null + })).filter((e => !!e)); + return s.common.async ? Promise.all(r).then((e => m.mergeArray(t, e))) : m.mergeArray(t, r) + } + + get items() { + return this._def.items + } + + rest(e) { + return new pe({...this._def, rest: e}) + } +} + +pe.create = (e, t) => { + if (!Array.isArray(e)) throw new Error("You must pass an array of schemas to z.tuple([ ... ])"); + return new pe({items: e, typeName: $e.ZodTuple, rest: null, ...S(t)}) +}; + +class me extends E { + get keySchema() { + return this._def.keyType + } + + get valueSchema() { + return this._def.valueType + } + + _parse(e) { + const {status: t, ctx: s} = this._processInputParams(e); + if (s.parsedType !== a.object) return p(s, {code: n.invalid_type, expected: a.object, received: s.parsedType}), f; + const r = [], i = this._def.keyType, o = this._def.valueType; + for (const e in s.data) r.push({ + key: i._parse(new C(s, e, s.path, e)), + value: o._parse(new C(s, s.data[e], s.path, e)), + alwaysSet: e in s.data + }); + return s.common.async ? m.mergeObjectAsync(t, r) : m.mergeObjectSync(t, r) + } + + get element() { + return this._def.valueType + } + + static create(e, t, a) { + return new me(t instanceof E ? {keyType: e, valueType: t, typeName: $e.ZodRecord, ...S(a)} : { + keyType: F.create(), + valueType: e, + typeName: $e.ZodRecord, ...S(t) + }) + } +} + +class fe extends E { + get keySchema() { + return this._def.keyType + } + + get valueSchema() { + return this._def.valueType + } + + _parse(e) { + const {status: t, ctx: s} = this._processInputParams(e); + if (s.parsedType !== a.map) return p(s, {code: n.invalid_type, expected: a.map, received: s.parsedType}), f; + const r = this._def.keyType, i = this._def.valueType, o = [...s.data.entries()].map((([e, t], a) => ({ + key: r._parse(new C(s, e, s.path, [a, "key"])), + value: i._parse(new C(s, t, s.path, [a, "value"])) + }))); + if (s.common.async) { + const e = new Map; + return Promise.resolve().then((async () => { + for (const a of o) { + const s = await a.key, n = await a.value; + if ("aborted" === s.status || "aborted" === n.status) return f; + "dirty" !== s.status && "dirty" !== n.status || t.dirty(), e.set(s.value, n.value) + } + return {status: t.value, value: e} + })) + } + { + const e = new Map; + for (const a of o) { + const s = a.key, n = a.value; + if ("aborted" === s.status || "aborted" === n.status) return f; + "dirty" !== s.status && "dirty" !== n.status || t.dirty(), e.set(s.value, n.value) + } + return {status: t.value, value: e} + } + } +} + +fe.create = (e, t, a) => new fe({valueType: t, keyType: e, typeName: $e.ZodMap, ...S(a)}); + +class ye extends E { + _parse(e) { + const {status: t, ctx: s} = this._processInputParams(e); + if (s.parsedType !== a.set) return p(s, {code: n.invalid_type, expected: a.set, received: s.parsedType}), f; + const r = this._def; + null !== r.minSize && s.data.size < r.minSize.value && (p(s, { + code: n.too_small, + minimum: r.minSize.value, + type: "set", + inclusive: !0, + exact: !1, + message: r.minSize.message + }), t.dirty()), null !== r.maxSize && s.data.size > r.maxSize.value && (p(s, { + code: n.too_big, + maximum: r.maxSize.value, + type: "set", + inclusive: !0, + exact: !1, + message: r.maxSize.message + }), t.dirty()); + const i = this._def.valueType; + + function o(e) { + const a = new Set; + for (const s of e) { + if ("aborted" === s.status) return f; + "dirty" === s.status && t.dirty(), a.add(s.value) + } + return {status: t.value, value: a} + } + + const d = [...s.data.values()].map(((e, t) => i._parse(new C(s, e, s.path, t)))); + return s.common.async ? Promise.all(d).then((e => o(e))) : o(d) + } + + min(e, t) { + return new ye({...this._def, minSize: {value: e, message: Z.toString(t)}}) + } + + max(e, t) { + return new ye({...this._def, maxSize: {value: e, message: Z.toString(t)}}) + } + + size(e, t) { + return this.min(e, t).max(e, t) + } + + nonempty(e) { + return this.min(1, e) + } +} + +ye.create = (e, t) => new ye({valueType: e, minSize: null, maxSize: null, typeName: $e.ZodSet, ...S(t)}); + +class _e extends E { + constructor() { + super(...arguments), this.validate = this.implement + } + + _parse(e) { + const {ctx: t} = this._processInputParams(e); + if (t.parsedType !== a.function) return p(t, { + code: n.invalid_type, + expected: a.function, + received: t.parsedType + }), f; + + function s(e, a) { + return l({ + data: e, + path: t.path, + errorMaps: [t.common.contextualErrorMap, t.schemaErrorMap, u(), o].filter((e => !!e)), + issueData: {code: n.invalid_arguments, argumentsError: a} + }) + } + + function r(e, a) { + return l({ + data: e, + path: t.path, + errorMaps: [t.common.contextualErrorMap, t.schemaErrorMap, u(), o].filter((e => !!e)), + issueData: {code: n.invalid_return_type, returnTypeError: a} + }) + } + + const d = {errorMap: t.common.contextualErrorMap}, c = t.data; + if (this._def.returns instanceof we) { + const e = this; + return _((async function (...t) { + const a = new i([]), n = await e._def.args.parseAsync(t, d).catch((e => { + throw a.addIssue(s(t, e)), a + })), o = await Reflect.apply(c, this, n); + return await e._def.returns._def.type.parseAsync(o, d).catch((e => { + throw a.addIssue(r(o, e)), a + })) + })) + } + { + const e = this; + return _((function (...t) { + const a = e._def.args.safeParse(t, d); + if (!a.success) throw new i([s(t, a.error)]); + const n = Reflect.apply(c, this, a.data), o = e._def.returns.safeParse(n, d); + if (!o.success) throw new i([r(n, o.error)]); + return o.data + })) + } + } + + parameters() { + return this._def.args + } + + returnType() { + return this._def.returns + } + + args(...e) { + return new _e({...this._def, args: pe.create(e).rest(ae.create())}) + } + + returns(e) { + return new _e({...this._def, returns: e}) + } + + implement(e) { + return this.parse(e) + } + + strictImplement(e) { + return this.parse(e) + } + + static create(e, t, a) { + return new _e({ + args: e || pe.create([]).rest(ae.create()), + returns: t || ae.create(), + typeName: $e.ZodFunction, ...S(a) + }) + } +} + +class ve extends E { + get schema() { + return this._def.getter() + } + + _parse(e) { + const {ctx: t} = this._processInputParams(e); + return this._def.getter()._parse({data: t.data, path: t.path, parent: t}) + } +} + +ve.create = (e, t) => new ve({getter: e, typeName: $e.ZodLazy, ...S(t)}); + +class ge extends E { + _parse(e) { + if (e.data !== this._def.value) { + const t = this._getOrReturnCtx(e); + return p(t, {received: t.data, code: n.invalid_literal, expected: this._def.value}), f + } + return {status: "valid", value: e.data} + } + + get value() { + return this._def.value + } +} + +function ke(e, t) { + return new be({values: e, typeName: $e.ZodEnum, ...S(t)}) +} + +ge.create = (e, t) => new ge({value: e, typeName: $e.ZodLiteral, ...S(t)}); + +class be extends E { + constructor() { + super(...arguments), T.set(this, void 0) + } + + _parse(t) { + if ("string" != typeof t.data) { + const a = this._getOrReturnCtx(t), s = this._def.values; + return p(a, {expected: e.joinValues(s), received: a.parsedType, code: n.invalid_type}), f + } + if (x(this, T, "f") || w(this, T, new Set(this._def.values), "f"), !x(this, T, "f").has(t.data)) { + const e = this._getOrReturnCtx(t), a = this._def.values; + return p(e, {received: e.data, code: n.invalid_enum_value, options: a}), f + } + return _(t.data) + } + + get options() { + return this._def.values + } + + get enum() { + const e = {}; + for (const t of this._def.values) e[t] = t; + return e + } + + get Values() { + const e = {}; + for (const t of this._def.values) e[t] = t; + return e + } + + get Enum() { + const e = {}; + for (const t of this._def.values) e[t] = t; + return e + } + + extract(e, t = this._def) { + return be.create(e, {...this._def, ...t}) + } + + exclude(e, t = this._def) { + return be.create(this.options.filter((t => !e.includes(t))), {...this._def, ...t}) + } +} + +T = new WeakMap, be.create = ke; + +class xe extends E { + constructor() { + super(...arguments), O.set(this, void 0) + } + + _parse(t) { + const s = e.getValidEnumValues(this._def.values), r = this._getOrReturnCtx(t); + if (r.parsedType !== a.string && r.parsedType !== a.number) { + const t = e.objectValues(s); + return p(r, {expected: e.joinValues(t), received: r.parsedType, code: n.invalid_type}), f + } + if (x(this, O, "f") || w(this, O, new Set(e.getValidEnumValues(this._def.values)), "f"), !x(this, O, "f").has(t.data)) { + const t = e.objectValues(s); + return p(r, {received: r.data, code: n.invalid_enum_value, options: t}), f + } + return _(t.data) + } + + get enum() { + return this._def.values + } +} + +O = new WeakMap, xe.create = (e, t) => new xe({values: e, typeName: $e.ZodNativeEnum, ...S(t)}); + +class we extends E { + unwrap() { + return this._def.type + } + + _parse(e) { + const {ctx: t} = this._processInputParams(e); + if (t.parsedType !== a.promise && !1 === t.common.async) return p(t, { + code: n.invalid_type, + expected: a.promise, + received: t.parsedType + }), f; + const s = t.parsedType === a.promise ? t.data : Promise.resolve(t.data); + return _(s.then((e => this._def.type.parseAsync(e, {path: t.path, errorMap: t.common.contextualErrorMap})))) + } +} + +we.create = (e, t) => new we({type: e, typeName: $e.ZodPromise, ...S(t)}); + +class Ze extends E { + innerType() { + return this._def.schema + } + + sourceType() { + return this._def.schema._def.typeName === $e.ZodEffects ? this._def.schema.sourceType() : this._def.schema + } + + _parse(t) { + const {status: a, ctx: s} = this._processInputParams(t), n = this._def.effect || null, r = { + addIssue: e => { + p(s, e), e.fatal ? a.abort() : a.dirty() + }, get path() { + return s.path + } + }; + if (r.addIssue = r.addIssue.bind(r), "preprocess" === n.type) { + const e = n.transform(s.data, r); + if (s.common.async) return Promise.resolve(e).then((async e => { + if ("aborted" === a.value) return f; + const t = await this._def.schema._parseAsync({data: e, path: s.path, parent: s}); + return "aborted" === t.status ? f : "dirty" === t.status || "dirty" === a.value ? y(t.value) : t + })); + { + if ("aborted" === a.value) return f; + const t = this._def.schema._parseSync({data: e, path: s.path, parent: s}); + return "aborted" === t.status ? f : "dirty" === t.status || "dirty" === a.value ? y(t.value) : t + } + } + if ("refinement" === n.type) { + const e = e => { + const t = n.refinement(e, r); + if (s.common.async) return Promise.resolve(t); + if (t instanceof Promise) throw new Error("Async refinement encountered during synchronous parse operation. Use .parseAsync instead."); + return e + }; + if (!1 === s.common.async) { + const t = this._def.schema._parseSync({data: s.data, path: s.path, parent: s}); + return "aborted" === t.status ? f : ("dirty" === t.status && a.dirty(), e(t.value), { + status: a.value, + value: t.value + }) + } + return this._def.schema._parseAsync({ + data: s.data, + path: s.path, + parent: s + }).then((t => "aborted" === t.status ? f : ("dirty" === t.status && a.dirty(), e(t.value).then((() => ({ + status: a.value, + value: t.value + })))))) + } + if ("transform" === n.type) { + if (!1 === s.common.async) { + const e = this._def.schema._parseSync({data: s.data, path: s.path, parent: s}); + if (!k(e)) return e; + const t = n.transform(e.value, r); + if (t instanceof Promise) throw new Error("Asynchronous transform encountered during synchronous parse operation. Use .parseAsync instead."); + return {status: a.value, value: t} + } + return this._def.schema._parseAsync({ + data: s.data, + path: s.path, + parent: s + }).then((e => k(e) ? Promise.resolve(n.transform(e.value, r)).then((e => ({status: a.value, value: e}))) : e)) + } + e.assertNever(n) + } +} + +Ze.create = (e, t, a) => new Ze({ + schema: e, + typeName: $e.ZodEffects, + effect: t, ...S(a) +}), Ze.createWithPreprocess = (e, t, a) => new Ze({ + schema: t, + effect: {type: "preprocess", transform: e}, + typeName: $e.ZodEffects, ...S(a) +}); + +class Te extends E { + _parse(e) { + return this._getType(e) === a.undefined ? _(void 0) : this._def.innerType._parse(e) + } + + unwrap() { + return this._def.innerType + } +} + +Te.create = (e, t) => new Te({innerType: e, typeName: $e.ZodOptional, ...S(t)}); + +class Oe extends E { + _parse(e) { + return this._getType(e) === a.null ? _(null) : this._def.innerType._parse(e) + } + + unwrap() { + return this._def.innerType + } +} + +Oe.create = (e, t) => new Oe({innerType: e, typeName: $e.ZodNullable, ...S(t)}); + +class Ce extends E { + _parse(e) { + const {ctx: t} = this._processInputParams(e); + let s = t.data; + return t.parsedType === a.undefined && (s = this._def.defaultValue()), this._def.innerType._parse({ + data: s, + path: t.path, + parent: t + }) + } + + removeDefault() { + return this._def.innerType + } +} + +Ce.create = (e, t) => new Ce({ + innerType: e, + typeName: $e.ZodDefault, + defaultValue: "function" == typeof t.default ? t.default : () => t.default, ...S(t) +}); + +class Ne extends E { + _parse(e) { + const {ctx: t} = this._processInputParams(e), a = {...t, common: {...t.common, issues: []}}, + s = this._def.innerType._parse({data: a.data, path: a.path, parent: {...a}}); + return b(s) ? s.then((e => ({ + status: "valid", + value: "valid" === e.status ? e.value : this._def.catchValue({ + get error() { + return new i(a.common.issues) + }, input: a.data + }) + }))) : { + status: "valid", value: "valid" === s.status ? s.value : this._def.catchValue({ + get error() { + return new i(a.common.issues) + }, input: a.data + }) + } + } + + removeCatch() { + return this._def.innerType + } +} + +Ne.create = (e, t) => new Ne({ + innerType: e, + typeName: $e.ZodCatch, + catchValue: "function" == typeof t.catch ? t.catch : () => t.catch, ...S(t) +}); + +class Se extends E { + _parse(e) { + if (this._getType(e) !== a.nan) { + const t = this._getOrReturnCtx(e); + return p(t, {code: n.invalid_type, expected: a.nan, received: t.parsedType}), f + } + return {status: "valid", value: e.data} + } +} + +Se.create = e => new Se({typeName: $e.ZodNaN, ...S(e)}); +const Ee = Symbol("zod_brand"); + +class je extends E { + _parse(e) { + const {ctx: t} = this._processInputParams(e), a = t.data; + return this._def.type._parse({data: a, path: t.path, parent: t}) + } + + unwrap() { + return this._def.type + } +} + +class Ie extends E { + _parse(e) { + const {status: t, ctx: a} = this._processInputParams(e); + if (a.common.async) { + return (async () => { + const e = await this._def.in._parseAsync({data: a.data, path: a.path, parent: a}); + return "aborted" === e.status ? f : "dirty" === e.status ? (t.dirty(), y(e.value)) : this._def.out._parseAsync({ + data: e.value, + path: a.path, + parent: a + }) + })() + } + { + const e = this._def.in._parseSync({data: a.data, path: a.path, parent: a}); + return "aborted" === e.status ? f : "dirty" === e.status ? (t.dirty(), { + status: "dirty", + value: e.value + }) : this._def.out._parseSync({data: e.value, path: a.path, parent: a}) + } + } + + static create(e, t) { + return new Ie({in: e, out: t, typeName: $e.ZodPipeline}) + } +} + +class Pe extends E { + _parse(e) { + const t = this._def.innerType._parse(e), a = e => (k(e) && (e.value = Object.freeze(e.value)), e); + return b(t) ? t.then((e => a(e))) : a(t) + } + + unwrap() { + return this._def.innerType + } +} + +function Re(e, t = {}, a) { + return e ? te.create().superRefine(((s, n) => { + var r, i; + if (!e(s)) { + const e = "function" == typeof t ? t(s) : "string" == typeof t ? {message: t} : t, + o = null === (i = null !== (r = e.fatal) && void 0 !== r ? r : a) || void 0 === i || i, + d = "string" == typeof e ? {message: e} : e; + n.addIssue({code: "custom", ...d, fatal: o}) + } + })) : te.create() +} + +Pe.create = (e, t) => new Pe({innerType: e, typeName: $e.ZodReadonly, ...S(t)}); +const Ae = {object: oe.lazycreate}; +var $e; +!function (e) { + e.ZodString = "ZodString", e.ZodNumber = "ZodNumber", e.ZodNaN = "ZodNaN", e.ZodBigInt = "ZodBigInt", e.ZodBoolean = "ZodBoolean", e.ZodDate = "ZodDate", e.ZodSymbol = "ZodSymbol", e.ZodUndefined = "ZodUndefined", e.ZodNull = "ZodNull", e.ZodAny = "ZodAny", e.ZodUnknown = "ZodUnknown", e.ZodNever = "ZodNever", e.ZodVoid = "ZodVoid", e.ZodArray = "ZodArray", e.ZodObject = "ZodObject", e.ZodUnion = "ZodUnion", e.ZodDiscriminatedUnion = "ZodDiscriminatedUnion", e.ZodIntersection = "ZodIntersection", e.ZodTuple = "ZodTuple", e.ZodRecord = "ZodRecord", e.ZodMap = "ZodMap", e.ZodSet = "ZodSet", e.ZodFunction = "ZodFunction", e.ZodLazy = "ZodLazy", e.ZodLiteral = "ZodLiteral", e.ZodEnum = "ZodEnum", e.ZodEffects = "ZodEffects", e.ZodNativeEnum = "ZodNativeEnum", e.ZodOptional = "ZodOptional", e.ZodNullable = "ZodNullable", e.ZodDefault = "ZodDefault", e.ZodCatch = "ZodCatch", e.ZodPromise = "ZodPromise", e.ZodBranded = "ZodBranded", e.ZodPipeline = "ZodPipeline", e.ZodReadonly = "ZodReadonly" +}($e || ($e = {})); +const Me = (e, t = {message: `Input not instance of ${e.name}`}) => Re((t => t instanceof e), t), Le = F.create, + De = J.create, ze = Se.create, Ve = Y.create, Ue = H.create, Ke = G.create, Be = X.create, We = Q.create, + Fe = ee.create, qe = te.create, Je = ae.create, Ye = se.create, He = ne.create, Ge = re.create, Xe = oe.create, + Qe = oe.strictCreate, et = de.create, tt = ue.create, at = he.create, st = pe.create, nt = me.create, rt = fe.create, + it = ye.create, ot = _e.create, dt = ve.create, ct = ge.create, ut = be.create, lt = xe.create, ht = we.create, + pt = Ze.create, mt = Te.create, ft = Oe.create, yt = Ze.createWithPreprocess, _t = Ie.create, + vt = () => Le().optional(), gt = () => De().optional(), kt = () => Ue().optional(), bt = { + string: e => F.create({...e, coerce: !0}), + number: e => J.create({...e, coerce: !0}), + boolean: e => H.create({...e, coerce: !0}), + bigint: e => Y.create({...e, coerce: !0}), + date: e => G.create({...e, coerce: !0}) + }, xt = f; +var wt = Object.freeze({ + __proto__: null, + defaultErrorMap: o, + setErrorMap: c, + getErrorMap: u, + makeIssue: l, + EMPTY_PATH: h, + addIssueToContext: p, + ParseStatus: m, + INVALID: f, + DIRTY: y, + OK: _, + isAborted: v, + isDirty: g, + isValid: k, + isAsync: b, + get util() { + return e + }, + get objectUtil() { + return t + }, + ZodParsedType: a, + getParsedType: s, + ZodType: E, + datetimeRegex: W, + ZodString: F, + ZodNumber: J, + ZodBigInt: Y, + ZodBoolean: H, + ZodDate: G, + ZodSymbol: X, + ZodUndefined: Q, + ZodNull: ee, + ZodAny: te, + ZodUnknown: ae, + ZodNever: se, + ZodVoid: ne, + ZodArray: re, + ZodObject: oe, + ZodUnion: de, + ZodDiscriminatedUnion: ue, + ZodIntersection: he, + ZodTuple: pe, + ZodRecord: me, + ZodMap: fe, + ZodSet: ye, + ZodFunction: _e, + ZodLazy: ve, + ZodLiteral: ge, + ZodEnum: be, + ZodNativeEnum: xe, + ZodPromise: we, + ZodEffects: Ze, + ZodTransformer: Ze, + ZodOptional: Te, + ZodNullable: Oe, + ZodDefault: Ce, + ZodCatch: Ne, + ZodNaN: Se, + BRAND: Ee, + ZodBranded: je, + ZodPipeline: Ie, + ZodReadonly: Pe, + custom: Re, + Schema: E, + ZodSchema: E, + late: Ae, + get ZodFirstPartyTypeKind() { + return $e + }, + coerce: bt, + any: qe, + array: Ge, + bigint: Ve, + boolean: Ue, + date: Ke, + discriminatedUnion: tt, + effect: pt, + enum: ut, + function: ot, + instanceof: Me, + intersection: at, + lazy: dt, + literal: ct, + map: rt, + nan: ze, + nativeEnum: lt, + never: Ye, + null: Fe, + nullable: ft, + number: De, + object: Xe, + oboolean: kt, + onumber: gt, + optional: mt, + ostring: vt, + pipeline: _t, + preprocess: yt, + promise: ht, + record: nt, + set: it, + strictObject: Qe, + string: Le, + symbol: Be, + transformer: pt, + tuple: st, + undefined: We, + union: et, + unknown: Je, + void: He, + NEVER: xt, + ZodIssueCode: n, + quotelessJson: r, + ZodError: i +}); +export { + Ee as BRAND, + y as DIRTY, + h as EMPTY_PATH, + f as INVALID, + xt as NEVER, + _ as OK, + m as ParseStatus, + E as Schema, + te as ZodAny, + re as ZodArray, + Y as ZodBigInt, + H as ZodBoolean, + je as ZodBranded, + Ne as ZodCatch, + G as ZodDate, + Ce as ZodDefault, + ue as ZodDiscriminatedUnion, + Ze as ZodEffects, + be as ZodEnum, + i as ZodError, + $e as ZodFirstPartyTypeKind, + _e as ZodFunction, + he as ZodIntersection, + n as ZodIssueCode, + ve as ZodLazy, + ge as ZodLiteral, + fe as ZodMap, + Se as ZodNaN, + xe as ZodNativeEnum, + se as ZodNever, + ee as ZodNull, + Oe as ZodNullable, + J as ZodNumber, + oe as ZodObject, + Te as ZodOptional, + a as ZodParsedType, + Ie as ZodPipeline, + we as ZodPromise, + Pe as ZodReadonly, + me as ZodRecord, + E as ZodSchema, + ye as ZodSet, + F as ZodString, + X as ZodSymbol, + Ze as ZodTransformer, + pe as ZodTuple, + E as ZodType, + Q as ZodUndefined, + de as ZodUnion, + ae as ZodUnknown, + ne as ZodVoid, + p as addIssueToContext, + qe as any, + Ge as array, + Ve as bigint, + Ue as boolean, + bt as coerce, + Re as custom, + Ke as date, + W as datetimeRegex, + wt as default, + o as defaultErrorMap, + tt as discriminatedUnion, + pt as effect, + ut as enum, + ot as function, + u as getErrorMap, + s as getParsedType, + Me as instanceof, + at as intersection, + v as isAborted, + b as isAsync, + g as isDirty, + k as isValid, + Ae as late, + dt as lazy, + ct as literal, + l as makeIssue, + rt as map, + ze as nan, + lt as nativeEnum, + Ye as never, + Fe as null, + ft as nullable, + De as number, + Xe as object, + t as objectUtil, + kt as oboolean, + gt as onumber, + mt as optional, + vt as ostring, + _t as pipeline, + yt as preprocess, + ht as promise, + r as quotelessJson, + nt as record, + it as set, + c as setErrorMap, + Qe as strictObject, + Le as string, + Be as symbol, + pt as transformer, + st as tuple, + We as undefined, + et as union, + Je as unknown, + e as util, + He as void, + wt as z +}; +//# sourceMappingURL=/sm/6465f69a3f45d303d7cdccd2977f2a4521617110a452cdc4907f5e808726b930.map \ No newline at end of file diff --git a/core/engine/src/decision.rs b/core/engine/src/decision.rs index c8ea9d92..d044124e 100644 --- a/core/engine/src/decision.rs +++ b/core/engine/src/decision.rs @@ -5,7 +5,7 @@ use serde_json::Value; use crate::engine::EvaluationOptions; use crate::handler::custom_node_adapter::{CustomNodeAdapter, NoopCustomNode}; use crate::handler::graph::{DecisionGraph, DecisionGraphConfig, DecisionGraphResponse}; -use crate::loader::{DecisionLoader, NoopLoader}; +use crate::loader::{CachedLoader, DecisionLoader, NoopLoader}; use crate::model::DecisionContent; use crate::{DecisionGraphValidationError, EvaluationError}; @@ -13,8 +13,8 @@ use crate::{DecisionGraphValidationError, EvaluationError}; #[derive(Debug, Clone)] pub struct Decision where - Loader: DecisionLoader, - CustomNode: CustomNodeAdapter, + Loader: DecisionLoader + 'static, + CustomNode: CustomNodeAdapter + 'static, { content: Arc, loader: Arc, @@ -43,8 +43,8 @@ impl From> for Decision { impl Decision where - L: DecisionLoader, - A: CustomNodeAdapter, + L: DecisionLoader + 'static, + A: CustomNodeAdapter + 'static, { pub fn with_loader(self, loader: Arc) -> Decision where @@ -85,7 +85,7 @@ where let mut decision_graph = DecisionGraph::try_new(DecisionGraphConfig { max_depth: options.max_depth.unwrap_or(5), trace: options.trace.unwrap_or_default(), - loader: self.loader.clone(), + loader: Arc::new(CachedLoader::from(self.loader.clone())), adapter: self.adapter.clone(), iteration: 0, content: &self.content, @@ -98,7 +98,7 @@ where let decision_graph = DecisionGraph::try_new(DecisionGraphConfig { max_depth: 1, trace: false, - loader: self.loader.clone(), + loader: Arc::new(CachedLoader::from(self.loader.clone())), adapter: self.adapter.clone(), iteration: 0, content: &self.content, diff --git a/core/engine/src/engine.rs b/core/engine/src/engine.rs index 216d55f5..310072a8 100644 --- a/core/engine/src/engine.rs +++ b/core/engine/src/engine.rs @@ -14,8 +14,8 @@ use crate::EvaluationError; #[derive(Debug, Clone)] pub struct DecisionEngine where - Loader: DecisionLoader, - CustomNode: CustomNodeAdapter, + Loader: DecisionLoader + 'static, + CustomNode: CustomNodeAdapter + 'static, { loader: Arc, adapter: Arc, @@ -36,7 +36,7 @@ impl Default for DecisionEngine { } } -impl DecisionEngine { +impl DecisionEngine { pub fn new(loader: Arc, adapter: Arc) -> Self { Self { loader, adapter } } diff --git a/core/engine/src/handler/decision.rs b/core/engine/src/handler/decision.rs index 1aa760b0..4ce8cbeb 100644 --- a/core/engine/src/handler/decision.rs +++ b/core/engine/src/handler/decision.rs @@ -1,68 +1,82 @@ +use std::future::Future; +use std::ops::Deref; +use std::pin::Pin; +use std::rc::Rc; +use std::sync::Arc; + +use anyhow::{anyhow, Context}; + use crate::handler::custom_node_adapter::CustomNodeAdapter; +use crate::handler::function::function::Function; use crate::handler::graph::{DecisionGraph, DecisionGraphConfig}; use crate::handler::node::{NodeRequest, NodeResponse, NodeResult}; use crate::loader::DecisionLoader; use crate::model::DecisionNodeKind; -use anyhow::{anyhow, Context}; -use async_recursion::async_recursion; -use rquickjs::Runtime; -use std::ops::Deref; -use std::sync::Arc; -pub struct DecisionHandler { +pub struct DecisionHandler { trace: bool, loader: Arc, adapter: Arc, max_depth: u8, - js_runtime: Option, + js_function: Option>, } -impl DecisionHandler { +impl DecisionHandler { pub fn new( trace: bool, max_depth: u8, loader: Arc, adapter: Arc, - js_runtime: Option, + js_function: Option>, ) -> Self { Self { trace, loader, adapter, max_depth, - js_runtime, + js_function, } } - #[async_recursion(?Send)] - pub async fn handle(&self, request: &NodeRequest<'_>) -> NodeResult { - let content = match &request.node.kind { - DecisionNodeKind::DecisionNode { content } => Ok(content), - _ => Err(anyhow!("Unexpected node type")), - }?; + pub fn handle<'s, 'arg, 'recursion>( + &'s self, + request: &'arg NodeRequest<'_>, + ) -> Pin + 'recursion>> + where + 's: 'recursion, + 'arg: 'recursion, + { + Box::pin(async move { + let content = match &request.node.kind { + DecisionNodeKind::DecisionNode { content } => Ok(content), + _ => Err(anyhow!("Unexpected node type")), + }?; - let sub_decision = self.loader.load(&content.key).await?; - let mut sub_tree = DecisionGraph::try_new(DecisionGraphConfig { - content: sub_decision.deref(), - max_depth: self.max_depth, - loader: self.loader.clone(), - adapter: self.adapter.clone(), - iteration: request.iteration + 1, - trace: self.trace, - })? - .with_runtime(self.js_runtime.clone()); + let sub_decision = self.loader.load(&content.key).await?; + let mut sub_tree = DecisionGraph::try_new(DecisionGraphConfig { + content: sub_decision.deref(), + max_depth: self.max_depth, + loader: self.loader.clone(), + adapter: self.adapter.clone(), + iteration: request.iteration + 1, + trace: self.trace, + })? + .with_function(self.js_function.clone()); - let result = sub_tree - .evaluate(&request.input) - .await - .map_err(|e| e.source)?; + let result = sub_tree + .evaluate(&request.input) + .await + .map_err(|e| e.source)?; - Ok(NodeResponse { - output: result.result, - trace_data: self - .trace - .then(|| serde_json::to_value(result.trace).context("Failed to parse trace data")) - .transpose()?, + Ok(NodeResponse { + output: result.result, + trace_data: self + .trace + .then(|| { + serde_json::to_value(result.trace).context("Failed to parse trace data") + }) + .transpose()?, + }) }) } } diff --git a/core/engine/src/handler/function/error.rs b/core/engine/src/handler/function/error.rs new file mode 100644 index 00000000..cba49e7f --- /dev/null +++ b/core/engine/src/handler/function/error.rs @@ -0,0 +1,63 @@ +use std::fmt::{Display, Formatter}; + +use rquickjs::{CaughtError, Ctx, Error, Exception}; + +pub type FunctionResult = Result; + +#[derive(Debug)] +pub enum FunctionError { + Caught(String), + Runtime(Error), +} + +impl<'js> From> for FunctionError { + fn from(value: CaughtError<'js>) -> Self { + Self::Caught(value.to_string()) + } +} + +impl From 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 { + #[allow(dead_code)] + fn or_throw_msg(self, ctx: &Ctx, msg: &str) -> rquickjs::Result; + fn or_throw(self, ctx: &Ctx) -> rquickjs::Result; +} + +impl ResultExt for Result { + fn or_throw_msg(self, ctx: &Ctx, msg: &str) -> rquickjs::Result { + 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 { + self.map_err(|err| Exception::throw_message(ctx, &err.to_string())) + } +} + +impl ResultExt for Option { + fn or_throw_msg(self, ctx: &Ctx, msg: &str) -> rquickjs::Result { + self.ok_or(Exception::throw_message(ctx, msg)) + } + + fn or_throw(self, ctx: &Ctx) -> rquickjs::Result { + self.ok_or(Exception::throw_message(ctx, "Value is not present")) + } +} diff --git a/core/engine/src/handler/function/function.rs b/core/engine/src/handler/function/function.rs new file mode 100644 index 00000000..422fc5e1 --- /dev/null +++ b/core/engine/src/handler/function/function.rs @@ -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>>, +} + +pub struct Function { + rt: Arc, + ctx: AsyncContext, + listeners: Vec>, + module_loader: ModuleLoader, +} + +impl Function { + pub async fn create<'js>(config: FunctionConfig) -> FunctionResult { + 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 = 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 { + let k: FunctionResult = 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::().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, + pub data: Value, +} diff --git a/core/engine/src/handler/function/listener.rs b/core/engine/src/handler/function/listener.rs new file mode 100644 index 00000000..de6f3651 --- /dev/null +++ b/core/engine/src/handler/function/listener.rs @@ -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 + 'js>>; +} diff --git a/core/engine/src/handler/function/mod.rs b/core/engine/src/handler/function/mod.rs index 646789a4..77cfba99 100644 --- a/core/engine/src/handler/function/mod.rs +++ b/core/engine/src/handler/function/mod.rs @@ -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, } -static MAX_DURATION: Duration = Duration::from_millis(500); +pub struct FunctionHandler { + function: Rc, + 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, 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 + } } diff --git a/core/engine/src/handler/function/module/console.rs b/core/engine/src/handler/function/module/console.rs new file mode 100644 index 00000000..3cb5c589 --- /dev/null +++ b/core/engine/src/handler/function/module/console.rs @@ -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 + '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, + ms_since_run: usize, +} + +#[derive(rquickjs::class::Trace, Clone)] +#[rquickjs::class] +pub struct Console { + #[qjs(skip_trace)] + pub logs: RefCell>, + #[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 { + let obj: Self = ctx.globals().get("console")?; + Ok(obj) + } + + pub fn log<'js>(&self, ctx: Ctx<'js>, args: Rest>) -> 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::>>, _>>()?; + + let step2 = step1 + .into_iter() + .map(|s| s.map(|i| i.to_string()).transpose()) + .collect::>, _>>()?; + + let step3 = step2 + .into_iter() + .map(|s| s.unwrap_or_default()) + .collect::>(); + + 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(()) + } +} diff --git a/core/engine/src/handler/function/module/http.rs b/core/engine/src/handler/function/module/http.rs new file mode 100644 index 00000000..975fea84 --- /dev/null +++ b/core/engine/src/handler/function/module/http.rs @@ -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> { + 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, + config: Option, +) -> rquickjs::Result { + static HTTP_CLIENT: OnceLock = 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, +} + +impl<'js> FromJs<'js> for HttpConfig { + fn from_js(ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result { + let object = value.into_object().or_throw(ctx)?; + let headers_obj: Option> = 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.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> = 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, +) -> rquickjs::Result { + execute_http(ctx, Method::GET, url, None, config.0).await +} + +async fn post<'js>( + ctx: Ctx<'js>, + url: String, + data: JsValue, + config: Opt, +) -> rquickjs::Result { + execute_http(ctx, Method::POST, url, Some(data), config.0).await +} + +async fn patch<'js>( + ctx: Ctx<'js>, + url: String, + data: JsValue, + config: Opt, +) -> rquickjs::Result { + execute_http(ctx, Method::PATCH, url, Some(data), config.0).await +} + +async fn put<'js>( + ctx: Ctx<'js>, + url: String, + data: JsValue, + config: Opt, +) -> rquickjs::Result { + execute_http(ctx, Method::PUT, url, Some(data), config.0).await +} + +async fn delete<'js>( + ctx: Ctx<'js>, + url: String, + config: Opt, +) -> rquickjs::Result { + execute_http(ctx, Method::DELETE, url, None, config.0).await +} + +async fn head<'js>( + ctx: Ctx<'js>, + url: String, + config: Opt, +) -> rquickjs::Result { + 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(()) + }) + } +} diff --git a/core/engine/src/handler/function/module/mod.rs b/core/engine/src/handler/function/module/mod.rs new file mode 100644 index 00000000..180afb31 --- /dev/null +++ b/core/engine/src/handler/function/module/mod.rs @@ -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>); + +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 { + 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> { + let mut inner = self.0.borrow_mut(); + inner.deref_mut().load(ctx, name) + } +} + +struct BaseModuleLoader { + bundle: Bundle, + defined_modules: RefCell>, + 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 { + 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> { + 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::() { + let name = name?; + let value: rquickjs::Value = default.get(&name)?; + exports.export(name, value)?; + } + + exports.export("default", default)?; + + Ok(()) +} diff --git a/core/engine/src/handler/function/module/zen.rs b/core/engine/src/handler/function/module/zen.rs new file mode 100644 index 00000000..41e50aa6 --- /dev/null +++ b/core/engine/src/handler/function/module/zen.rs @@ -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 { + pub loader: Arc, + pub adapter: Arc, +} + +impl RuntimeListener + for ZenListener +{ + fn on_event<'js>( + &self, + ctx: Ctx<'js>, + event: RuntimeEvent, + ) -> Pin + '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>| { + 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 { + 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 { + 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>, +) -> rquickjs::Result> { + 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(()) + }) + } +} diff --git a/core/engine/src/handler/function/js_value.rs b/core/engine/src/handler/function/serde.rs similarity index 52% rename from core/engine/src/handler/function/js_value.rs rename to core/engine/src/handler/function/serde.rs index 8d7f60c1..8a12c01e 100644 --- a/core/engine/src/handler/function/js_value.rs +++ b/core/engine/src/handler/function/serde.rs @@ -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 { 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 = arr .iter::() .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 = object .props::() @@ -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> { + 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) + } +} diff --git a/core/engine/src/handler/function_v1/mod.rs b/core/engine/src/handler/function_v1/mod.rs new file mode 100644 index 00000000..fcc31345 --- /dev/null +++ b/core/engine/src/handler/function_v1/mod.rs @@ -0,0 +1,49 @@ +use std::time::{Duration, Instant}; + +use crate::handler::function_v1::script::Script; +use crate::handler::node::{NodeRequest, NodeResponse, NodeResult}; +use crate::model::{DecisionNodeKind, FunctionNodeContent}; +use anyhow::anyhow; +use rquickjs::Runtime; +use serde_json::json; + +pub(crate) mod runtime; +mod script; + +pub struct FunctionHandler { + trace: bool, + runtime: Runtime, +} + +static MAX_DURATION: Duration = Duration::from_millis(500); + +impl FunctionHandler { + pub fn new(trace: bool, runtime: Runtime) -> Self { + Self { trace, runtime } + } + + pub async fn handle(&self, request: &NodeRequest<'_>) -> NodeResult { + let content = match &request.node.kind { + DecisionNodeKind::FunctionNode { content } => match content { + FunctionNodeContent::Version1(content) => Ok(content), + _ => Err(anyhow!("Unexpected node type")), + }, + _ => Err(anyhow!("Unexpected node type")), + }?; + + let start = Instant::now(); + let interrupt_handler = Box::new(move || start.elapsed() > MAX_DURATION); + self.runtime.set_interrupt_handler(Some(interrupt_handler)); + + let mut script = Script::new(self.runtime.clone()); + let result_response = script.call(content, &request.input).await; + + self.runtime.set_interrupt_handler(None); + + let response = result_response?; + Ok(NodeResponse { + output: response.output, + trace_data: self.trace.then(|| json!({ "log": response.log })), + }) + } +} diff --git a/core/engine/src/handler/function/runtime.rs b/core/engine/src/handler/function_v1/runtime.rs similarity index 76% rename from core/engine/src/handler/function/runtime.rs rename to core/engine/src/handler/function_v1/runtime.rs index 63ad68a3..916a5b01 100644 --- a/core/engine/src/handler/function/runtime.rs +++ b/core/engine/src/handler/function_v1/runtime.rs @@ -3,9 +3,9 @@ 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" + "dayjs": "js/v1/dayjs.js", + "big": "js/v1/big.js", + "internals": "js/v1/internals.js" }; pub(crate) fn create_runtime() -> anyhow::Result { diff --git a/core/engine/src/handler/function/script.rs b/core/engine/src/handler/function_v1/script.rs similarity index 80% rename from core/engine/src/handler/function/script.rs rename to core/engine/src/handler/function_v1/script.rs index f6f76d25..62c16cec 100644 --- a/core/engine/src/handler/function/script.rs +++ b/core/engine/src/handler/function_v1/script.rs @@ -1,12 +1,11 @@ use std::fmt::Debug; +use crate::handler::function::serde::JsValue; use anyhow::Context as _; -use rquickjs::{Context, Ctx, Error as QError, FromJs, Runtime}; +use rquickjs::{Context, Ctx, Error as QError, FromJs, Module, 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 { @@ -34,10 +33,14 @@ impl Script { serde_json::to_string(args).context("Failed to serialize function arguments")?; let json_response = context.with(|ctx| -> anyhow::Result { - let _ = ctx - .clone() - .compile("main", "import 'internals'; globalThis.now = Date.now();") - .map_err(|e| map_js_error(&ctx, e))?; + Module::evaluate( + ctx.clone(), + "main", + "import 'internals'; globalThis.now = Date.now();", + ) + .unwrap() + .finish::<()>() + .unwrap(); let _ = ctx .globals() diff --git a/core/engine/src/handler/graph.rs b/core/engine/src/handler/graph.rs index 917cba33..32d2a467 100644 --- a/core/engine/src/handler/graph.rs +++ b/core/engine/src/handler/graph.rs @@ -1,10 +1,10 @@ use std::collections::HashMap; +use std::rc::Rc; use std::sync::Arc; use std::time::Instant; use anyhow::anyhow; use petgraph::algo::is_cyclic_directed; -use rquickjs::Runtime; use serde::ser::SerializeMap; use serde::{Deserialize, Serialize, Serializer}; use serde_json::Value; @@ -13,26 +13,30 @@ use thiserror::Error; use crate::handler::custom_node_adapter::{CustomNodeAdapter, CustomNodeRequest}; use crate::handler::decision::DecisionHandler; use crate::handler::expression::ExpressionHandler; -use crate::handler::function::runtime::create_runtime; +use crate::handler::function::function::{Function, FunctionConfig}; +use crate::handler::function::module::console::ConsoleListener; +use crate::handler::function::module::zen::ZenListener; use crate::handler::function::FunctionHandler; +use crate::handler::function_v1; +use crate::handler::function_v1::runtime::create_runtime; use crate::handler::node::NodeRequest; use crate::handler::table::zen::DecisionTableHandler; use crate::handler::traversal::{GraphWalker, StableDiDecisionGraph}; use crate::loader::DecisionLoader; -use crate::model::{DecisionContent, DecisionNodeKind}; +use crate::model::{DecisionContent, DecisionNodeKind, FunctionNodeContent}; use crate::{EvaluationError, NodeError}; -pub struct DecisionGraph<'a, L: DecisionLoader, A: CustomNodeAdapter> { +pub struct DecisionGraph<'a, L: DecisionLoader + 'static, A: CustomNodeAdapter + 'static> { graph: StableDiDecisionGraph<'a>, adapter: Arc, loader: Arc, trace: bool, max_depth: u8, iteration: u8, - runtime: Option, + runtime: Option>, } -pub struct DecisionGraphConfig<'a, L: DecisionLoader, A: CustomNodeAdapter> { +pub struct DecisionGraphConfig<'a, L: DecisionLoader + 'static, A: CustomNodeAdapter + 'static> { pub loader: Arc, pub adapter: Arc, pub content: &'a DecisionContent, @@ -41,7 +45,7 @@ pub struct DecisionGraphConfig<'a, L: DecisionLoader, A: CustomNodeAdapter> { pub max_depth: u8, } -impl<'a, L: DecisionLoader, A: CustomNodeAdapter> DecisionGraph<'a, L, A> { +impl<'a, L: DecisionLoader + 'static, A: CustomNodeAdapter + 'static> DecisionGraph<'a, L, A> { pub fn try_new( config: DecisionGraphConfig<'a, L, A>, ) -> Result { @@ -79,20 +83,31 @@ impl<'a, L: DecisionLoader, A: CustomNodeAdapter> DecisionGraph<'a, L, A> { }) } - pub(crate) fn with_runtime(mut self, runtime: Option) -> Self { + pub(crate) fn with_function(mut self, runtime: Option>) -> Self { self.runtime = runtime; self } - fn get_or_insert_runtime(&mut self) -> anyhow::Result { - if let Some(runtime) = &self.runtime { - return Ok(runtime.clone()); + async fn get_or_insert_function(&mut self) -> anyhow::Result> { + if let Some(function) = &self.runtime { + return Ok(function.clone()); } - let runtime = create_runtime()?; - self.runtime.replace(runtime.clone()); + let function = Function::create(FunctionConfig { + listeners: Some(vec![ + Box::new(ConsoleListener), + Box::new(ZenListener { + loader: self.loader.clone(), + adapter: self.adapter.clone(), + }), + ]), + }) + .await + .map_err(|err| anyhow!(err.to_string()))?; + let rc_function = Rc::new(function); + self.runtime.replace(rc_function.clone()); - Ok(runtime) + Ok(rc_function) } pub fn validate(&self) -> Result<(), DecisionGraphValidationError> { @@ -200,8 +215,8 @@ impl<'a, L: DecisionLoader, A: CustomNodeAdapter> DecisionGraph<'a, L, A> { walker.set_node_data(nid, input_data); } - DecisionNodeKind::FunctionNode { .. } => { - let runtime = self.get_or_insert_runtime().map_err(|e| NodeError { + DecisionNodeKind::FunctionNode { content } => { + let function = self.get_or_insert_function().await.map_err(|e| NodeError { source: e.into(), node_id: node.id.clone(), })?; @@ -211,13 +226,34 @@ impl<'a, L: DecisionLoader, A: CustomNodeAdapter> DecisionGraph<'a, L, A> { iteration: self.iteration, input: walker.incoming_node_data(&self.graph, nid, true), }; - let mut res = FunctionHandler::new(self.trace, runtime) + let mut res = match content { + FunctionNodeContent::Version2(_) => FunctionHandler::new( + function, + self.trace, + self.iteration, + self.max_depth, + ) .handle(&node_request) .await .map_err(|e| NodeError { source: e.into(), node_id: node.id.clone(), - })?; + })?, + FunctionNodeContent::Version1(_) => { + let runtime = create_runtime().map_err(|e| NodeError { + source: e.into(), + node_id: node.id.clone(), + })?; + + function_v1::FunctionHandler::new(self.trace, runtime) + .handle(&node_request) + .await + .map_err(|e| NodeError { + source: e.into(), + node_id: node.id.clone(), + })? + } + }; trim_nodes(&mut node_request.input); trim_nodes(&mut res.output); diff --git a/core/engine/src/handler/mod.rs b/core/engine/src/handler/mod.rs index 22596d91..1e4764b7 100644 --- a/core/engine/src/handler/mod.rs +++ b/core/engine/src/handler/mod.rs @@ -4,6 +4,7 @@ pub mod function; pub mod table; pub mod custom_node_adapter; +mod function_v1; pub mod graph; pub mod node; pub(crate) mod traversal; diff --git a/core/engine/src/lib.rs b/core/engine/src/lib.rs index aec3a6b8..97e89c65 100644 --- a/core/engine/src/lib.rs +++ b/core/engine/src/lib.rs @@ -70,7 +70,7 @@ //! ``` //! //! ## Custom loader -//! You may create a custom loader for zen engine by implementing `DecisionLoader` trait using async_trait crate. +//! You may create a custom loader for zen engine by implementing `DecisionLoader` trait. //! Here's an example of how MemoryLoader has been implemented. //! ```rust //! use std::collections::HashMap; @@ -110,11 +110,11 @@ //! } //! } //! -//! #[async_trait] //! impl DecisionLoader for MemoryLoader { -//! async fn load(&self, key: &str) -> LoaderResponse { +//! fn load<'a>(&'a self, key: &'a str) -> impl Future + 'a { +//! async move { //! self.get(&key) -//! .ok_or_else(|| LoaderError::NotFound(key.to_string())) +//! .ok_or_else(|| LoaderError::NotFound(key.to_string()).into()) //! } //! } //! ``` diff --git a/core/engine/src/loader/cached.rs b/core/engine/src/loader/cached.rs new file mode 100644 index 00000000..9ce1d3d1 --- /dev/null +++ b/core/engine/src/loader/cached.rs @@ -0,0 +1,37 @@ +use std::collections::HashMap; +use std::future::Future; +use std::sync::Arc; + +use tokio::sync::Mutex; + +use crate::loader::{DecisionLoader, LoaderResponse}; +use crate::model::DecisionContent; + +pub struct CachedLoader { + loader: Arc, + cache: Mutex>>, +} + +impl From> for CachedLoader { + fn from(value: Arc) -> Self { + Self { + loader: value, + cache: Mutex::new(HashMap::new()), + } + } +} + +impl DecisionLoader for CachedLoader { + fn load<'a>(&'a self, key: &'a str) -> impl Future + 'a { + async move { + let mut cache = self.cache.lock().await; + if let Some(content) = cache.get(key) { + return Ok(content.clone()); + } + + let decision_content = self.loader.load(key).await?; + cache.insert(key.to_string(), decision_content.clone()); + Ok(decision_content) + } + } +} diff --git a/core/engine/src/loader/closure.rs b/core/engine/src/loader/closure.rs index c22f33b0..7b13dde2 100644 --- a/core/engine/src/loader/closure.rs +++ b/core/engine/src/loader/closure.rs @@ -1,7 +1,7 @@ -use crate::loader::{DecisionLoader, LoaderResponse}; -use async_trait::async_trait; use std::future::Future; +use crate::loader::{DecisionLoader, LoaderResponse}; + /// Loads decisions using an async closure #[derive(Debug)] pub struct ClosureLoader @@ -21,14 +21,15 @@ where } } -#[async_trait] impl DecisionLoader for ClosureLoader where F: Fn(String) -> O + Sync + Send, O: Future + Send, { - async fn load(&self, key: &str) -> LoaderResponse { - let closure = &self.closure; - closure(key.to_string()).await + fn load<'a>(&'a self, key: &'a str) -> impl Future + 'a { + async move { + let closure = &self.closure; + closure(key.to_string()).await + } } } diff --git a/core/engine/src/loader/filesystem.rs b/core/engine/src/loader/filesystem.rs index d2f599df..b5180b81 100644 --- a/core/engine/src/loader/filesystem.rs +++ b/core/engine/src/loader/filesystem.rs @@ -1,13 +1,15 @@ -use crate::loader::{DecisionLoader, LoaderError, LoaderResponse}; -use async_trait::async_trait; - -use crate::model::DecisionContent; -use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::fs::File; +use std::future::Future; use std::io::BufReader; use std::path::{Path, PathBuf}; -use std::sync::{Arc, RwLock}; +use std::sync::Arc; + +use serde::{Deserialize, Serialize}; +use tokio::sync::RwLock; + +use crate::loader::{DecisionLoader, LoaderError, LoaderResponse}; +use crate::model::DecisionContent; /// Loads decisions based on filesystem root #[derive(Debug)] @@ -41,12 +43,12 @@ impl FilesystemLoader { Path::new(&self.root).join(key.as_ref()) } - fn read_from_file(&self, key: K) -> LoaderResponse + async fn read_from_file(&self, key: K) -> LoaderResponse where K: AsRef, { if let Some(memory_refs) = &self.memory_refs { - let mref = memory_refs.read().unwrap(); + let mref = memory_refs.read().await; if let Some(decision_content) = mref.get(key.as_ref()) { return Ok(decision_content.clone()); } @@ -71,7 +73,7 @@ impl FilesystemLoader { let ptr = Arc::new(result); if let Some(memory_refs) = &self.memory_refs { - let mut mref = memory_refs.write().unwrap(); + let mut mref = memory_refs.write().await; mref.insert(key.as_ref().to_string(), ptr.clone()); } @@ -79,9 +81,8 @@ impl FilesystemLoader { } } -#[async_trait] impl DecisionLoader for FilesystemLoader { - async fn load(&self, key: &str) -> LoaderResponse { - self.read_from_file(key) + fn load<'a>(&'a self, key: &'a str) -> impl Future + 'a { + async move { self.read_from_file(key).await } } } diff --git a/core/engine/src/loader/memory.rs b/core/engine/src/loader/memory.rs index 0bb47f5a..0e4ffbf4 100644 --- a/core/engine/src/loader/memory.rs +++ b/core/engine/src/loader/memory.rs @@ -1,8 +1,9 @@ +use std::collections::HashMap; +use std::future::Future; +use std::sync::{Arc, RwLock}; + use crate::loader::{DecisionLoader, LoaderError, LoaderResponse}; use crate::model::DecisionContent; -use async_trait::async_trait; -use std::collections::HashMap; -use std::sync::{Arc, RwLock}; /// Loads decisions from in-memory hashmap #[derive(Debug, Default)] @@ -37,10 +38,11 @@ impl MemoryLoader { } } -#[async_trait] impl DecisionLoader for MemoryLoader { - async fn load(&self, key: &str) -> LoaderResponse { - self.get(&key) - .ok_or_else(|| LoaderError::NotFound(key.to_string()).into()) + fn load<'a>(&'a self, key: &'a str) -> impl Future + 'a { + async move { + self.get(&key) + .ok_or_else(|| LoaderError::NotFound(key.to_string()).into()) + } } } diff --git a/core/engine/src/loader/mod.rs b/core/engine/src/loader/mod.rs index 6978b772..4cb8a27b 100644 --- a/core/engine/src/loader/mod.rs +++ b/core/engine/src/loader/mod.rs @@ -1,27 +1,29 @@ -mod closure; -mod filesystem; -mod memory; -mod noop; +use std::fmt::Debug; +use std::future::Future; +use std::sync::Arc; +use thiserror::Error; + +pub use cached::CachedLoader; pub use closure::ClosureLoader; pub use filesystem::{FilesystemLoader, FilesystemLoaderOptions}; pub use memory::MemoryLoader; pub use noop::NoopLoader; -use async_trait::async_trait; - use crate::model::DecisionContent; -use std::fmt::Debug; -use std::sync::Arc; -use thiserror::Error; + +mod cached; +mod closure; +mod filesystem; +mod memory; +mod noop; pub type LoaderResult = Result>; pub type LoaderResponse = LoaderResult>; /// Trait used for implementing a loader for decisions -#[async_trait] pub trait DecisionLoader { - async fn load(&self, key: &str) -> LoaderResponse; + fn load<'a>(&'a self, key: &'a str) -> impl Future + 'a; } #[derive(Error, Debug)] diff --git a/core/engine/src/loader/noop.rs b/core/engine/src/loader/noop.rs index c44e201b..2e2c85c9 100644 --- a/core/engine/src/loader/noop.rs +++ b/core/engine/src/loader/noop.rs @@ -1,18 +1,21 @@ -use crate::loader::{DecisionLoader, LoaderError, LoaderResponse}; +use std::future::Future; + use anyhow::anyhow; -use async_trait::async_trait; + +use crate::loader::{DecisionLoader, LoaderError, LoaderResponse}; /// Default loader which always fails #[derive(Default, Debug)] pub struct NoopLoader; -#[async_trait] impl DecisionLoader for NoopLoader { - async fn load(&self, key: &str) -> LoaderResponse { - Err(LoaderError::Internal { - key: key.to_string(), - source: anyhow!("Loader is no-op"), + fn load<'a>(&'a self, key: &'a str) -> impl Future + 'a { + async move { + Err(LoaderError::Internal { + key: key.to_string(), + source: anyhow!("Loader is no-op"), + } + .into()) } - .into()) } } diff --git a/core/engine/src/model/mod.rs b/core/engine/src/model/mod.rs index 1bd968ae..7dcbe5dd 100644 --- a/core/engine/src/model/mod.rs +++ b/core/engine/src/model/mod.rs @@ -46,7 +46,7 @@ impl PartialEq for DecisionNode { pub enum DecisionNodeKind { InputNode, OutputNode, - FunctionNode { content: String }, + FunctionNode { content: FunctionNodeContent }, DecisionNode { content: DecisionNodeContent }, DecisionTableNode { content: DecisionTableContent }, ExpressionNode { content: ExpressionNodeContent }, @@ -54,6 +54,22 @@ pub enum DecisionNodeKind { CustomNode { content: CustomNodeContent }, } +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +#[cfg_attr(feature = "bincode", derive(bincode::Encode, bincode::Decode))] +#[serde(rename_all = "camelCase")] +#[serde(untagged)] +pub enum FunctionNodeContent { + Version2(FunctionContent), + Version1(String), +} + +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize, Default)] +#[cfg_attr(feature = "bincode", derive(bincode::Encode, bincode::Decode))] +#[serde(rename_all = "camelCase")] +pub struct FunctionContent { + pub source: String, +} + #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] #[cfg_attr(feature = "bincode", derive(bincode::Encode, bincode::Decode))] #[serde(rename_all = "camelCase")] diff --git a/core/engine/tests/decision.rs b/core/engine/tests/decision.rs index 1862b4d7..2684d0a9 100644 --- a/core/engine/tests/decision.rs +++ b/core/engine/tests/decision.rs @@ -2,6 +2,7 @@ use crate::support::{create_fs_loader, load_test_data}; use serde_json::json; use std::ops::Deref; use std::sync::Arc; +use tokio::runtime::Builder; use zen_engine::{Decision, DecisionGraphValidationError, EvaluationError}; mod support; @@ -44,8 +45,9 @@ async fn decision_from_content_recursive() { } } -#[tokio::test] -async fn decision_expression_node() { +#[test] +fn decision_expression_node() { + let rt = Builder::new_current_thread().build().unwrap(); let decision = Decision::from(load_test_data("expression.json")); let context = json!({ "numbers": [1, 5, 15, 25], @@ -53,7 +55,7 @@ async fn decision_expression_node() { "lastName": "Doe" }); - let result = decision.evaluate(&context).await; + let result = rt.block_on(decision.evaluate(&context)); assert_eq!( result.unwrap().result, json!({ @@ -69,8 +71,8 @@ async fn decision_expression_node() { ) } -#[tokio::test] -async fn decision_validation() { +#[test] +fn decision_validation() { let cyclic_decision = Decision::from(load_test_data("error-cyclic.json")); let cyclic_error = cyclic_decision.validate().unwrap_err(); assert!(matches!( diff --git a/core/engine/tests/engine.rs b/core/engine/tests/engine.rs index aaa5260b..d7ca8e18 100644 --- a/core/engine/tests/engine.rs +++ b/core/engine/tests/engine.rs @@ -1,14 +1,13 @@ +use serde::Deserialize; +use serde_json::{json, Value}; use std::fs; use std::io::Read; use std::ops::Deref; use std::path::Path; use std::sync::Arc; - -use serde::Deserialize; -use serde_json::{json, Value}; - +use tokio::runtime::Builder; use zen_engine::loader::{LoaderError, MemoryLoader}; -use zen_engine::model::{DecisionContent, DecisionNodeKind}; +use zen_engine::model::{DecisionContent, DecisionNodeKind, FunctionNodeContent}; use zen_engine::{DecisionEngine, EvaluationError, EvaluationOptions}; use crate::support::{create_fs_loader, load_raw_test_data, load_test_data, test_data_root}; @@ -69,25 +68,27 @@ async fn engine_closure_loader() { assert_eq!(not_found.unwrap_err().to_string(), "Loader error"); } -#[tokio::test] -async fn engine_noop_loader() { +#[test] +fn engine_noop_loader() { + let rt = Builder::new_current_thread().build().unwrap(); // Default engine is noop let engine = DecisionEngine::default(); - let result = engine.evaluate("any.json", &json!({})).await; + let result = rt.block_on(engine.evaluate("any.json", &json!({}))); assert_eq!(result.unwrap_err().to_string(), "Loader error"); } -#[tokio::test] -async fn engine_get_decision() { +#[test] +fn engine_get_decision() { + let rt = Builder::new_current_thread().build().unwrap(); let engine = DecisionEngine::default().with_loader(create_fs_loader().into()); - assert!(engine.get_decision("table.json").await.is_ok()); - assert!(engine.get_decision("any.json").await.is_err()); + assert!(rt.block_on(engine.get_decision("table.json")).is_ok()); + assert!(rt.block_on(engine.get_decision("any.json")).is_err()); } -#[tokio::test] -async fn engine_create_decision() { +#[test] +fn engine_create_decision() { let engine = DecisionEngine::default(); engine.create_decision(load_test_data("table.json").into()); } @@ -115,21 +116,20 @@ async fn engine_errors() { } } -#[tokio::test] -async fn engine_with_trace() { +#[test] +fn engine_with_trace() { + let rt = Builder::new_current_thread().build().unwrap(); let engine = DecisionEngine::default().with_loader(create_fs_loader().into()); - let table_r = engine.evaluate("table.json", &json!({ "input": 12 })).await; - let table_opt_r = engine - .evaluate_with_opts( - "table.json", - &json!({ "input": 12 }), - EvaluationOptions { - trace: Some(true), - max_depth: None, - }, - ) - .await; + let table_r = rt.block_on(engine.evaluate("table.json", &json!({ "input": 12 }))); + let table_opt_r = rt.block_on(engine.evaluate_with_opts( + "table.json", + &json!({ "input": 12 }), + EvaluationOptions { + trace: Some(true), + max_depth: None, + }, + )); let table = table_r.unwrap(); let table_opt = table_opt_r.unwrap(); @@ -152,8 +152,13 @@ async fn engine_function_imports() { replace_buffer.read_to_string(&mut replace_data).unwrap(); function_content.nodes.iter_mut().for_each(|node| { - if let DecisionNodeKind::FunctionNode { content } = &mut node.kind { - let _ = std::mem::replace(content, replace_data.clone()); + if let DecisionNodeKind::FunctionNode { content, .. } = &mut node.kind { + match content { + FunctionNodeContent::Version1(content) => { + let _ = std::mem::replace(content, replace_data.clone()); + } + _ => {} + } } }); @@ -177,13 +182,13 @@ async fn engine_function_imports() { assert!(result.moment_valid); } -#[tokio::test] -async fn engine_switch_node() { +#[test] +fn engine_switch_node() { + let rt = Builder::new_current_thread().build().unwrap(); let engine = DecisionEngine::default().with_loader(create_fs_loader().into()); - let switch_node_r = engine - .evaluate("switch-node.json", &json!({ "color": "yellow" })) - .await; + let switch_node_r = + rt.block_on(engine.evaluate("switch-node.json", &json!({ "color": "yellow" }))); let table = switch_node_r.unwrap(); println!("{table:?}");