├── core ├── engine │ ├── tests │ │ ├── snapshots │ │ │ ├── .gitignore │ │ │ ├── engine__expression-default_4.snap │ │ │ ├── engine__expression-default_0.snap │ │ │ ├── engine__expression-default_3.snap │ │ │ ├── engine__expression-default_1.snap │ │ │ ├── engine__expression-default_2.snap │ │ │ ├── engine__expression-passthrough_0.snap │ │ │ ├── engine__table-loop_0.snap │ │ │ ├── engine__empty-column-with-space_1.snap │ │ │ ├── engine__empty-column-without-space_1.snap │ │ │ ├── engine__expression-passthrough_1.snap │ │ │ ├── engine__empty-column-with-space_0.snap │ │ │ ├── engine__empty-column-without-space_0.snap │ │ │ ├── engine__expression-passthrough_3.snap │ │ │ ├── engine__expression-passthrough_2.snap │ │ │ ├── engine__expression-fields_0.snap │ │ │ ├── engine__merch-bags_0.snap │ │ │ ├── engine__expression-fields_1.snap │ │ │ ├── engine__merch-bags_1.snap │ │ │ ├── engine__nested-request_0.snap │ │ │ ├── engine__merch-bags_2.snap │ │ │ ├── engine__expression-passthrough_4.snap │ │ │ └── engine__multi-switch_0.snap │ │ ├── model.rs │ │ ├── support │ │ │ └── mod.rs │ │ └── schema │ │ │ └── customer.schema.json │ ├── src │ │ ├── model │ │ │ └── mod.rs │ │ ├── nodes │ │ │ ├── function │ │ │ │ ├── v2 │ │ │ │ │ ├── module │ │ │ │ │ │ └── http │ │ │ │ │ │ │ ├── auth │ │ │ │ │ │ │ └── mod.rs │ │ │ │ │ │ │ ├── backend │ │ │ │ │ │ │ ├── mod.rs │ │ │ │ │ │ │ └── callback.rs │ │ │ │ │ │ │ └── listener.rs │ │ │ │ │ ├── listener.rs │ │ │ │ │ └── error.rs │ │ │ │ ├── v1 │ │ │ │ │ ├── runtime.rs │ │ │ │ │ ├── mod.rs │ │ │ │ │ └── script.rs │ │ │ │ ├── http_handler.rs │ │ │ │ └── mod.rs │ │ │ ├── mod.rs │ │ │ ├── input │ │ │ │ └── mod.rs │ │ │ ├── output │ │ │ │ └── mod.rs │ │ │ ├── result.rs │ │ │ ├── validator_cache.rs │ │ │ ├── custom │ │ │ │ ├── mod.rs │ │ │ │ └── adapter.rs │ │ │ └── definition.rs │ │ ├── decision_graph │ │ │ ├── mod.rs │ │ │ └── error.rs │ │ ├── config.rs │ │ └── loader │ │ │ ├── noop.rs │ │ │ ├── closure.rs │ │ │ ├── cached.rs │ │ │ ├── mod.rs │ │ │ └── memory.rs │ ├── js │ │ └── v1 │ │ │ └── internals.js │ └── Cargo.toml ├── expression │ ├── src │ │ ├── intellisense │ │ │ ├── types │ │ │ │ ├── mod.rs │ │ │ │ └── type_info.rs │ │ │ └── scope.rs │ │ ├── variable │ │ │ └── mod.rs │ │ ├── parser │ │ │ ├── error.rs │ │ │ ├── mod.rs │ │ │ └── result.rs │ │ ├── lexer │ │ │ ├── mod.rs │ │ │ ├── error.rs │ │ │ └── codes.rs │ │ ├── vm │ │ │ ├── mod.rs │ │ │ ├── error.rs │ │ │ └── date │ │ │ │ ├── duration.rs │ │ │ │ └── duration_unit.rs │ │ ├── compiler │ │ │ ├── mod.rs │ │ │ ├── error.rs │ │ │ └── opcode.rs │ │ ├── validate.rs │ │ ├── arena.rs │ │ ├── exports.rs │ │ └── functions │ │ │ ├── method.rs │ │ │ ├── registry.rs │ │ │ └── mod.rs │ ├── benches │ │ └── unary.rs │ └── Cargo.toml ├── types │ ├── src │ │ ├── constant.rs │ │ ├── lib.rs │ │ ├── rcvalue │ │ │ ├── mod.rs │ │ │ └── ser.rs │ │ └── variable │ │ │ └── ser.rs │ └── Cargo.toml ├── macros │ ├── src │ │ └── lib.rs │ └── Cargo.toml ├── expression_repl │ ├── Cargo.toml │ └── src │ │ └── main.rs └── template │ ├── src │ ├── lib.rs │ ├── error.rs │ ├── lexer.rs │ └── parser.rs │ └── Cargo.toml ├── bindings ├── uniffi │ ├── .gitignore │ ├── settings.gradle.kts │ ├── uniffi-bindgen.rs │ ├── gradle.android │ │ ├── settings.gradle.kts │ │ ├── AndroidManifest.xml │ │ ├── gradle.properties │ │ ├── build.gradle.kts │ │ └── zen-engine-android-build.gradle.kts │ ├── gradle │ │ └── wrapper │ │ │ ├── gradle-wrapper.jar │ │ │ └── gradle-wrapper.properties │ ├── gradle.properties │ ├── .bumpversion.cfg │ ├── src │ │ ├── lib.rs │ │ ├── config.rs │ │ ├── decision.rs │ │ └── loader.rs │ ├── uniffi-android.toml │ ├── lib │ │ ├── kotlin │ │ │ └── io │ │ │ │ └── gorules │ │ │ │ └── zen_engine │ │ │ │ └── kotlin │ │ │ │ └── JsonBuffer.kt │ │ └── java │ │ │ └── io │ │ │ └── gorules │ │ │ └── zen_engine │ │ │ └── JsonBuffer.java │ ├── uniffi.toml │ ├── Cargo.toml │ └── Makefile ├── nodejs │ ├── browser.js │ ├── .gitignore │ ├── build.rs │ ├── jest.config.js │ ├── Makefile │ ├── src │ │ ├── lib.rs │ │ ├── dispose.rs │ │ ├── mt.rs │ │ ├── config.rs │ │ ├── content.rs │ │ ├── safe_result.rs │ │ ├── decision.rs │ │ └── expression.rs │ ├── lerna.json │ ├── Cargo.toml │ ├── wasi-worker-browser.mjs │ ├── LICENSE │ ├── tsconfig.json │ └── wasi-worker.mjs ├── c │ ├── src │ │ ├── languages │ │ │ └── mod.rs │ │ ├── lib.rs │ │ ├── mt.rs │ │ ├── helper.rs │ │ ├── error.rs │ │ └── custom_node.rs │ ├── test.c │ ├── build.rs │ └── Cargo.toml └── python │ ├── .bumpversion.cfg │ ├── src │ ├── mt.rs │ ├── lib.rs │ └── content.rs │ ├── Cargo.toml │ ├── .gitignore │ ├── LICENSE │ └── pyproject.toml ├── actions └── cargo-version-action │ ├── dist │ └── package.json │ ├── .prettierignore │ ├── tsconfig.json │ ├── .prettierrc │ ├── .eslintrc │ ├── jest.config.cjs │ ├── README.md │ ├── action.yaml │ ├── src │ └── cargo.ts │ └── package.json ├── examples ├── nodejs-lambda-s3 │ ├── README.md │ └── index.mjs └── python-lambda-s3 │ ├── README.md │ └── lambda_function.py ├── .gitignore ├── Cargo.toml ├── test-data ├── passthrough.json ├── js │ └── imports.js ├── $nodes-parent.json ├── sleep-function.json ├── http-function.json ├── recursive-table1.json ├── custom.json ├── recursive-table2.json ├── function-v2.json ├── function.json ├── infinite-function.json ├── error-missing-output.json ├── error-missing-input.json ├── customer-input-schema.json ├── customer-output-schema.json ├── expression.json ├── table.json ├── graphs │ ├── expression-default.json │ ├── expression-fields.json │ └── expression-loop.json └── $nodes-child.json ├── LICENSE └── .github └── workflows ├── cargo-version-action-test.yaml ├── python-version-bump.yaml ├── uniffi-version-bump.yaml └── node-version-bump.yaml /core/engine/tests/snapshots/.gitignore: -------------------------------------------------------------------------------- 1 | *.new -------------------------------------------------------------------------------- /bindings/uniffi/.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .gradle 3 | build/ 4 | -------------------------------------------------------------------------------- /bindings/uniffi/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "zen-engine" -------------------------------------------------------------------------------- /bindings/nodejs/browser.js: -------------------------------------------------------------------------------- 1 | export * from '@gorules/zen-engine-wasm32-wasi' 2 | -------------------------------------------------------------------------------- /actions/cargo-version-action/dist/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module" 3 | } 4 | -------------------------------------------------------------------------------- /bindings/uniffi/uniffi-bindgen.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | uniffi::uniffi_bindgen_main() 3 | } 4 | -------------------------------------------------------------------------------- /core/expression/src/intellisense/types/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod provider; 2 | mod type_info; 3 | -------------------------------------------------------------------------------- /bindings/nodejs/.gitignore: -------------------------------------------------------------------------------- 1 | *.node 2 | *.internal.js 3 | *.tgz 4 | temp.d.ts 5 | *.wasm 6 | npm/ -------------------------------------------------------------------------------- /actions/cargo-version-action/.prettierignore: -------------------------------------------------------------------------------- 1 | # Ignore artifacts: 2 | dist 3 | node_modules 4 | 5 | -------------------------------------------------------------------------------- /core/types/src/constant.rs: -------------------------------------------------------------------------------- 1 | pub(crate) const NUMBER_TOKEN: &str = "$serde_json::private::Number"; 2 | -------------------------------------------------------------------------------- /bindings/nodejs/build.rs: -------------------------------------------------------------------------------- 1 | // build.rs 2 | extern crate napi_build; 3 | 4 | fn main() { 5 | napi_build::setup(); 6 | } 7 | -------------------------------------------------------------------------------- /bindings/uniffi/gradle.android/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "ZenEngineAndroid" 2 | include(":zen-engine-android") 3 | -------------------------------------------------------------------------------- /core/types/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod constant; 2 | pub mod decision; 3 | pub mod rcvalue; 4 | pub mod variable; 5 | pub mod variable_type; 6 | -------------------------------------------------------------------------------- /bindings/uniffi/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gorules/zen/HEAD/bindings/uniffi/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /examples/nodejs-lambda-s3/README.md: -------------------------------------------------------------------------------- 1 | # NodeJS + Lambda + S3 Serverless Rules Engine 2 | 3 | Requirements: 4 | * ZenEngineNodeJS Lambda Layer 5 | -------------------------------------------------------------------------------- /examples/python-lambda-s3/README.md: -------------------------------------------------------------------------------- 1 | # Python + Lambda + S3 Serverless Rules Engine 2 | 3 | Requirements: 4 | * ZenEnginePython Lambda Layer 5 | -------------------------------------------------------------------------------- /actions/cargo-version-action/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2015", 4 | "moduleResolution": "node" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /bindings/uniffi/gradle.properties: -------------------------------------------------------------------------------- 1 | org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled 2 | org.jetbrains.dokka.experimental.gradle.pluginMode.noWarn=true -------------------------------------------------------------------------------- /bindings/nodejs/jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 2 | module.exports = { 3 | preset: 'ts-jest', 4 | testEnvironment: 'node', 5 | }; -------------------------------------------------------------------------------- /actions/cargo-version-action/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "trailingComma": "all", 4 | "singleQuote": true, 5 | "printWidth": 120, 6 | "tabWidth": 2 7 | } 8 | -------------------------------------------------------------------------------- /bindings/c/src/languages/mod.rs: -------------------------------------------------------------------------------- 1 | /// Language specific bindings and loaders are defined here 2 | pub(crate) mod native; 3 | 4 | #[cfg(feature = "go")] 5 | pub(crate) mod go; 6 | -------------------------------------------------------------------------------- /bindings/uniffi/gradle.android/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /core/engine/src/model/mod.rs: -------------------------------------------------------------------------------- 1 | pub use zen_types::decision::*; 2 | mod decision_content; 3 | pub use decision_content::CompilationKey; 4 | pub use decision_content::DecisionContent; 5 | -------------------------------------------------------------------------------- /core/engine/src/nodes/function/v2/module/http/auth/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(not(target_family = "wasm"))] 2 | pub(crate) mod providers; 3 | 4 | mod types; 5 | 6 | pub(crate) use types::*; 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .temp 3 | 4 | /target 5 | /Cargo.lock 6 | 7 | .idea 8 | .env.docker 9 | 10 | node_modules 11 | index.node 12 | 13 | .npmrc 14 | yarn-error.log 15 | -------------------------------------------------------------------------------- /core/expression/src/variable/mod.rs: -------------------------------------------------------------------------------- 1 | pub use zen_types::rcvalue::*; 2 | pub use zen_types::variable::*; 3 | pub use zen_types::variable_type::*; 4 | 5 | pub use zen_macros::ToVariable; 6 | -------------------------------------------------------------------------------- /bindings/uniffi/gradle.android/gradle.properties: -------------------------------------------------------------------------------- 1 | android.useAndroidX=true 2 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 3 | org.gradle.parallel=true 4 | org.gradle.caching=true 5 | -------------------------------------------------------------------------------- /bindings/python/.bumpversion.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 0.50.3 3 | commit = True 4 | tag = True 5 | message = chore(release): publish python 6 | 7 | [bumpversion:file:Cargo.toml] 8 | -------------------------------------------------------------------------------- /bindings/uniffi/.bumpversion.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 0.4.7 3 | commit = True 4 | tag = True 5 | message = chore(release): publish uniffi 6 | 7 | [bumpversion:file:Cargo.toml] 8 | -------------------------------------------------------------------------------- /bindings/uniffi/src/lib.rs: -------------------------------------------------------------------------------- 1 | uniffi::setup_scaffolding!(); 2 | mod config; 3 | mod custom_node; 4 | mod decision; 5 | mod engine; 6 | mod error; 7 | mod expression; 8 | mod loader; 9 | mod types; 10 | -------------------------------------------------------------------------------- /bindings/c/test.c: -------------------------------------------------------------------------------- 1 | #include "bindings.h" 2 | 3 | 4 | int main() { 5 | while(true) { 6 | void* engine = zen_engine_new(); 7 | zen_engine_free(engine); 8 | } 9 | 10 | return 0; 11 | } -------------------------------------------------------------------------------- /bindings/c/src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate zen_engine; 2 | 3 | mod custom_node; 4 | mod decision; 5 | mod engine; 6 | mod error; 7 | mod expression; 8 | mod helper; 9 | mod languages; 10 | mod loader; 11 | mod mt; 12 | mod result; 13 | -------------------------------------------------------------------------------- /actions/cargo-version-action/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "extends": ["plugin:@typescript-eslint/recommended"], 4 | "parserOptions": { "ecmaVersion": 2018, "sourceType": "module" }, 5 | "rules": {} 6 | } 7 | -------------------------------------------------------------------------------- /bindings/nodejs/Makefile: -------------------------------------------------------------------------------- 1 | wasm: 2 | yarn exec napi build --target wasm32-wasip1-threads --output-dir npm/wasm32-wasi --release 3 | mv npm/wasm32-wasi/browser.js . 4 | mv npm/wasm32-wasi/zen-engine.wasm npm/wasm32-wasi/zen-engine.wasm32-wasi.wasm -------------------------------------------------------------------------------- /bindings/nodejs/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod config; 2 | mod content; 3 | mod custom_node; 4 | mod decision; 5 | mod dispose; 6 | mod engine; 7 | mod expression; 8 | mod http_handler; 9 | mod loader; 10 | mod mt; 11 | mod safe_result; 12 | mod types; 13 | -------------------------------------------------------------------------------- /bindings/nodejs/lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | "./" 4 | ], 5 | "npmClient": "yarn", 6 | "version": "independent", 7 | "command": { 8 | "version": { 9 | "message": "chore(release): publish nodejs" 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /core/macros/src/lib.rs: -------------------------------------------------------------------------------- 1 | use proc_macro::TokenStream; 2 | 3 | mod to_variable; 4 | 5 | #[proc_macro_derive(ToVariable, attributes(serde))] 6 | pub fn derive_to_variable(input: TokenStream) -> TokenStream { 7 | to_variable::to_variable_impl(input) 8 | } 9 | -------------------------------------------------------------------------------- /actions/cargo-version-action/jest.config.cjs: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 2 | module.exports = { 3 | preset: 'ts-jest', 4 | testEnvironment: 'node', 5 | testMatch: ["**/?(*.)+(spec|test).ts?(x)"], 6 | extensionsToTreatAsEsm: [] 7 | }; -------------------------------------------------------------------------------- /core/expression/src/intellisense/scope.rs: -------------------------------------------------------------------------------- 1 | use crate::variable::VariableType; 2 | 3 | #[derive(Clone, Debug)] 4 | pub struct IntelliSenseScope { 5 | pub root_data: VariableType, 6 | pub current_data: VariableType, 7 | pub pointer_data: VariableType, 8 | } 9 | -------------------------------------------------------------------------------- /core/expression/src/parser/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | #[derive(Debug, PartialEq, Eq, Clone, Error)] 4 | pub enum ParserError { 5 | #[error("{0}")] 6 | NodeError(String), 7 | 8 | #[error("Incomplete parser output")] 9 | Incomplete, 10 | } 11 | -------------------------------------------------------------------------------- /core/engine/src/decision_graph/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod cleaner; 2 | mod error; 3 | pub(crate) mod graph; 4 | mod tracer; 5 | mod walker; 6 | 7 | pub use error::DecisionGraphValidationError; 8 | pub use graph::DecisionGraphResponse; 9 | pub use tracer::DecisionGraphTrace; 10 | -------------------------------------------------------------------------------- /bindings/uniffi/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /core/expression/src/lexer/mod.rs: -------------------------------------------------------------------------------- 1 | //! Performs lexical analysis on string inputs 2 | //! 3 | //! The Lexer module transforms strings into tokens using Strum. 4 | mod error; 5 | mod token; 6 | 7 | mod codes; 8 | mod cursor; 9 | mod lexer; 10 | 11 | pub use error::LexerError; 12 | pub use lexer::Lexer; 13 | pub use token::*; 14 | -------------------------------------------------------------------------------- /core/macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "zen-macros" 3 | description = "Zen Helper Macros" 4 | version = "0.52.2" 5 | edition = "2024" 6 | license = "MIT" 7 | 8 | [lib] 9 | proc-macro = true 10 | 11 | [dependencies] 12 | proc-macro2 = "1" 13 | quote = "1" 14 | syn = { version = "2", features = ["full"] } 15 | serde_derive_internals = "0.29" 16 | -------------------------------------------------------------------------------- /bindings/uniffi/uniffi-android.toml: -------------------------------------------------------------------------------- 1 | # Kotlin Android 2 | [bindings.kotlin] 3 | package_name = "io.gorules.zen_engine.kotlin_android" 4 | generate_immutable_records = true 5 | android = true 6 | 7 | [bindings.kotlin.custom_types.JsonBuffer] 8 | imports = ["io.gorules.zen_engine.kotlin.JsonBuffer"] 9 | into_custom = "JsonBuffer({})" 10 | from_custom = "{}.value" 11 | -------------------------------------------------------------------------------- /core/expression/src/vm/mod.rs: -------------------------------------------------------------------------------- 1 | //! Virtual Machine - Evaluation of Opcodes 2 | //! 3 | //! The VM (Virtual Machine) module executes the generated machine-readable opcodes. 4 | pub use error::VMError; 5 | pub use vm::VM; 6 | 7 | pub(crate) mod date; 8 | mod error; 9 | pub(crate) mod helpers; 10 | mod interval; 11 | mod vm; 12 | 13 | pub(crate) use date::VmDate; 14 | -------------------------------------------------------------------------------- /bindings/uniffi/lib/kotlin/io/gorules/zen_engine/kotlin/JsonBuffer.kt: -------------------------------------------------------------------------------- 1 | package io.gorules.zen_engine.kotlin 2 | 3 | @JvmInline 4 | value class JsonBuffer(val value: ByteArray) { 5 | constructor(json: String) : this(json.toByteArray(Charsets.UTF_8)) 6 | 7 | override fun toString(): String = value.toString(Charsets.UTF_8) 8 | fun toByteArray(): ByteArray = value 9 | } -------------------------------------------------------------------------------- /bindings/c/build.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | 3 | fn main() { 4 | let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); 5 | 6 | cbindgen::Builder::new() 7 | .with_language(cbindgen::Language::C) 8 | .with_crate(crate_dir) 9 | .generate() 10 | .expect("Unable to generate bindings") 11 | .write_to_file("zen_engine.h"); 12 | } 13 | -------------------------------------------------------------------------------- /core/expression/src/compiler/mod.rs: -------------------------------------------------------------------------------- 1 | //! Compilation from AST into Opcodes 2 | //! 3 | //! The Compiler module transforms an Abstract Syntax Tree (AST) representation of source code into machine-readable opcodes. 4 | mod compiler; 5 | mod error; 6 | mod opcode; 7 | 8 | pub use compiler::Compiler; 9 | pub use error::CompilerError; 10 | pub use opcode::{Compare, FetchFastTarget, Jump, Opcode}; 11 | -------------------------------------------------------------------------------- /core/expression_repl/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "expression_repl" 3 | version = "0.52.2" 4 | edition = "2021" 5 | publish = false 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | colored = "3" 11 | rustyline = "15" 12 | serde_json = { workspace = true } 13 | zen-expression = { path = "../expression", version = "0.52.2" } 14 | 15 | -------------------------------------------------------------------------------- /bindings/uniffi/src/config.rs: -------------------------------------------------------------------------------- 1 | use std::sync::atomic::Ordering; 2 | use zen_engine::ZEN_CONFIG; 3 | 4 | #[derive(uniffi::Record)] 5 | pub struct ZenConfig { 6 | pub nodes_in_context: Option, 7 | } 8 | 9 | #[uniffi::export] 10 | pub fn override_config(config: ZenConfig) { 11 | if let Some(val) = config.nodes_in_context { 12 | ZEN_CONFIG.nodes_in_context.store(val, Ordering::Relaxed) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /core/types/src/rcvalue/mod.rs: -------------------------------------------------------------------------------- 1 | mod conv; 2 | mod de; 3 | mod ser; 4 | 5 | use ahash::HashMap; 6 | pub use de::RcValueDeserializer; 7 | use rust_decimal::Decimal; 8 | use std::rc::Rc; 9 | 10 | #[derive(Debug, Clone, Default, PartialEq, Eq)] 11 | pub enum RcValue { 12 | #[default] 13 | Null, 14 | Bool(bool), 15 | Number(Decimal), 16 | String(Rc), 17 | Array(Vec), 18 | Object(HashMap, RcValue>), 19 | } 20 | -------------------------------------------------------------------------------- /core/expression/src/validate.rs: -------------------------------------------------------------------------------- 1 | use crate::{Isolate, IsolateError}; 2 | 3 | pub fn validate_unary_expression(expression: &str) -> Result<(), IsolateError> { 4 | let mut isolate = Isolate::new(); 5 | isolate.compile_unary(expression)?; 6 | 7 | Ok(()) 8 | } 9 | 10 | pub fn validate_expression(expression: &str) -> Result<(), IsolateError> { 11 | let mut isolate = Isolate::new(); 12 | isolate.compile_standard(expression)?; 13 | 14 | Ok(()) 15 | } 16 | -------------------------------------------------------------------------------- /bindings/uniffi/lib/java/io/gorules/zen_engine/JsonBuffer.java: -------------------------------------------------------------------------------- 1 | package io.gorules.zen_engine; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | import java.nio.charset.StandardCharsets; 6 | 7 | public record JsonBuffer(byte[] value) { 8 | public JsonBuffer(String value) { 9 | this(value.getBytes(StandardCharsets.UTF_8)); 10 | } 11 | 12 | @NotNull 13 | @Override 14 | public String toString() { 15 | return new String(value, StandardCharsets.UTF_8); 16 | } 17 | } -------------------------------------------------------------------------------- /core/engine/src/nodes/function/v1/runtime.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Context; 2 | use rquickjs::loader::Bundle; 3 | use rquickjs::{embed, Runtime}; 4 | 5 | static JS_BUNDLE: Bundle = embed! { 6 | "dayjs": "js/v1/dayjs.js", 7 | "big": "js/v1/big.js", 8 | "internals": "js/v1/internals.js" 9 | }; 10 | 11 | pub(crate) fn create_runtime() -> anyhow::Result { 12 | let runtime = Runtime::new().context("Failed to create runtime")?; 13 | runtime.set_loader(JS_BUNDLE, JS_BUNDLE); 14 | 15 | Ok(runtime) 16 | } 17 | -------------------------------------------------------------------------------- /core/engine/src/nodes/function/v2/listener.rs: -------------------------------------------------------------------------------- 1 | use std::future::Future; 2 | use std::pin::Pin; 3 | 4 | use crate::nodes::function::v2::error::FunctionResult; 5 | use rquickjs::Ctx; 6 | 7 | #[derive(Clone, PartialEq)] 8 | pub(crate) enum RuntimeEvent { 9 | Startup, 10 | SoftReset, 11 | } 12 | 13 | pub(crate) trait RuntimeListener { 14 | fn on_event<'js>( 15 | &self, 16 | ctx: Ctx<'js>, 17 | event: RuntimeEvent, 18 | ) -> Pin + 'js>>; 19 | } 20 | -------------------------------------------------------------------------------- /bindings/uniffi/gradle.android/build.gradle.kts: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | } 6 | dependencies { 7 | classpath("com.android.tools.build:gradle:8.1.0") 8 | classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:2.1.0") 9 | } 10 | } 11 | 12 | allprojects { 13 | repositories { 14 | google() 15 | mavenCentral() 16 | } 17 | } 18 | 19 | tasks.register("clean", Delete::class) { 20 | delete(rootProject.layout.buildDirectory) 21 | } 22 | -------------------------------------------------------------------------------- /core/types/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "zen-types" 3 | description = "Zen Core Types" 4 | version = "0.52.2" 5 | edition = "2024" 6 | license = "MIT" 7 | 8 | [dependencies] 9 | ahash = { workspace = true } 10 | serde = { workspace = true, features = ["rc", "derive"] } 11 | serde_json = { workspace = true, features = ["arbitrary_precision"] } 12 | rust_decimal = { workspace = true, features = ["maths-nopanic"] } 13 | rust_decimal_macros = { workspace = true } 14 | thiserror = { workspace = true } 15 | nohash-hasher = { workspace = true } 16 | -------------------------------------------------------------------------------- /bindings/c/src/mt.rs: -------------------------------------------------------------------------------- 1 | use std::sync::{Arc, OnceLock}; 2 | 3 | use tokio::runtime; 4 | use tokio::runtime::Runtime; 5 | 6 | pub(crate) fn tokio_runtime() -> Arc { 7 | static RUNTIME: OnceLock> = OnceLock::new(); 8 | RUNTIME 9 | .get_or_init(|| { 10 | Arc::new( 11 | runtime::Builder::new_current_thread() 12 | .enable_all() 13 | .build() 14 | .expect("Failed to build tokio runtime"), 15 | ) 16 | }) 17 | .clone() 18 | } 19 | -------------------------------------------------------------------------------- /actions/cargo-version-action/README.md: -------------------------------------------------------------------------------- 1 | # Zen Cargo action 2 | 3 | This action bumps the Cargo version of the core Engine. 4 | 5 | ## Inputs 6 | 7 | ### `version` 8 | 9 | **Required** The bump of the version, one of `"patch"`, `"minor"`, `"major"`. Default `"patch"`. 10 | 11 | ### `tag-prefix` 12 | 13 | **Optional** The prefix of the tag. Default `"v"`. 14 | 15 | ## Outputs 16 | 17 | ### `version` 18 | 19 | The new version in the form of `x.x.x`. 20 | 21 | ## Developmentw 22 | ```bash 23 | npm i 24 | #dev 25 | npm run dev 26 | 27 | #build 28 | npm run build 29 | ``` 30 | 31 | -------------------------------------------------------------------------------- /core/expression/src/lexer/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | #[derive(Debug, PartialEq, Eq, Clone, Error)] 4 | pub enum LexerError { 5 | #[error("Unexpected symbol: {symbol} at ({}, {})", span.0, span.1)] 6 | UnexpectedSymbol { symbol: String, span: (u32, u32) }, 7 | 8 | #[error("Unmatched symbol: {symbol} at {position}")] 9 | UnmatchedSymbol { symbol: char, position: u32 }, 10 | 11 | #[error("Unexpected EOF: {symbol} at {position}")] 12 | UnexpectedEof { symbol: char, position: u32 }, 13 | } 14 | 15 | pub(crate) type LexerResult = Result; 16 | -------------------------------------------------------------------------------- /core/template/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod error; 2 | mod interpreter; 3 | mod lexer; 4 | mod parser; 5 | 6 | use crate::interpreter::Interpreter; 7 | use crate::lexer::Lexer; 8 | use crate::parser::Parser; 9 | use zen_expression::variable::Variable; 10 | 11 | pub use crate::error::{ParserError, TemplateRenderError}; 12 | 13 | pub fn render(template: &str, context: Variable) -> Result { 14 | let tokens = Lexer::from(template.trim()).collect(); 15 | let nodes = Parser::from(tokens.as_slice()).collect()?; 16 | 17 | Interpreter::from(nodes.as_slice()).collect_for(context) 18 | } 19 | -------------------------------------------------------------------------------- /core/template/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["GoRules Team "] 3 | description = "Zen Template Language" 4 | name = "zen-tmpl" 5 | license = "MIT" 6 | version = "0.52.2" 7 | edition = "2021" 8 | repository = "https://github.com/gorules/zen.git" 9 | 10 | [dependencies] 11 | zen-expression = { path = "../expression", version = "0.52.2" } 12 | thiserror = { workspace = true } 13 | serde = { workspace = true } 14 | serde_json = { workspace = true } 15 | 16 | [features] 17 | default = ["regex-deprecated"] 18 | 19 | regex-lite = ["zen-expression/regex-lite"] 20 | regex-deprecated = ["zen-expression/regex-deprecated"] -------------------------------------------------------------------------------- /actions/cargo-version-action/action.yaml: -------------------------------------------------------------------------------- 1 | name: 'Zen Cargo Action' 2 | description: 'Bump Zen Engine version' 3 | inputs: 4 | version: 5 | description: 'One of patch | minor | major' 6 | required: true 7 | default: 'patch' 8 | commit-message: 9 | description: 'Commit message' 10 | required: true 11 | default: 'chore(release): publish core' 12 | tag-prefix: 13 | description: 'Prefix of the tag' 14 | required: true 15 | default: 'v' 16 | outputs: 17 | version: 18 | description: 'Output version' 19 | tag: 20 | description: 'Output tag' 21 | runs: 22 | using: 'node16' 23 | main: 'dist/index.js' 24 | -------------------------------------------------------------------------------- /examples/python-lambda-s3/lambda_function.py: -------------------------------------------------------------------------------- 1 | import json 2 | import zen 3 | import boto3 4 | import os 5 | 6 | s3_client = boto3.client("s3") 7 | BUCKET_NAME = os.environ["BUCKET_NAME"] 8 | 9 | def loader(key): 10 | file_content = s3_client.get_object( 11 | Bucket=BUCKET_NAME, Key=key)["Body"].read().decode("ascii") 12 | return file_content 13 | 14 | def lambda_handler(event, context): 15 | engine = zen.ZenEngine({"loader": loader}) 16 | decision = engine.get_decision(event["key"]) 17 | result = decision.evaluate(event["context"]) 18 | return { 19 | "statusCode": 200, 20 | "body": result 21 | } 22 | -------------------------------------------------------------------------------- /core/engine/src/config.rs: -------------------------------------------------------------------------------- 1 | use once_cell::sync::Lazy; 2 | use std::sync::atomic::{AtomicBool, AtomicU64}; 3 | 4 | #[derive(Debug)] 5 | pub struct ZenConfig { 6 | pub nodes_in_context: AtomicBool, 7 | pub function_timeout_millis: AtomicU64, 8 | pub http_auth: AtomicBool, 9 | } 10 | 11 | impl Default for ZenConfig { 12 | fn default() -> Self { 13 | Self { 14 | nodes_in_context: AtomicBool::new(true), 15 | function_timeout_millis: AtomicU64::new(5_000), 16 | http_auth: AtomicBool::new(true), 17 | } 18 | } 19 | } 20 | 21 | pub static ZEN_CONFIG: Lazy = Lazy::new(|| Default::default()); 22 | -------------------------------------------------------------------------------- /core/engine/src/nodes/mod.rs: -------------------------------------------------------------------------------- 1 | mod context; 2 | pub mod custom; 3 | pub mod decision; 4 | pub mod decision_table; 5 | mod definition; 6 | pub mod expression; 7 | mod extensions; 8 | pub mod function; 9 | pub mod input; 10 | pub mod output; 11 | mod result; 12 | pub(crate) mod transform_attributes; 13 | pub(crate) mod validator_cache; 14 | 15 | pub use context::{NodeContext, NodeContextBase, NodeContextConfig, NodeContextExt}; 16 | pub use definition::NodeHandler; 17 | pub(crate) use definition::{NodeDataType, TraceDataType}; 18 | pub use extensions::NodeHandlerExtensions; 19 | pub use function::http_handler; 20 | pub use result::{NodeError, NodeRequest, NodeResponse, NodeResult}; 21 | -------------------------------------------------------------------------------- /core/expression/src/vm/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | #[derive(Debug, PartialEq, Eq, Clone, Error)] 4 | pub enum VMError { 5 | #[error("Opcode {opcode}: {message}")] 6 | OpcodeErr { opcode: String, message: String }, 7 | 8 | #[error("Opcode out of bounds")] 9 | OpcodeOutOfBounds { index: usize, bytecode: String }, 10 | 11 | #[error("Stack out of bounds")] 12 | StackOutOfBounds { stack: String }, 13 | 14 | #[error("Failed to parse date time")] 15 | ParseDateTimeErr { timestamp: String }, 16 | 17 | #[error("Number conversion error")] 18 | NumberConversionError, 19 | } 20 | 21 | pub(crate) type VMResult = Result; 22 | -------------------------------------------------------------------------------- /bindings/c/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "zen-ffi" 3 | version = "0.1.0" 4 | edition = "2021" 5 | license = "MIT" 6 | publish = false 7 | 8 | [dependencies] 9 | anyhow = { workspace = true } 10 | libc = "0.2" 11 | serde = { workspace = true } 12 | serde_json = { workspace = true } 13 | strum = { workspace = true, features = ["derive"] } 14 | tokio = { workspace = true, features = ["rt"] } 15 | zen-engine = { path = "../../core/engine" } 16 | zen-expression = { path = "../../core/expression" } 17 | zen-tmpl = { path = "../../core/template" } 18 | 19 | [lib] 20 | crate-type = ["staticlib"] 21 | 22 | [build-dependencies] 23 | cbindgen = "0.28" 24 | 25 | [features] 26 | default = ["go"] 27 | go = [] -------------------------------------------------------------------------------- /bindings/uniffi/uniffi.toml: -------------------------------------------------------------------------------- 1 | # Kotlin 2 | [bindings.kotlin] 3 | package_name = "io.gorules.zen_engine.kotlin" 4 | generate_immutable_records = true 5 | 6 | [bindings.kotlin.custom_types.JsonBuffer] 7 | into_custom = "JsonBuffer({})" 8 | from_custom = "{}.value" 9 | 10 | # Java 11 | [bindings.java] 12 | package_name = 'io.gorules.zen_engine' 13 | generate_immutable_records = true 14 | 15 | [bindings.java.custom_types.JsonBuffer] 16 | into_custom = "{}" 17 | from_custom = "{}" 18 | 19 | # C# 20 | [bindings.csharp] 21 | namespace = "GoRules.ZenEngine" 22 | cdylib_name = "Libs/zen_uniffi" 23 | 24 | [bindings.csharp.custom_types.JsonBuffer] 25 | into_custom = "new JsonBuffer({})" 26 | from_custom = "{}.Value" -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = [ 4 | "core/*", 5 | "bindings/*" 6 | ] 7 | 8 | [workspace.dependencies] 9 | ahash = "0.8" 10 | bumpalo = "3" 11 | chrono = "0.4" 12 | criterion = "0.5" 13 | fastrand = "2" 14 | humantime = "2" 15 | tokio = "1" 16 | tokio-util = "0.7" 17 | once_cell = "1" 18 | petgraph = "0.8" 19 | recursive = "0.1" 20 | regex = "1" 21 | regex-lite = "0.1" 22 | strum = "0.27" 23 | strum_macros = "0.27" 24 | serde = "1" 25 | serde_json = "1" 26 | rust_decimal = "1" 27 | rust_decimal_macros = "1" 28 | json_dotpath = "1" 29 | nohash-hasher = "0.2" 30 | 31 | anyhow = "1" 32 | thiserror = "1" 33 | 34 | [profile.release] 35 | lto = true 36 | codegen-units = 1 37 | strip = "symbols" -------------------------------------------------------------------------------- /core/engine/js/v1/internals.js: -------------------------------------------------------------------------------- 1 | import 'dayjs'; 2 | import 'big'; 3 | 4 | const log = []; 5 | 6 | globalThis.console = { 7 | log: (...args) => { 8 | try { 9 | log.push({ 10 | msSinceRun: Date.now() - now, 11 | lines: args.map(a => JSON.stringify(a)) 12 | }); 13 | } catch (e) { 14 | log.push({ 15 | msSinceRun: Date.now() - now, 16 | lines: [JSON.stringify('failed to parse logging line')] 17 | }); 18 | } 19 | } 20 | }; 21 | 22 | globalThis.main = (input) => JSON.stringify({ 23 | output: handler(input, {moment: dayjs, dayjs: dayjs, Big: Big, env: {}}), 24 | log, 25 | }); 26 | -------------------------------------------------------------------------------- /core/engine/src/loader/noop.rs: -------------------------------------------------------------------------------- 1 | use crate::loader::{DecisionLoader, LoaderError, LoaderResponse}; 2 | use anyhow::anyhow; 3 | use std::future::Future; 4 | use std::pin::Pin; 5 | 6 | /// Default loader which always fails 7 | #[derive(Default, Debug)] 8 | pub struct NoopLoader; 9 | 10 | impl DecisionLoader for NoopLoader { 11 | fn load<'a>( 12 | &'a self, 13 | key: &'a str, 14 | ) -> Pin + 'a + Send>> { 15 | Box::pin(async move { 16 | Err(LoaderError::Internal { 17 | key: key.to_string(), 18 | source: anyhow!("Loader is no-op"), 19 | } 20 | .into()) 21 | }) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /core/expression/src/parser/mod.rs: -------------------------------------------------------------------------------- 1 | //! Parses Tokens into AST 2 | //! 3 | //! The Parser module processes tokens from the Lexer, constructing an Abstract Syntax Tree (AST). 4 | //! 5 | //! It's available in two specialized variants: 6 | //! - Standard, designed for comprehensive expression evaluation yielding any result 7 | //! - Unary, specifically created for truthy tests with exclusive boolean outcomes 8 | mod ast; 9 | mod constants; 10 | mod error; 11 | mod parser; 12 | mod result; 13 | mod standard; 14 | mod unary; 15 | 16 | pub use ast::Node; 17 | pub use error::ParserError; 18 | pub use parser::Parser; 19 | pub use result::{NodeMetadata, ParserResult}; 20 | pub use standard::Standard; 21 | pub use unary::Unary; 22 | -------------------------------------------------------------------------------- /core/expression/src/lexer/codes.rs: -------------------------------------------------------------------------------- 1 | macro_rules! token_type { 2 | ("space") => { ' ' | '\n' | '\t' }; 3 | ("digit") => { '0'..='9' }; 4 | ("bracket") => { '(' | ')' | '[' | ']' | '{' | '}' }; 5 | ("cmp_operator") => { '>' | '<' | '!' }; 6 | ("operator") => { ',' | ':' | '+' | '-' | '/' | '*' | '^' | '%' }; 7 | ("alpha") => { 'A'..='Z' | 'a'..='z' | '$' | '_' | '#' }; 8 | ("alphanumeric") => { 'A'..='Z' | 'a'..='z' | '0'..='9' | '$' | '_' | '#' }; 9 | ("question_mark") => { '?' } 10 | } 11 | 12 | macro_rules! is_token_type { 13 | ($str: expr, $t: tt) => { 14 | matches!($str, crate::lexer::codes::token_type!($t)) 15 | }; 16 | } 17 | 18 | pub(crate) use is_token_type; 19 | pub(crate) use token_type; 20 | -------------------------------------------------------------------------------- /bindings/uniffi/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "zen-uniffi" 3 | version = "0.4.7" 4 | edition = "2024" 5 | license = "MIT" 6 | publish = false 7 | 8 | [lib] 9 | crate-type = ["cdylib", "staticlib"] 10 | 11 | [[bin]] 12 | name = "uniffi-bindgen" 13 | path = "uniffi-bindgen.rs" 14 | 15 | [dependencies] 16 | uniffi = { version = "0.29", features = ["tokio", "cli"] } 17 | serde_json = { workspace = true } 18 | zen-engine = { path = "../../core/engine" } 19 | zen-expression = { path = "../../core/expression" } 20 | serde = { workspace = true, features = ["derive"] } 21 | async-trait = "0.1" 22 | tokio = "1.46" 23 | 24 | [build-dependencies] 25 | uniffi = { version = "0.29", features = ["build"] } 26 | 27 | [features] 28 | bindgen = ["zen-engine/bindgen"] -------------------------------------------------------------------------------- /test-data/passthrough.json: -------------------------------------------------------------------------------- 1 | { 2 | "contentType": "application/vnd.gorules.decision", 3 | "nodes": [ 4 | { 5 | "type": "inputNode", 6 | "id": "4354dede-b4ed-4a57-9b80-45c1e33e2326", 7 | "name": "request", 8 | "position": { 9 | "x": 90, 10 | "y": 200 11 | } 12 | }, 13 | { 14 | "type": "outputNode", 15 | "id": "27e18970-f565-43eb-859e-568c9f53b7a8", 16 | "name": "response", 17 | "position": { 18 | "x": 510, 19 | "y": 200 20 | } 21 | } 22 | ], 23 | "edges": [ 24 | { 25 | "id": "4c036317-bc39-4ad2-a825-adbfd4ca2df6", 26 | "sourceId": "4354dede-b4ed-4a57-9b80-45c1e33e2326", 27 | "type": "edge", 28 | "targetId": "27e18970-f565-43eb-859e-568c9f53b7a8" 29 | } 30 | ] 31 | } -------------------------------------------------------------------------------- /test-data/js/imports.js: -------------------------------------------------------------------------------- 1 | // Moment exists for reverse compatibility 2 | 3 | /** 4 | * @param input 5 | * @param {{ 6 | * dayjs: import('dayjs') 7 | * moment: import('dayjs') 8 | * Big: import('big.js').BigConstructor 9 | * }} helpers 10 | */ 11 | const handler = (input, { dayjs, Big, moment }) => { 12 | const momentValid = typeof moment === 'function' && Object.keys(moment).includes('isDayjs'); 13 | const dayjsValid = typeof dayjs === 'function' && Object.keys(moment).includes('isDayjs'); 14 | const bigjsValid = typeof Big === 'function'; 15 | 16 | return { 17 | momentValid, 18 | dayjsValid, 19 | bigjsValid, 20 | bigjsTests: [ 21 | Big(0.1).add(0.2).eq(0.3), 22 | Big(123.12).mul(0.1).round(2).eq(12.31), 23 | ] 24 | }; 25 | } -------------------------------------------------------------------------------- /core/engine/tests/model.rs: -------------------------------------------------------------------------------- 1 | use crate::support::test_data_root; 2 | use std::fs; 3 | use std::path::Path; 4 | use zen_engine::model::DecisionContent; 5 | 6 | mod support; 7 | 8 | #[test] 9 | #[cfg_attr(miri, ignore)] 10 | fn jdm_serde() { 11 | let root_dir = test_data_root(); 12 | let dir_entries = fs::read_dir(Path::new(root_dir.as_str())).unwrap(); 13 | for maybe_dir_entry in dir_entries { 14 | let dir_entry = maybe_dir_entry.unwrap(); 15 | let Ok(file_contents) = fs::read_to_string(dir_entry.path()) else { 16 | // We expect some directories to be skipped 17 | continue; 18 | }; 19 | 20 | let serialized = serde_json::from_str::(&file_contents).unwrap(); 21 | assert!(serde_json::to_string(&serialized).is_ok()); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /bindings/nodejs/src/dispose.rs: -------------------------------------------------------------------------------- 1 | use napi::check_status; 2 | use napi::threadsafe_function::ThreadsafeFunctionHandle; 3 | 4 | pub trait DisposeThreadsafeHandler { 5 | fn dispose(&self) -> napi::Result<()>; 6 | } 7 | 8 | impl DisposeThreadsafeHandler for ThreadsafeFunctionHandle { 9 | fn dispose(&self) -> napi::Result<()> { 10 | self.with_write_aborted(|mut aborted_guard| { 11 | if !*aborted_guard { 12 | check_status!(unsafe { 13 | napi_sys::napi_release_threadsafe_function( 14 | self.get_raw(), 15 | napi_sys::ThreadsafeFunctionReleaseMode::abort, 16 | ) 17 | })?; 18 | *aborted_guard = true; 19 | } 20 | 21 | Ok(()) 22 | }) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /bindings/nodejs/src/mt.rs: -------------------------------------------------------------------------------- 1 | use napi::tokio::task::JoinHandle; 2 | use std::future::Future; 3 | use std::sync::OnceLock; 4 | use std::thread::available_parallelism; 5 | use tokio_util::task::LocalPoolHandle; 6 | 7 | fn parallelism() -> usize { 8 | available_parallelism().map(Into::into).unwrap_or(1) 9 | } 10 | 11 | pub(crate) fn worker_pool() -> LocalPoolHandle { 12 | static LOCAL_POOL: OnceLock = OnceLock::new(); 13 | LOCAL_POOL 14 | .get_or_init(|| LocalPoolHandle::new(parallelism())) 15 | .clone() 16 | } 17 | 18 | pub(crate) fn spawn_worker(create_task: F) -> JoinHandle 19 | where 20 | F: FnOnce() -> Fut, 21 | F: Send + 'static, 22 | Fut: Future + 'static, 23 | Fut::Output: Send + 'static, 24 | { 25 | worker_pool().spawn_pinned(create_task) 26 | } 27 | -------------------------------------------------------------------------------- /bindings/python/src/mt.rs: -------------------------------------------------------------------------------- 1 | use pyo3_async_runtimes::tokio::re_exports::runtime::Runtime; 2 | use std::sync::OnceLock; 3 | use std::thread::available_parallelism; 4 | use tokio_util::task::LocalPoolHandle; 5 | 6 | fn parallelism() -> usize { 7 | available_parallelism().map(Into::into).unwrap_or(1) 8 | } 9 | 10 | pub(crate) fn worker_pool() -> LocalPoolHandle { 11 | static LOCAL_POOL: OnceLock = OnceLock::new(); 12 | LOCAL_POOL 13 | .get_or_init(|| LocalPoolHandle::new(parallelism())) 14 | .clone() 15 | } 16 | 17 | static RUNTIME: OnceLock = OnceLock::new(); 18 | 19 | fn get_runtime() -> &'static Runtime { 20 | RUNTIME.get_or_init(|| Runtime::new().unwrap()) 21 | } 22 | 23 | pub(crate) fn block_on(future: F) -> F::Output { 24 | get_runtime().block_on(future) 25 | } 26 | -------------------------------------------------------------------------------- /core/expression/src/parser/result.rs: -------------------------------------------------------------------------------- 1 | use crate::parser::{Node, ParserError}; 2 | use nohash_hasher::BuildNoHashHasher; 3 | use std::collections::HashMap; 4 | 5 | #[derive(Debug)] 6 | pub struct ParserResult<'a> { 7 | pub root: &'a Node<'a>, 8 | pub is_complete: bool, 9 | pub metadata: Option>>, 10 | } 11 | 12 | #[derive(Debug, Clone)] 13 | pub struct NodeMetadata { 14 | pub span: (u32, u32), 15 | } 16 | 17 | impl<'a> ParserResult<'a> { 18 | pub fn error(&self) -> Result<(), ParserError> { 19 | if !self.is_complete { 20 | return Err(ParserError::Incomplete); 21 | } 22 | 23 | match self.root.first_error() { 24 | None => Ok(()), 25 | Some(err) => Err(ParserError::NodeError(err.to_string())), 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /bindings/python/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "zen-python" 3 | version = "0.50.3" 4 | edition = "2021" 5 | license = "MIT" 6 | publish = false 7 | 8 | [lib] 9 | name = "zen" 10 | crate-type = ["cdylib"] 11 | 12 | [dependencies] 13 | anyhow = { workspace = true } 14 | either = "1" 15 | pyo3 = { version = "0.25", features = ["anyhow", "serde", "either"] } 16 | pyo3-async-runtimes = { version = "0.25", features = ["tokio-runtime", "attributes"] } 17 | pythonize = "0.25" 18 | json_dotpath = { workspace = true } 19 | serde = { workspace = true } 20 | serde_json = { workspace = true } 21 | rust_decimal = { workspace = true, features = ["maths-nopanic"] } 22 | tokio-util = { version = "0.7", features = ["rt"] } 23 | zen-engine = { path = "../../core/engine" } 24 | zen-expression = { path = "../../core/expression" } 25 | zen-tmpl = { path = "../../core/template" } 26 | -------------------------------------------------------------------------------- /bindings/nodejs/src/config.rs: -------------------------------------------------------------------------------- 1 | use napi_derive::napi; 2 | use std::sync::atomic::Ordering; 3 | use zen_engine::ZEN_CONFIG; 4 | 5 | #[napi(object)] 6 | pub struct ZenConfig { 7 | pub nodes_in_context: Option, 8 | pub function_timeout_millis: Option, 9 | pub http_auth: Option, 10 | } 11 | 12 | #[allow(dead_code)] 13 | #[napi] 14 | pub fn override_config(config: ZenConfig) { 15 | if let Some(val) = config.nodes_in_context { 16 | ZEN_CONFIG.nodes_in_context.store(val, Ordering::Relaxed); 17 | } 18 | 19 | if let Some(val) = config.function_timeout_millis { 20 | ZEN_CONFIG 21 | .function_timeout_millis 22 | .store(val as u64, Ordering::Relaxed); 23 | } 24 | 25 | if let Some(val) = config.http_auth { 26 | ZEN_CONFIG.http_auth.store(val, Ordering::Relaxed); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /test-data/$nodes-parent.json: -------------------------------------------------------------------------------- 1 | { 2 | "contentType": "application/vnd.gorules.decision", 3 | "edges": [ 4 | { 5 | "id": "0f5ca374-811e-44da-b882-0d5566f43b65", 6 | "type": "edge", 7 | "sourceId": "341e36a6-be77-44e1-99a5-d7c7ff1b7aba", 8 | "targetId": "0b8dcf6b-fc04-47cb-bf82-bda764e6c09b" 9 | } 10 | ], 11 | "nodes": [ 12 | { 13 | "id": "341e36a6-be77-44e1-99a5-d7c7ff1b7aba", 14 | "name": "request", 15 | "type": "inputNode", 16 | "position": { 17 | "x": 40, 18 | "y": 240 19 | } 20 | }, 21 | { 22 | "id": "0b8dcf6b-fc04-47cb-bf82-bda764e6c09b", 23 | "name": "nodesChild", 24 | "type": "decisionNode", 25 | "content": { 26 | "key": "$nodes-child.json" 27 | }, 28 | "position": { 29 | "x": 370, 30 | "y": 240 31 | } 32 | } 33 | ] 34 | } -------------------------------------------------------------------------------- /bindings/nodejs/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "zen-nodejs" 3 | version = "0.1.0" 4 | edition = "2021" 5 | license = "MIT" 6 | publish = false 7 | 8 | [lib] 9 | crate-type = ["cdylib"] 10 | 11 | [dependencies] 12 | ahash = { workspace = true } 13 | napi = { version = "3", features = ["serde-json", "error_anyhow", "tokio_rt"] } 14 | napi-sys = "3" 15 | napi-derive = "3" 16 | tokio-util = { workspace = true, features = ["rt"] } 17 | serde_json = { workspace = true } 18 | zen-engine = { path = "../../core/engine" } 19 | zen-expression = { path = "../../core/expression" } 20 | zen-tmpl = { path = "../../core/template" } 21 | serde = { workspace = true, features = ["derive"] } 22 | json_dotpath = { workspace = true } 23 | 24 | [target.'cfg(target_family = "wasm")'.dependencies] 25 | iana-time-zone = { version = "0.1", features = ["fallback"] } 26 | 27 | [build-dependencies] 28 | napi-build = "2.3" -------------------------------------------------------------------------------- /bindings/uniffi/Makefile: -------------------------------------------------------------------------------- 1 | # Just for local testing, at the moment it relies on .dylib (Mac), feel free to change below 2 | build: 3 | cargo build --lib --release 4 | @mkdir -p build/generated/resources 5 | cp -f ../../target/release/libzen_uniffi.dylib build/generated/resources/libzen_uniffi.dylib 6 | 7 | generate-java: 8 | uniffi-bindgen-java generate \ 9 | --library build/generated/resources/libzen_uniffi.dylib \ 10 | --out-dir build/generated/java 11 | 12 | generate-kotlin: 13 | cargo run --bin uniffi-bindgen generate \ 14 | --library build/generated/resources/libzen_uniffi.dylib \ 15 | --language kotlin \ 16 | --out-dir build/generated/kotlin 17 | 18 | generate-csharp: 19 | uniffi-bindgen-cs \ 20 | --library build/generated/resources/libzen_uniffi.dylib \ 21 | --out-dir build/generated/csharp 22 | 23 | all: build generate-java generate-kotlin generate-csharp 24 | 25 | .PHONY: all build generate-java generate-kotlin generate-csharp -------------------------------------------------------------------------------- /bindings/c/src/helper.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::CStr; 2 | 3 | use libc::c_char; 4 | 5 | pub(crate) fn safe_cstr_from_ptr<'a>(ptr: *const c_char) -> Option<&'a CStr> { 6 | if ptr.is_null() { 7 | None 8 | } else { 9 | // SAFETY: The caller must ensure the pointer is not null and points to a valid, null-terminated C string. 10 | // This unsafe block is necessary because CStr::from_ptr inherently requires an unsafe operation. 11 | Some(unsafe { CStr::from_ptr(ptr) }) 12 | } 13 | } 14 | 15 | pub(crate) fn safe_str_from_ptr<'a>(ptr: *const c_char) -> Option<&'a str> { 16 | if ptr.is_null() { 17 | None 18 | } else { 19 | // SAFETY: The caller must ensure the pointer is not null and points to a valid, null-terminated C string. 20 | // This unsafe block is necessary because CStr::from_ptr inherently requires an unsafe operation. 21 | Some(unsafe { CStr::from_ptr(ptr) }.to_str().ok()?) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /core/engine/src/nodes/input/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::decision_graph::cleaner::VariableCleaner; 2 | use crate::nodes::definition::NodeHandler; 3 | use crate::nodes::result::NodeResult; 4 | use crate::nodes::NodeContext; 5 | use zen_types::decision::InputNodeContent; 6 | use zen_types::variable::Variable; 7 | 8 | #[derive(Debug, Clone)] 9 | pub struct InputNodeHandler; 10 | 11 | pub type InputNodeData = InputNodeContent; 12 | pub type InputNodeTrace = Variable; 13 | 14 | impl NodeHandler for InputNodeHandler { 15 | type NodeData = InputNodeData; 16 | type TraceData = InputNodeTrace; 17 | 18 | async fn handle(&self, ctx: NodeContext) -> NodeResult { 19 | if let Some(json_schema) = &ctx.node.schema { 20 | let input_json = VariableCleaner::new().clone_clean(&ctx.input).to_value(); 21 | ctx.validate(json_schema, &input_json)?; 22 | }; 23 | 24 | ctx.success(ctx.input.clone()) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /core/expression/benches/unary.rs: -------------------------------------------------------------------------------- 1 | use bumpalo::Bump; 2 | use criterion::{criterion_group, criterion_main, Bencher, Criterion}; 3 | 4 | use zen_expression::lexer::Lexer; 5 | use zen_expression::parser::Parser; 6 | 7 | fn bench_source(b: &mut Bencher, src: &'static str) { 8 | let mut lexer = Lexer::new(); 9 | let mut bump = Bump::new(); 10 | let tokens = lexer.tokenize(src).unwrap(); 11 | 12 | b.iter(|| { 13 | let unary_parser = Parser::try_new(tokens, &bump).unwrap().unary(); 14 | criterion::black_box(unary_parser.parse()); 15 | 16 | bump.reset(); 17 | }) 18 | } 19 | 20 | fn bench_functions(c: &mut Criterion) { 21 | c.bench_function("unary/simple", |b| { 22 | bench_source(b, "'hello world'"); 23 | }); 24 | 25 | c.bench_function("unary/large", |b| { 26 | bench_source(b, "'a', 'b', 'c', 'd', 'e', 'f'") 27 | }); 28 | } 29 | 30 | criterion_group!(benches, bench_functions); 31 | criterion_main!(benches); 32 | -------------------------------------------------------------------------------- /core/engine/src/nodes/output/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::decision_graph::cleaner::VariableCleaner; 2 | use crate::nodes::definition::NodeHandler; 3 | use crate::nodes::result::NodeResult; 4 | use crate::nodes::NodeContext; 5 | use zen_types::decision::OutputNodeContent; 6 | use zen_types::variable::Variable; 7 | 8 | #[derive(Debug, Clone)] 9 | pub struct OutputNodeHandler; 10 | 11 | pub type OutputNodeData = OutputNodeContent; 12 | pub type OutputNodeTrace = Variable; 13 | 14 | impl NodeHandler for OutputNodeHandler { 15 | type NodeData = OutputNodeData; 16 | type TraceData = OutputNodeTrace; 17 | 18 | async fn handle(&self, ctx: NodeContext) -> NodeResult { 19 | if let Some(json_schema) = &ctx.node.schema { 20 | let input_json = VariableCleaner::new().clone_clean(&ctx.input).to_value(); 21 | ctx.validate(json_schema, &input_json)?; 22 | }; 23 | 24 | ctx.success(ctx.input.clone()) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /bindings/nodejs/wasi-worker-browser.mjs: -------------------------------------------------------------------------------- 1 | import { instantiateNapiModuleSync, MessageHandler, WASI } from '@napi-rs/wasm-runtime' 2 | 3 | const handler = new MessageHandler({ 4 | onLoad({ wasmModule, wasmMemory }) { 5 | const wasi = new WASI({ 6 | print: function () { 7 | // eslint-disable-next-line no-console 8 | console.log.apply(console, arguments) 9 | }, 10 | printErr: function() { 11 | // eslint-disable-next-line no-console 12 | console.error.apply(console, arguments) 13 | }, 14 | }) 15 | return instantiateNapiModuleSync(wasmModule, { 16 | childThread: true, 17 | wasi, 18 | overwriteImports(importObject) { 19 | importObject.env = { 20 | ...importObject.env, 21 | ...importObject.napi, 22 | ...importObject.emnapi, 23 | memory: wasmMemory, 24 | } 25 | }, 26 | }) 27 | }, 28 | }) 29 | 30 | globalThis.onmessage = function (e) { 31 | handler.handle(e) 32 | } 33 | -------------------------------------------------------------------------------- /core/types/src/rcvalue/ser.rs: -------------------------------------------------------------------------------- 1 | use crate::constant::NUMBER_TOKEN; 2 | use crate::rcvalue::RcValue; 3 | use serde::ser::SerializeStruct; 4 | use serde::{Serialize, Serializer}; 5 | 6 | impl Serialize for RcValue { 7 | fn serialize(&self, serializer: S) -> Result 8 | where 9 | S: Serializer, 10 | { 11 | match self { 12 | RcValue::Null => serializer.serialize_unit(), 13 | RcValue::Bool(v) => serializer.serialize_bool(*v), 14 | RcValue::Number(v) => { 15 | let str = v.normalize().to_string(); 16 | 17 | let mut s = serializer.serialize_struct(NUMBER_TOKEN, 1)?; 18 | s.serialize_field(NUMBER_TOKEN, &str)?; 19 | s.end() 20 | } 21 | RcValue::String(v) => serializer.serialize_str(v), 22 | RcValue::Array(v) => serializer.collect_seq(v.iter()), 23 | RcValue::Object(v) => serializer.collect_map(v.iter()), 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /core/engine/src/nodes/result.rs: -------------------------------------------------------------------------------- 1 | use crate::model::DecisionNode; 2 | use serde::{Deserialize, Serialize}; 3 | use std::fmt::{Display, Formatter}; 4 | use std::sync::Arc; 5 | use thiserror::Error; 6 | use zen_expression::variable::Variable; 7 | 8 | #[derive(Debug, Deserialize, Serialize)] 9 | #[serde(rename_all = "camelCase")] 10 | pub struct NodeResponse { 11 | pub output: Variable, 12 | pub trace_data: Option, 13 | } 14 | 15 | #[derive(Debug, Serialize, Clone)] 16 | pub struct NodeRequest { 17 | pub input: Variable, 18 | pub iteration: u8, 19 | pub node: Arc, 20 | } 21 | 22 | pub type NodeResult = Result; 23 | 24 | #[derive(Debug, Error)] 25 | pub struct NodeError { 26 | pub node_id: Arc, 27 | pub trace: Option, 28 | pub source: Box, 29 | } 30 | 31 | impl Display for NodeError { 32 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 33 | write!(f, "{}", self.source) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /bindings/python/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | .pytest_cache/ 6 | *.py[cod] 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | .env/ 14 | .venv/ 15 | env/ 16 | bin/ 17 | build/ 18 | develop-eggs/ 19 | dist/ 20 | eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | include/ 27 | man/ 28 | venv/ 29 | *.egg-info/ 30 | .installed.cfg 31 | *.egg 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | pip-selfcheck.json 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | 46 | # Translations 47 | *.mo 48 | 49 | # Mr Developer 50 | .mr.developer.cfg 51 | .project 52 | .pydevproject 53 | 54 | # Rope 55 | .ropeproject 56 | 57 | # Django stuff: 58 | *.log 59 | *.pot 60 | 61 | .DS_Store 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyCharm 67 | .idea/ 68 | 69 | # VSCode 70 | .vscode/ 71 | 72 | # Pyenv 73 | .python-version -------------------------------------------------------------------------------- /test-data/sleep-function.json: -------------------------------------------------------------------------------- 1 | { 2 | "contentType": "application/vnd.gorules.decision", 3 | "nodes": [ 4 | { 5 | "type": "inputNode", 6 | "id": "2d560c7d-3528-43ed-88d8-28f4d8ef17be", 7 | "name": "request", 8 | "position": { 9 | "x": 295, 10 | "y": 175 11 | } 12 | }, 13 | { 14 | "type": "functionNode", 15 | "content": { 16 | "source": "import zen from 'zen';\nimport http from 'http';\n\n/** @type {Handler} **/\nexport const handler = async (input) => {\n await console.sleep(50);\n\n return { hello: 'world' };\n};\n" 17 | }, 18 | "id": "66cb12f4-4cdd-4422-850b-4534f959407d", 19 | "name": "function1", 20 | "position": { 21 | "x": 600, 22 | "y": 175 23 | } 24 | } 25 | ], 26 | "edges": [ 27 | { 28 | "id": "b65ca09a-a010-4fc2-bca1-f7cf21a0ddc3", 29 | "sourceId": "2d560c7d-3528-43ed-88d8-28f4d8ef17be", 30 | "type": "edge", 31 | "targetId": "66cb12f4-4cdd-4422-850b-4534f959407d" 32 | } 33 | ] 34 | } -------------------------------------------------------------------------------- /core/engine/src/nodes/validator_cache.rs: -------------------------------------------------------------------------------- 1 | use ahash::HashMap; 2 | use anyhow::Context; 3 | use jsonschema::Validator; 4 | use serde_json::Value; 5 | use std::sync::{Arc, RwLock}; 6 | 7 | #[derive(Clone, Default, Debug)] 8 | pub struct ValidatorCache { 9 | inner: Arc>>>, 10 | } 11 | 12 | impl ValidatorCache { 13 | pub fn get(&self, key: u64) -> Option> { 14 | let read = self.inner.read().ok()?; 15 | read.get(&key).cloned() 16 | } 17 | 18 | pub fn get_or_insert(&self, key: u64, schema: &Value) -> anyhow::Result> { 19 | if let Some(v) = self.get(key) { 20 | return Ok(v); 21 | } 22 | 23 | let mut w_shared = self 24 | .inner 25 | .write() 26 | .ok() 27 | .context("Failed to acquire lock on validator cache")?; 28 | let validator = Arc::new(jsonschema::draft7::new(&schema)?); 29 | w_shared.insert(key, validator.clone()); 30 | 31 | Ok(validator) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /test-data/http-function.json: -------------------------------------------------------------------------------- 1 | { 2 | "contentType": "application/vnd.gorules.decision", 3 | "nodes": [ 4 | { 5 | "type": "inputNode", 6 | "id": "2d560c7d-3528-43ed-88d8-28f4d8ef17be", 7 | "name": "request", 8 | "position": { 9 | "x": 295, 10 | "y": 175 11 | } 12 | }, 13 | { 14 | "type": "functionNode", 15 | "content": { 16 | "source": "import zen from 'zen';\nimport http from 'http';\n\n/** @type {Handler} **/\nexport const handler = async (input) => {\n const r = await http.get('https://fakestoreapi.com/products/1');\n\n return r;\n};\n" 17 | }, 18 | "id": "66cb12f4-4cdd-4422-850b-4534f959407d", 19 | "name": "function1", 20 | "position": { 21 | "x": 600, 22 | "y": 175 23 | } 24 | } 25 | ], 26 | "edges": [ 27 | { 28 | "id": "b65ca09a-a010-4fc2-bca1-f7cf21a0ddc3", 29 | "sourceId": "2d560c7d-3528-43ed-88d8-28f4d8ef17be", 30 | "type": "edge", 31 | "targetId": "66cb12f4-4cdd-4422-850b-4534f959407d" 32 | } 33 | ] 34 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2023 GoRules.io 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation 4 | files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, 5 | modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software 6 | is furnished to do so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 9 | 10 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE 11 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 12 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 13 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /bindings/nodejs/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2023 GoRules.io 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation 4 | files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, 5 | modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software 6 | is furnished to do so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 9 | 10 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE 11 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 12 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 13 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /bindings/python/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2023 GoRules.io 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation 4 | files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, 5 | modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software 6 | is furnished to do so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 9 | 10 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE 11 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 12 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 13 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /core/expression/src/compiler/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | #[derive(Debug, PartialEq, Eq, Clone, Error)] 4 | pub enum CompilerError { 5 | #[error("Unknown unary operator: {operator}")] 6 | UnknownUnaryOperator { operator: String }, 7 | 8 | #[error("Unknown binary operator: {operator}")] 9 | UnknownBinaryOperator { operator: String }, 10 | 11 | #[error("Argument not found for function {function} at index {index}")] 12 | ArgumentNotFound { function: String, index: usize }, 13 | 14 | #[error("Unexpected error node")] 15 | UnexpectedErrorNode, 16 | 17 | #[error("Unknown function `{name}`")] 18 | UnknownFunction { name: String }, 19 | 20 | #[error("Invalid function call `{name}`: {message}")] 21 | InvalidFunctionCall { name: String, message: String }, 22 | 23 | #[error("Invalid method call `{name}`: {message}")] 24 | InvalidMethodCall { name: String, message: String }, 25 | 26 | #[error("Unexpected assigned object")] 27 | UnexpectedAssignedObject, 28 | } 29 | 30 | pub(crate) type CompilerResult = Result; 31 | -------------------------------------------------------------------------------- /actions/cargo-version-action/src/cargo.ts: -------------------------------------------------------------------------------- 1 | import * as toml from 'toml'; 2 | 3 | type UpdateCargoOptions = { 4 | version: string; 5 | }; 6 | 7 | const versionRegex = /version = "[0-9]+\.[0-9]+\.[0-9]+"$/im; 8 | const expressionDep = /zen-expression =.*$/im; 9 | const templateDep = /zen-tmpl =.*$/im; 10 | const macroDep = /zen-macros =.*$/im; 11 | const typesDep = /zen-types =.*$/im; 12 | 13 | export const updateCargoContents = (contents: string, { version }: UpdateCargoOptions): string => { 14 | return contents 15 | .replace(versionRegex, `version = "${version}"`) 16 | .replace(expressionDep, `zen-expression = { path = "../expression", version = "${version}" }`) 17 | .replace(macroDep, `zen-macros = { path = "../macros", version = "${version}" }`) 18 | .replace(typesDep, `zen-types = { path = "../types", version = "${version}" }`) 19 | .replace(templateDep, `zen-tmpl = { path = "../template", version = "${version}" }`); 20 | }; 21 | 22 | export const getCargoVersion = (contents: string): string => { 23 | const t = toml.parse(contents); 24 | return t?.package?.version; 25 | }; 26 | -------------------------------------------------------------------------------- /bindings/nodejs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "declaration": true, 5 | "downlevelIteration": true, 6 | "importHelpers": true, 7 | "allowJs": true, 8 | "module": "CommonJS", 9 | "moduleResolution": "node", 10 | "newLine": "LF", 11 | "noEmitHelpers": true, 12 | "strict": true, 13 | "skipLibCheck": true, 14 | "suppressImplicitAnyIndexErrors": true, 15 | "suppressExcessPropertyErrors": true, 16 | "forceConsistentCasingInFileNames": true, 17 | "preserveSymlinks": true, 18 | "target": "ES2015", 19 | "sourceMap": true, 20 | "esModuleInterop": true, 21 | "stripInternal": true, 22 | "resolveJsonModule": true, 23 | "importsNotUsedAsValues": "remove", 24 | "outDir": "dist", 25 | "lib": [ 26 | "dom", 27 | "DOM.Iterable", 28 | "ES2019", 29 | "ES2020", 30 | "esnext" 31 | ] 32 | }, 33 | "include": [ 34 | "." 35 | ], 36 | "exclude": [ 37 | "node_modules", 38 | "bench", 39 | "cli/scripts", 40 | "scripts", 41 | "target" 42 | ] 43 | } -------------------------------------------------------------------------------- /core/engine/tests/support/mod.rs: -------------------------------------------------------------------------------- 1 | use std::fs::File; 2 | use std::io::BufReader; 3 | use std::path::Path; 4 | use zen_engine::loader::{FilesystemLoader, FilesystemLoaderOptions}; 5 | use zen_engine::model::DecisionContent; 6 | 7 | #[allow(dead_code)] 8 | pub fn test_data_root() -> String { 9 | let cargo_root = Path::new(env!("CARGO_MANIFEST_DIR")); 10 | cargo_root 11 | .parent() 12 | .unwrap() 13 | .parent() 14 | .unwrap() 15 | .join("test-data") 16 | .to_string_lossy() 17 | .to_string() 18 | } 19 | 20 | pub fn load_raw_test_data(key: &str) -> BufReader { 21 | let file = File::open(Path::new(&test_data_root()).join(key)).unwrap(); 22 | BufReader::new(file) 23 | } 24 | 25 | #[allow(dead_code)] 26 | pub fn load_test_data(key: &str) -> DecisionContent { 27 | serde_json::from_reader(load_raw_test_data(key)).unwrap() 28 | } 29 | 30 | #[allow(dead_code)] 31 | pub fn create_fs_loader() -> FilesystemLoader { 32 | FilesystemLoader::new(FilesystemLoaderOptions { 33 | keep_in_memory: false, 34 | root: test_data_root(), 35 | }) 36 | } 37 | -------------------------------------------------------------------------------- /.github/workflows/cargo-version-action-test.yaml: -------------------------------------------------------------------------------- 1 | name: "Release: Core" 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | version: 7 | description: 'Version' 8 | required: true 9 | default: 'patch' 10 | type: choice 11 | options: 12 | - patch 13 | - minor 14 | - major 15 | 16 | jobs: 17 | cargo_version_test: 18 | runs-on: ubuntu-latest 19 | name: A job to test cargo version action 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v3 23 | with: 24 | persist-credentials: false 25 | 26 | - name: Set Git author 27 | run: | 28 | git config user.name "Bot" 29 | git config user.email "bot@gorules.io" 30 | 31 | - name: Cargo version 32 | uses: ./actions/cargo-version-action 33 | id: semver 34 | with: 35 | version: ${{ github.event.inputs.version }} 36 | tag-prefix: core-v 37 | 38 | - name: Push changes 39 | uses: ad-m/github-push-action@v0.6.0 40 | with: 41 | github_token: ${{ secrets.PAT }} -------------------------------------------------------------------------------- /bindings/python/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["maturin>=1,<2"] 3 | build-backend = "maturin" 4 | 5 | [project] 6 | name = "zen-engine" 7 | requires-python = ">=3.7" 8 | classifiers = [ 9 | "Programming Language :: Rust", 10 | "Programming Language :: Python :: Implementation :: CPython", 11 | "Programming Language :: Python :: Implementation :: PyPy", 12 | ] 13 | 14 | description = "Open-Source Business Rules Engine" 15 | readme = "README.md" 16 | authors = [{ name = "GoRules Team", email = "hi@gorules.io" }] 17 | license = { file = "LICENSE" } 18 | 19 | keywords = ["gorules", 20 | "zen-engine", 21 | "business rules engine", 22 | "rules engine", 23 | "rule engine", 24 | "bre", 25 | "rule", 26 | "rules", 27 | "engine", 28 | "decision", 29 | "decision table", 30 | "rust", 31 | "pyo3" 32 | ] 33 | 34 | [project.optional-dependencies] 35 | dev = ["black", "bumpver", "isort", "pip-tools", "pytest", "asyncio"] 36 | 37 | [project.urls] 38 | Homepage = "https://github.com/gorules/zen" 39 | 40 | [project.scripts] 41 | zenengine = "reader.__main__:main" 42 | 43 | [tool.maturin] 44 | features = ["pyo3/extension-module"] 45 | -------------------------------------------------------------------------------- /bindings/nodejs/src/content.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use napi::bindgen_prelude::{Buffer, Object}; 4 | use napi::{Either, Env}; 5 | use napi_derive::napi; 6 | use serde_json::Value; 7 | 8 | use zen_engine::model::DecisionContent; 9 | 10 | #[napi] 11 | pub struct ZenDecisionContent { 12 | pub(crate) inner: Arc, 13 | } 14 | 15 | #[napi] 16 | impl ZenDecisionContent { 17 | #[napi(constructor)] 18 | pub fn new(env: Env, content: Either) -> napi::Result { 19 | let mut decision_content: DecisionContent = match content { 20 | Either::A(buf) => serde_json::from_slice(buf.as_ref())?, 21 | Either::B(obj) => { 22 | let serde_val: Value = env.from_js_value(obj)?; 23 | serde_json::from_value(serde_val)? 24 | } 25 | }; 26 | decision_content.compile(); 27 | 28 | Ok(Self { 29 | inner: Arc::new(decision_content), 30 | }) 31 | } 32 | 33 | #[napi] 34 | pub fn to_buffer(&self) -> napi::Result { 35 | let content_vec = serde_json::to_vec(&self.inner.as_ref())?; 36 | Ok(Buffer::from(content_vec)) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /actions/cargo-version-action/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cargo-version-action", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "dist/index.js", 6 | "author": "GoRules (https://gorules.io)", 7 | "homepage": "https://github.com/gorules/zen", 8 | "type": "module", 9 | "scripts": { 10 | "dev": "ncc run src/index.ts", 11 | "build": "ncc build src/index.ts -o dist", 12 | "lint": "eslint src/**/*.ts", 13 | "test": "jest", 14 | "format": "npx prettier --write src/**/*.ts" 15 | }, 16 | "keywords": [], 17 | "license": "MIT", 18 | "dependencies": { 19 | "@actions/core": "^1.11.1", 20 | "@actions/exec": "^1.1.1", 21 | "@actions/github": "^6.0.0", 22 | "semver": "^7.7.1", 23 | "toml": "^3.0.0" 24 | }, 25 | "devDependencies": { 26 | "@jest/globals": "^29.7.0", 27 | "@types/node": "^22.14.1", 28 | "@types/semver": "^7.7.0", 29 | "@typescript-eslint/eslint-plugin": "^8.30.1", 30 | "@typescript-eslint/parser": "^8.30.1", 31 | "@vercel/ncc": "^0.38.3", 32 | "eslint": "^9.24.0", 33 | "jest": "^29.7.0", 34 | "prettier": "^3.5.3", 35 | "ts-jest": "^29.3.2", 36 | "typescript": "^5.8.3" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /core/engine/src/nodes/function/http_handler.rs: -------------------------------------------------------------------------------- 1 | use ahash::HashMap; 2 | use serde::{Deserialize, Serialize}; 3 | use serde_json::Value; 4 | use std::fmt::Debug; 5 | use std::future::Future; 6 | use std::pin::Pin; 7 | use std::sync::Arc; 8 | 9 | pub trait HttpHandler: Debug + Send + Sync { 10 | fn handle( 11 | &self, 12 | request: HttpHandlerRequest, 13 | ) -> Pin> + Send + '_>>; 14 | } 15 | 16 | #[derive(Debug, Clone, Serialize, Deserialize)] 17 | #[serde(rename_all = "camelCase")] 18 | pub struct HttpHandlerRequest { 19 | pub method: String, 20 | pub url: String, 21 | #[serde(default)] 22 | pub body: Option, 23 | #[serde(default)] 24 | pub headers: HashMap, 25 | #[serde(default)] 26 | pub params: HashMap, 27 | #[serde(default)] 28 | pub auth: Option, 29 | } 30 | 31 | #[derive(Debug, Clone, Serialize, Deserialize)] 32 | #[serde(rename_all = "camelCase")] 33 | pub struct HttpHandlerResponse { 34 | pub status: u16, 35 | pub headers: Value, 36 | pub data: Value, 37 | } 38 | 39 | pub type DynamicHttpHandler = Option>; 40 | -------------------------------------------------------------------------------- /core/engine/tests/schema/customer.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "title": "Generated schema for Root", 4 | "type": "object", 5 | "properties": { 6 | "color": { 7 | "enum": [ 8 | "red", 9 | "blue", 10 | "green" 11 | ] 12 | }, 13 | "customer": { 14 | "type": "object", 15 | "properties": { 16 | "firstName": { 17 | "description": "Customer first name", 18 | "type": "string", 19 | "minimum": 1 20 | }, 21 | "lastName": { 22 | "description": "Customer last name", 23 | "type": "string", 24 | "minimum": 1 25 | }, 26 | "email": { 27 | "description": "Customer email", 28 | "type": "string", 29 | "format": "email" 30 | }, 31 | "age": { 32 | "description": "Customer age", 33 | "type": "number", 34 | "minimum": 18 35 | } 36 | }, 37 | "required": [ 38 | "firstName", 39 | "lastName", 40 | "email", 41 | "age" 42 | ] 43 | } 44 | }, 45 | "required": [ 46 | "color", 47 | "customer" 48 | ] 49 | } -------------------------------------------------------------------------------- /core/engine/tests/snapshots/engine__expression-default_4.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: core/engine/tests/engine.rs 3 | expression: serialized_result 4 | --- 5 | performance: "[perf]" 6 | result: 7 | sum: 8 | "$serde_json::private::Number": "0" 9 | trace: 10 | 6b9cfc7e-4776-4b3f-8a19-c2a4d7874770: 11 | id: 6b9cfc7e-4776-4b3f-8a19-c2a4d7874770 12 | input: 13 | a: 14 | "$serde_json::private::Number": "-5" 15 | b: 16 | "$serde_json::private::Number": "5" 17 | negative: true 18 | name: expression1 19 | order: 20 | "$serde_json::private::Number": "1" 21 | output: 22 | sum: 23 | "$serde_json::private::Number": "0" 24 | performance: "[perf]" 25 | traceData: 26 | sum: 27 | result: 28 | "$serde_json::private::Number": "0" 29 | deced339-bace-452a-8db0-777f038bffe8: 30 | id: deced339-bace-452a-8db0-777f038bffe8 31 | input: ~ 32 | name: request 33 | order: 34 | "$serde_json::private::Number": "0" 35 | output: 36 | a: 37 | "$serde_json::private::Number": "-5" 38 | b: 39 | "$serde_json::private::Number": "5" 40 | negative: true 41 | performance: "[perf]" 42 | traceData: ~ 43 | -------------------------------------------------------------------------------- /core/engine/src/loader/closure.rs: -------------------------------------------------------------------------------- 1 | use crate::loader::{DecisionLoader, LoaderResponse}; 2 | use std::fmt::{Debug, Formatter}; 3 | use std::future::Future; 4 | use std::pin::Pin; 5 | 6 | /// Loads decisions using an async closure 7 | pub struct ClosureLoader 8 | where 9 | F: Sync + Send, 10 | { 11 | closure: F, 12 | } 13 | 14 | impl Debug for ClosureLoader 15 | where 16 | T: Sync + Send, 17 | { 18 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 19 | write!(f, "ClosureLoader") 20 | } 21 | } 22 | 23 | impl ClosureLoader 24 | where 25 | F: Fn(String) -> O + Sync + Send, 26 | O: Future + Send, 27 | { 28 | pub fn new(closure: F) -> Self { 29 | Self { closure } 30 | } 31 | } 32 | 33 | impl DecisionLoader for ClosureLoader 34 | where 35 | F: Fn(String) -> O + Sync + Send + 'static, 36 | O: Future + Send, 37 | { 38 | fn load<'a>( 39 | &'a self, 40 | key: &'a str, 41 | ) -> Pin + 'a + Send>> { 42 | Box::pin(async move { 43 | let closure = &self.closure; 44 | closure(key.to_string()).await 45 | }) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /bindings/uniffi/gradle.android/zen-engine-android-build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.library") 3 | id("org.jetbrains.kotlin.android") 4 | } 5 | 6 | android { 7 | namespace = "io.gorules.zen_engine.kotlin_android" 8 | compileSdk = 34 9 | 10 | defaultConfig { 11 | minSdk = 24 12 | targetSdk = 34 13 | version = "0.4.0" 14 | } 15 | 16 | buildTypes { 17 | release { 18 | isMinifyEnabled = false 19 | proguardFiles( 20 | getDefaultProguardFile("proguard-android-optimize.txt"), 21 | "proguard-rules.pro" 22 | ) 23 | } 24 | } 25 | 26 | compileOptions { 27 | sourceCompatibility = JavaVersion.VERSION_11 28 | targetCompatibility = JavaVersion.VERSION_11 29 | } 30 | 31 | kotlinOptions { 32 | jvmTarget = "11" 33 | } 34 | 35 | sourceSets { 36 | getByName("main") { 37 | jniLibs.srcDirs("src/main/jniLibs") 38 | } 39 | } 40 | } 41 | 42 | dependencies { 43 | implementation("androidx.core:core-ktx:1.12.0") 44 | implementation("net.java.dev.jna:jna:5.14.0@aar") 45 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2") 46 | } 47 | -------------------------------------------------------------------------------- /core/engine/tests/snapshots/engine__expression-default_0.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: core/engine/tests/engine.rs 3 | expression: serialized_result 4 | --- 5 | performance: "[perf]" 6 | result: 7 | sum: 8 | "$serde_json::private::Number": "3" 9 | trace: 10 | 6b9cfc7e-4776-4b3f-8a19-c2a4d7874770: 11 | id: 6b9cfc7e-4776-4b3f-8a19-c2a4d7874770 12 | input: 13 | a: 14 | "$serde_json::private::Number": "1" 15 | b: 16 | "$serde_json::private::Number": "2" 17 | extra: This should not pass through 18 | name: expression1 19 | order: 20 | "$serde_json::private::Number": "1" 21 | output: 22 | sum: 23 | "$serde_json::private::Number": "3" 24 | performance: "[perf]" 25 | traceData: 26 | sum: 27 | result: 28 | "$serde_json::private::Number": "3" 29 | deced339-bace-452a-8db0-777f038bffe8: 30 | id: deced339-bace-452a-8db0-777f038bffe8 31 | input: ~ 32 | name: request 33 | order: 34 | "$serde_json::private::Number": "0" 35 | output: 36 | a: 37 | "$serde_json::private::Number": "1" 38 | b: 39 | "$serde_json::private::Number": "2" 40 | extra: This should not pass through 41 | performance: "[perf]" 42 | traceData: ~ 43 | -------------------------------------------------------------------------------- /core/engine/src/nodes/custom/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::nodes::result::NodeResult; 2 | use crate::nodes::{NodeContext, NodeHandler}; 3 | use zen_types::decision::CustomNodeContent; 4 | use zen_types::variable::Variable; 5 | 6 | pub use adapter::{ 7 | CustomDecisionNode, CustomNodeAdapter, CustomNodeRequest, DynamicCustomNode, NoopCustomNode, 8 | }; 9 | 10 | mod adapter; 11 | 12 | #[derive(Debug, Clone)] 13 | pub struct CustomNodeHandler; 14 | 15 | pub type CustomNodeData = CustomNodeContent; 16 | pub type CustomNodeTrace = Variable; 17 | 18 | impl NodeHandler for CustomNodeHandler { 19 | type NodeData = CustomNodeData; 20 | type TraceData = CustomNodeTrace; 21 | 22 | async fn handle(&self, ctx: NodeContext) -> NodeResult { 23 | let custom_node_request = CustomNodeRequest { 24 | input: ctx.input.clone(), 25 | node: CustomDecisionNode { 26 | id: ctx.id.clone(), 27 | name: ctx.name.clone(), 28 | kind: ctx.node.kind.clone(), 29 | config: ctx.node.config.clone(), 30 | }, 31 | }; 32 | 33 | ctx.extensions 34 | .custom_node() 35 | .handle(custom_node_request) 36 | .await 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /core/expression/src/arena.rs: -------------------------------------------------------------------------------- 1 | use std::marker::PhantomData; 2 | 3 | use bumpalo::Bump; 4 | 5 | /// Structure used for self-referential arena (Bump). 6 | /// Compared to regular reference, this prevents issues with retag caused by misaligned lifetimes 7 | /// when trying to transmute. Bump is safely dropped once the owner struct is dropped. 8 | #[derive(Debug)] 9 | pub(crate) struct UnsafeArena<'arena> { 10 | arena: *mut Bump, 11 | _marker: PhantomData<&'arena Bump>, 12 | } 13 | 14 | impl<'arena> UnsafeArena<'arena> { 15 | pub fn new() -> Self { 16 | let boxed = Box::new(Bump::new()); 17 | let leaked = Box::leak(boxed); 18 | 19 | Self { 20 | arena: leaked, 21 | _marker: Default::default(), 22 | } 23 | } 24 | 25 | pub fn get(&self) -> &'arena Bump { 26 | unsafe { &*self.arena } 27 | } 28 | 29 | pub fn with_mut(&mut self, callback: F) 30 | where 31 | F: FnOnce(&mut Bump), 32 | { 33 | let reference = unsafe { &mut *self.arena }; 34 | callback(reference); 35 | } 36 | } 37 | 38 | impl<'arena> Drop for UnsafeArena<'arena> { 39 | fn drop(&mut self) { 40 | unsafe { 41 | let _ = Box::from_raw(self.arena); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /core/engine/tests/snapshots/engine__expression-default_3.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: core/engine/tests/engine.rs 3 | expression: serialized_result 4 | --- 5 | performance: "[perf]" 6 | result: 7 | sum: 8 | "$serde_json::private::Number": "10" 9 | trace: 10 | 6b9cfc7e-4776-4b3f-8a19-c2a4d7874770: 11 | id: 6b9cfc7e-4776-4b3f-8a19-c2a4d7874770 12 | input: 13 | a: 14 | "$serde_json::private::Number": "7" 15 | b: 16 | "$serde_json::private::Number": "3" 17 | passThrough: This should not appear in output 18 | name: expression1 19 | order: 20 | "$serde_json::private::Number": "1" 21 | output: 22 | sum: 23 | "$serde_json::private::Number": "10" 24 | performance: "[perf]" 25 | traceData: 26 | sum: 27 | result: 28 | "$serde_json::private::Number": "10" 29 | deced339-bace-452a-8db0-777f038bffe8: 30 | id: deced339-bace-452a-8db0-777f038bffe8 31 | input: ~ 32 | name: request 33 | order: 34 | "$serde_json::private::Number": "0" 35 | output: 36 | a: 37 | "$serde_json::private::Number": "7" 38 | b: 39 | "$serde_json::private::Number": "3" 40 | passThrough: This should not appear in output 41 | performance: "[perf]" 42 | traceData: ~ 43 | -------------------------------------------------------------------------------- /core/expression/src/intellisense/types/type_info.rs: -------------------------------------------------------------------------------- 1 | use crate::variable::VariableType; 2 | use std::fmt::{Display, Formatter}; 3 | use std::ops::Deref; 4 | use std::rc::Rc; 5 | 6 | #[derive(Debug, Clone, PartialEq, Eq)] 7 | pub(crate) struct TypeInfo { 8 | pub(crate) kind: VariableType, 9 | pub(crate) error: Option, 10 | } 11 | 12 | impl Deref for TypeInfo { 13 | type Target = VariableType; 14 | 15 | fn deref(&self) -> &Self::Target { 16 | &self.kind 17 | } 18 | } 19 | 20 | impl Display for TypeInfo { 21 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 22 | write!(f, "{}", self.kind) 23 | } 24 | } 25 | 26 | impl Default for TypeInfo { 27 | fn default() -> Self { 28 | Self { 29 | kind: VariableType::Any, 30 | error: None, 31 | } 32 | } 33 | } 34 | 35 | impl From for TypeInfo { 36 | fn from(value: VariableType) -> Self { 37 | Self { 38 | kind: value, 39 | error: None, 40 | } 41 | } 42 | } 43 | 44 | impl From> for TypeInfo { 45 | fn from(value: Rc) -> Self { 46 | Self { 47 | kind: value.deref().clone(), 48 | error: None, 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /core/engine/src/loader/cached.rs: -------------------------------------------------------------------------------- 1 | use ahash::{HashMap, HashMapExt}; 2 | use std::future::Future; 3 | use std::pin::Pin; 4 | use std::sync::Arc; 5 | use tokio::sync::Mutex; 6 | 7 | use crate::loader::{DecisionLoader, DynamicLoader, LoaderResponse}; 8 | use crate::model::DecisionContent; 9 | 10 | #[derive(Debug)] 11 | pub struct CachedLoader { 12 | loader: DynamicLoader, 13 | cache: Mutex>>, 14 | } 15 | 16 | impl From for CachedLoader { 17 | fn from(value: DynamicLoader) -> Self { 18 | Self { 19 | loader: value, 20 | cache: Mutex::new(HashMap::new()), 21 | } 22 | } 23 | } 24 | 25 | impl DecisionLoader for CachedLoader { 26 | fn load<'a>( 27 | &'a self, 28 | key: &'a str, 29 | ) -> Pin + 'a + Send>> { 30 | Box::pin(async move { 31 | let mut cache = self.cache.lock().await; 32 | if let Some(content) = cache.get(key) { 33 | return Ok(content.clone()); 34 | } 35 | 36 | let decision_content = self.loader.load(key).await?; 37 | cache.insert(key.to_string(), decision_content.clone()); 38 | Ok(decision_content) 39 | }) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /core/engine/tests/snapshots/engine__expression-default_1.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: core/engine/tests/engine.rs 3 | expression: serialized_result 4 | --- 5 | performance: "[perf]" 6 | result: 7 | sum: 8 | "$serde_json::private::Number": "8" 9 | trace: 10 | 6b9cfc7e-4776-4b3f-8a19-c2a4d7874770: 11 | id: 6b9cfc7e-4776-4b3f-8a19-c2a4d7874770 12 | input: 13 | a: 14 | "$serde_json::private::Number": "5" 15 | b: 16 | "$serde_json::private::Number": "3" 17 | sum: 18 | "$serde_json::private::Number": "100" 19 | name: expression1 20 | order: 21 | "$serde_json::private::Number": "1" 22 | output: 23 | sum: 24 | "$serde_json::private::Number": "8" 25 | performance: "[perf]" 26 | traceData: 27 | sum: 28 | result: 29 | "$serde_json::private::Number": "8" 30 | deced339-bace-452a-8db0-777f038bffe8: 31 | id: deced339-bace-452a-8db0-777f038bffe8 32 | input: ~ 33 | name: request 34 | order: 35 | "$serde_json::private::Number": "0" 36 | output: 37 | a: 38 | "$serde_json::private::Number": "5" 39 | b: 40 | "$serde_json::private::Number": "3" 41 | sum: 42 | "$serde_json::private::Number": "100" 43 | performance: "[perf]" 44 | traceData: ~ 45 | -------------------------------------------------------------------------------- /core/types/src/variable/ser.rs: -------------------------------------------------------------------------------- 1 | use crate::constant::NUMBER_TOKEN; 2 | use crate::variable::Variable; 3 | use serde::ser::SerializeStruct; 4 | use serde::{Serialize, Serializer}; 5 | 6 | impl Serialize for Variable { 7 | fn serialize(&self, serializer: S) -> Result 8 | where 9 | S: Serializer, 10 | { 11 | match self { 12 | Variable::Null => serializer.serialize_unit(), 13 | Variable::Bool(v) => serializer.serialize_bool(*v), 14 | Variable::Number(v) => { 15 | let str = v.normalize().to_string(); 16 | 17 | let mut s = serializer.serialize_struct(NUMBER_TOKEN, 1)?; 18 | s.serialize_field(NUMBER_TOKEN, &str)?; 19 | s.end() 20 | } 21 | Variable::String(v) => serializer.serialize_str(v), 22 | Variable::Array(v) => { 23 | let borrowed = v.borrow(); 24 | serializer.collect_seq(borrowed.iter()) 25 | } 26 | Variable::Object(v) => { 27 | let borrowed = v.borrow(); 28 | serializer.collect_map(borrowed.iter()) 29 | } 30 | Variable::Dynamic(d) => serializer.serialize_str(d.to_string().as_str()), 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /examples/nodejs-lambda-s3/index.mjs: -------------------------------------------------------------------------------- 1 | import { ZenEngine } from '@gorules/zen-engine'; 2 | import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3'; 3 | 4 | const client = new S3Client({}); 5 | const { BUCKET_NAME } = process.env; 6 | 7 | const streamToBuffer = (stream) => new Promise((resolve, reject) => { 8 | const chunks = []; 9 | stream.on('data', (chunk) => chunks.push(chunk)); 10 | stream.on('error', reject); 11 | stream.on('end', () => resolve(Buffer.concat(chunks))); 12 | }); 13 | 14 | const loader = async (key) => { 15 | try { 16 | const params = { 17 | Bucket: BUCKET_NAME, 18 | Key: key, 19 | }; 20 | 21 | const command = new GetObjectCommand(params); 22 | const response = await client.send(command); 23 | 24 | const { Body } = response; 25 | 26 | return streamToBuffer(Body); 27 | } catch (e) { 28 | console.error(e); 29 | } 30 | }; 31 | 32 | export const handler = async(event) => { 33 | const { key, context } = event; 34 | const engine = new ZenEngine({ 35 | loader 36 | }); 37 | const decision = await engine.getDecision(key); 38 | const result = await decision.evaluate(context); 39 | return { 40 | statusCode: 200, 41 | body: result, 42 | }; 43 | }; 44 | -------------------------------------------------------------------------------- /test-data/recursive-table1.json: -------------------------------------------------------------------------------- 1 | { 2 | "contentType": "application/vnd.gorules.decision", 3 | "edges": [ 4 | { 5 | "id": "0f5ca374-811e-44da-b882-0d5566f43b65", 6 | "type": "edge", 7 | "sourceId": "341e36a6-be77-44e1-99a5-d7c7ff1b7aba", 8 | "targetId": "0b8dcf6b-fc04-47cb-bf82-bda764e6c09b" 9 | }, 10 | { 11 | "id": "f07bc0ac-05f1-43ce-b942-9ba7e0bca430", 12 | "type": "edge", 13 | "sourceId": "0b8dcf6b-fc04-47cb-bf82-bda764e6c09b", 14 | "targetId": "513e554b-ecf7-42f8-97fc-caca1849930c" 15 | } 16 | ], 17 | "nodes": [ 18 | { 19 | "id": "341e36a6-be77-44e1-99a5-d7c7ff1b7aba", 20 | "name": "Request", 21 | "type": "inputNode", 22 | "position": { 23 | "x": 40, 24 | "y": 240 25 | } 26 | }, 27 | { 28 | "id": "0b8dcf6b-fc04-47cb-bf82-bda764e6c09b", 29 | "name": "inf2", 30 | "type": "decisionNode", 31 | "content": { 32 | "key": "recursive-table2.json" 33 | }, 34 | "position": { 35 | "x": 370, 36 | "y": 240 37 | } 38 | }, 39 | { 40 | "id": "513e554b-ecf7-42f8-97fc-caca1849930c", 41 | "name": "Response", 42 | "type": "outputNode", 43 | "position": { 44 | "x": 710, 45 | "y": 240 46 | } 47 | } 48 | ] 49 | } -------------------------------------------------------------------------------- /test-data/custom.json: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": [ 3 | { 4 | "id": "115975ef-2f43-4e22-b553-0da6f4cc7f68", 5 | "type": "inputNode", 6 | "position": { 7 | "x": 180, 8 | "y": 240 9 | }, 10 | "name": "Request" 11 | }, 12 | { 13 | "id": "138b3b11-ff46-450f-9704-3f3c712067b2", 14 | "type": "customNode", 15 | "position": { 16 | "x": 470, 17 | "y": 240 18 | }, 19 | "name": "customNode1", 20 | "content": { 21 | "kind": "sum", 22 | "config": { 23 | "prop1": "{{ a + 10 }}" 24 | } 25 | } 26 | }, 27 | { 28 | "id": "db8797b1-bcc1-4fbf-a5d8-e7d43a181d5e", 29 | "type": "outputNode", 30 | "position": { 31 | "x": 780, 32 | "y": 240 33 | }, 34 | "name": "Response" 35 | } 36 | ], 37 | "edges": [ 38 | { 39 | "id": "05740fa7-3755-4756-b85e-bc1af2f6773b", 40 | "sourceId": "115975ef-2f43-4e22-b553-0da6f4cc7f68", 41 | "type": "edge", 42 | "targetId": "138b3b11-ff46-450f-9704-3f3c712067b2" 43 | }, 44 | { 45 | "id": "5d89c1d6-e894-4e8a-bd13-22368c2a6bc7", 46 | "sourceId": "138b3b11-ff46-450f-9704-3f3c712067b2", 47 | "type": "edge", 48 | "targetId": "db8797b1-bcc1-4fbf-a5d8-e7d43a181d5e" 49 | } 50 | ] 51 | } -------------------------------------------------------------------------------- /test-data/recursive-table2.json: -------------------------------------------------------------------------------- 1 | { 2 | "contentType": "application/vnd.gorules.decision", 3 | "edges": [ 4 | { 5 | "id": "47fd6cf6-fc8b-41ac-af3a-b59022b51911", 6 | "type": "edge", 7 | "sourceId": "137d8749-f625-4b50-a3c2-e0ec96f6a8bf", 8 | "targetId": "c7e1277c-4f1d-4073-a132-e00b832d0061" 9 | }, 10 | { 11 | "id": "5999c500-fc6c-4177-842f-e5ff1cefc8a1", 12 | "type": "edge", 13 | "sourceId": "c7e1277c-4f1d-4073-a132-e00b832d0061", 14 | "targetId": "8e5f573b-6eab-45e9-927d-65ea5da6d5f8" 15 | } 16 | ], 17 | "nodes": [ 18 | { 19 | "id": "137d8749-f625-4b50-a3c2-e0ec96f6a8bf", 20 | "name": "Request", 21 | "type": "inputNode", 22 | "position": { 23 | "x": 130, 24 | "y": 210 25 | } 26 | }, 27 | { 28 | "id": "c7e1277c-4f1d-4073-a132-e00b832d0061", 29 | "name": "inf1", 30 | "type": "decisionNode", 31 | "content": { 32 | "key": "recursive-table1.json" 33 | }, 34 | "position": { 35 | "x": 430, 36 | "y": 210 37 | } 38 | }, 39 | { 40 | "id": "8e5f573b-6eab-45e9-927d-65ea5da6d5f8", 41 | "name": "Response", 42 | "type": "outputNode", 43 | "position": { 44 | "x": 740, 45 | "y": 210 46 | } 47 | } 48 | ] 49 | } -------------------------------------------------------------------------------- /test-data/function-v2.json: -------------------------------------------------------------------------------- 1 | { 2 | "edges": [ 3 | { 4 | "id": "8ec75bf7-5229-4138-8fdc-486c8f82b600", 5 | "type": "edge", 6 | "sourceId": "c4dbea62-f0b1-421c-a504-a82983cea33a", 7 | "targetId": "54b6b317-0681-4f8f-b13a-18c579282500" 8 | }, 9 | { 10 | "id": "2d45f4f3-0afe-499f-9bc9-5d247bc036db", 11 | "type": "edge", 12 | "sourceId": "54b6b317-0681-4f8f-b13a-18c579282500", 13 | "targetId": "48984a7a-71c9-4642-adb3-f6ce0a75bcca" 14 | } 15 | ], 16 | "nodes": [ 17 | { 18 | "id": "c4dbea62-f0b1-421c-a504-a82983cea33a", 19 | "name": "request", 20 | "type": "inputNode", 21 | "position": { 22 | "x": 100, 23 | "y": 95 24 | } 25 | }, 26 | { 27 | "id": "48984a7a-71c9-4642-adb3-f6ce0a75bcca", 28 | "name": "response", 29 | "type": "outputNode", 30 | "position": { 31 | "x": 515, 32 | "y": 255 33 | } 34 | }, 35 | { 36 | "id": "54b6b317-0681-4f8f-b13a-18c579282500", 37 | "name": "function1", 38 | "type": "functionNode", 39 | "content": { 40 | "source": "export const handler = async (input) => ({ hello: 'world', multiplied: input.input * 2 })" 41 | }, 42 | "position": { 43 | "x": 210, 44 | "y": 285 45 | } 46 | } 47 | ] 48 | } -------------------------------------------------------------------------------- /bindings/nodejs/src/safe_result.rs: -------------------------------------------------------------------------------- 1 | use napi::bindgen_prelude::{Object, ToNapiValue, TypeName}; 2 | use napi::sys::{napi_env, napi_value}; 3 | use napi::ValueType; 4 | 5 | pub struct SafeResult(Result); 6 | 7 | impl TypeName for SafeResult { 8 | fn type_name() -> &'static str { 9 | "SafeResult" 10 | } 11 | 12 | fn value_type() -> ValueType { 13 | ValueType::Object 14 | } 15 | } 16 | 17 | impl ToNapiValue for SafeResult 18 | where 19 | T: ToNapiValue, 20 | E: ToNapiValue, 21 | { 22 | unsafe fn to_napi_value(env: napi_env, val: Self) -> napi::Result { 23 | let env_wrapper = &napi::bindgen_prelude::Env::from(env); 24 | let mut obj = Object::new(env_wrapper).unwrap(); 25 | 26 | match val.0 { 27 | Ok(data) => { 28 | obj.set("success", true)?; 29 | obj.set("data", data)?; 30 | } 31 | Err(error) => { 32 | obj.set("success", false)?; 33 | obj.set("error", error)?; 34 | } 35 | } 36 | 37 | Object::to_napi_value(env, obj) 38 | } 39 | } 40 | 41 | impl From> for SafeResult { 42 | fn from(value: napi::Result) -> Self { 43 | Self(value) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /core/engine/src/loader/mod.rs: -------------------------------------------------------------------------------- 1 | use downcast_rs::{impl_downcast, DowncastSync}; 2 | use std::fmt::Debug; 3 | use std::future::Future; 4 | use std::pin::Pin; 5 | use std::sync::Arc; 6 | use thiserror::Error; 7 | 8 | pub use cached::CachedLoader; 9 | pub use closure::ClosureLoader; 10 | pub use filesystem::{FilesystemLoader, FilesystemLoaderOptions}; 11 | pub use memory::MemoryLoader; 12 | pub use noop::NoopLoader; 13 | 14 | use crate::model::DecisionContent; 15 | 16 | mod cached; 17 | mod closure; 18 | mod filesystem; 19 | mod memory; 20 | mod noop; 21 | 22 | pub type DynamicLoader = Arc; 23 | 24 | pub type LoaderResult = Result; 25 | pub type LoaderResponse = LoaderResult>; 26 | 27 | /// Trait used for implementing a loader for decisions 28 | pub trait DecisionLoader: Debug + Send + Sync + DowncastSync { 29 | fn load<'a>( 30 | &'a self, 31 | key: &'a str, 32 | ) -> Pin + 'a + Send>>; 33 | } 34 | 35 | impl_downcast!(sync DecisionLoader); 36 | 37 | #[derive(Error, Debug)] 38 | pub enum LoaderError { 39 | #[error("Loader did not find item with key {0}")] 40 | NotFound(String), 41 | #[error("Loader failed internally on key {key}: {source}.")] 42 | Internal { 43 | key: String, 44 | #[source] 45 | source: anyhow::Error, 46 | }, 47 | } 48 | -------------------------------------------------------------------------------- /bindings/c/src/error.rs: -------------------------------------------------------------------------------- 1 | use serde_json::{json, Value}; 2 | use strum::{EnumDiscriminants, FromRepr}; 3 | 4 | #[allow(dead_code)] 5 | #[derive(EnumDiscriminants)] 6 | #[strum_discriminants(derive(FromRepr))] 7 | #[repr(u8)] 8 | pub enum ZenError { 9 | Zero, 10 | 11 | InvalidArgument, 12 | StringNullError, 13 | StringUtf8Error, 14 | JsonSerializationFailed, 15 | JsonDeserializationFailed, 16 | 17 | IsolateError(Value), 18 | EvaluationError(Value), 19 | 20 | LoaderKeyNotFound { key: String }, 21 | LoaderInternalError { key: String, message: String }, 22 | 23 | TemplateEngineError { template: String, message: String }, 24 | } 25 | 26 | impl ZenError { 27 | pub fn details(&self) -> Option { 28 | match &self { 29 | ZenError::IsolateError(error) => Some(error.to_string()), 30 | ZenError::EvaluationError(error) => Some(error.to_string()), 31 | ZenError::LoaderKeyNotFound { key } => Some(json!({ "key": key }).to_string()), 32 | ZenError::LoaderInternalError { key, message } => { 33 | Some(json!({ "key": key, "message": message }).to_string()) 34 | } 35 | ZenError::TemplateEngineError { template, message } => { 36 | Some(json!({ "template": template, "message": message }).to_string()) 37 | } 38 | _ => None, 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /core/engine/tests/snapshots/engine__expression-default_2.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: core/engine/tests/engine.rs 3 | expression: serialized_result 4 | --- 5 | performance: "[perf]" 6 | result: 7 | sum: 8 | "$serde_json::private::Number": "30" 9 | trace: 10 | 6b9cfc7e-4776-4b3f-8a19-c2a4d7874770: 11 | id: 6b9cfc7e-4776-4b3f-8a19-c2a4d7874770 12 | input: 13 | a: 14 | "$serde_json::private::Number": "10" 15 | b: 16 | "$serde_json::private::Number": "20" 17 | c: 18 | "$serde_json::private::Number": "30" 19 | d: 20 | "$serde_json::private::Number": "40" 21 | name: expression1 22 | order: 23 | "$serde_json::private::Number": "1" 24 | output: 25 | sum: 26 | "$serde_json::private::Number": "30" 27 | performance: "[perf]" 28 | traceData: 29 | sum: 30 | result: 31 | "$serde_json::private::Number": "30" 32 | deced339-bace-452a-8db0-777f038bffe8: 33 | id: deced339-bace-452a-8db0-777f038bffe8 34 | input: ~ 35 | name: request 36 | order: 37 | "$serde_json::private::Number": "0" 38 | output: 39 | a: 40 | "$serde_json::private::Number": "10" 41 | b: 42 | "$serde_json::private::Number": "20" 43 | c: 44 | "$serde_json::private::Number": "30" 45 | d: 46 | "$serde_json::private::Number": "40" 47 | performance: "[perf]" 48 | traceData: ~ 49 | -------------------------------------------------------------------------------- /core/engine/tests/snapshots/engine__expression-passthrough_0.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: core/engine/tests/engine.rs 3 | expression: serialized_result 4 | --- 5 | performance: "[perf]" 6 | result: 7 | a: 8 | "$serde_json::private::Number": "1" 9 | b: 10 | "$serde_json::private::Number": "2" 11 | sum: 12 | "$serde_json::private::Number": "3" 13 | trace: 14 | 6b9cfc7e-4776-4b3f-8a19-c2a4d7874770: 15 | id: 6b9cfc7e-4776-4b3f-8a19-c2a4d7874770 16 | input: 17 | a: 18 | "$serde_json::private::Number": "1" 19 | b: 20 | "$serde_json::private::Number": "2" 21 | sum: [] 22 | name: expression1 23 | order: 24 | "$serde_json::private::Number": "1" 25 | output: 26 | a: 27 | "$serde_json::private::Number": "1" 28 | b: 29 | "$serde_json::private::Number": "2" 30 | sum: 31 | "$serde_json::private::Number": "3" 32 | performance: "[perf]" 33 | traceData: 34 | sum: 35 | result: 36 | "$serde_json::private::Number": "3" 37 | deced339-bace-452a-8db0-777f038bffe8: 38 | id: deced339-bace-452a-8db0-777f038bffe8 39 | input: ~ 40 | name: request 41 | order: 42 | "$serde_json::private::Number": "0" 43 | output: 44 | a: 45 | "$serde_json::private::Number": "1" 46 | b: 47 | "$serde_json::private::Number": "2" 48 | sum: [] 49 | performance: "[perf]" 50 | traceData: ~ 51 | -------------------------------------------------------------------------------- /core/engine/src/nodes/definition.rs: -------------------------------------------------------------------------------- 1 | use crate::nodes::context::NodeContext; 2 | use crate::nodes::result::NodeResult; 3 | use crate::nodes::NodeError; 4 | use serde::{Deserialize, Serialize}; 5 | use std::fmt::Debug; 6 | use zen_types::decision::TransformAttributes; 7 | use zen_types::variable::ToVariable; 8 | 9 | pub trait NodeDataType: Clone + Debug + Serialize + for<'de> Deserialize<'de> {} 10 | impl NodeDataType for T where T: Clone + Debug + Serialize + for<'de> Deserialize<'de> {} 11 | 12 | pub trait TraceDataType: Clone + Debug + Default + ToVariable {} 13 | impl TraceDataType for T where T: Clone + Debug + Default + ToVariable {} 14 | 15 | pub trait NodeHandler: Clone { 16 | type NodeData: NodeDataType; 17 | type TraceData: TraceDataType; 18 | 19 | #[allow(unused_variables)] 20 | fn transform_attributes( 21 | &self, 22 | ctx: &NodeContext, 23 | ) -> Option { 24 | None 25 | } 26 | 27 | #[allow(unused_variables)] 28 | fn after_transform_attributes( 29 | &self, 30 | ctx: &NodeContext, 31 | ) -> impl std::future::Future> { 32 | Box::pin(async { Ok(()) }) 33 | } 34 | 35 | fn handle( 36 | &self, 37 | ctx: NodeContext, 38 | ) -> impl std::future::Future; 39 | } 40 | -------------------------------------------------------------------------------- /test-data/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": [ 3 | { 4 | "id": "115975ef-2f43-4e22-b553-0da6f4cc7f68", 5 | "type": "inputNode", 6 | "position": { 7 | "x": 180, 8 | "y": 240 9 | }, 10 | "name": "Request" 11 | }, 12 | { 13 | "id": "138b3b11-ff46-450f-9704-3f3c712067b2", 14 | "type": "functionNode", 15 | "position": { 16 | "x": 470, 17 | "y": 240 18 | }, 19 | "name": "functionNode 1", 20 | "content": "/**\n* @param {import('gorules').Input} input\n* @param {{\n* moment: import('dayjs')\n* env: Record\n* }} helpers\n*/\nconst handler = (input, { moment, env }) => {\n return {\n output: input.input * 2,\n };\n}" 21 | }, 22 | { 23 | "id": "db8797b1-bcc1-4fbf-a5d8-e7d43a181d5e", 24 | "type": "outputNode", 25 | "position": { 26 | "x": 780, 27 | "y": 240 28 | }, 29 | "name": "Response" 30 | } 31 | ], 32 | "edges": [ 33 | { 34 | "id": "05740fa7-3755-4756-b85e-bc1af2f6773b", 35 | "sourceId": "115975ef-2f43-4e22-b553-0da6f4cc7f68", 36 | "type": "edge", 37 | "targetId": "138b3b11-ff46-450f-9704-3f3c712067b2" 38 | }, 39 | { 40 | "id": "5d89c1d6-e894-4e8a-bd13-22368c2a6bc7", 41 | "sourceId": "138b3b11-ff46-450f-9704-3f3c712067b2", 42 | "type": "edge", 43 | "targetId": "db8797b1-bcc1-4fbf-a5d8-e7d43a181d5e" 44 | } 45 | ] 46 | } -------------------------------------------------------------------------------- /test-data/infinite-function.json: -------------------------------------------------------------------------------- 1 | { 2 | "edges": [ 3 | { 4 | "id": "7ef2014c-9ea3-4ec2-b3f8-aa3672a0956d", 5 | "sourceId": "a2e9b2aa-fbc5-46c1-bbe3-8b4ff45e2f59", 6 | "type": "edge", 7 | "targetId": "e0fd96d0-44dc-4f0e-b825-06e56b442d78" 8 | }, 9 | { 10 | "id": "8b122464-c8eb-4f8a-9deb-7bfd3b94bc96", 11 | "sourceId": "e0fd96d0-44dc-4f0e-b825-06e56b442d78", 12 | "type": "edge", 13 | "targetId": "b8b98fa5-3a16-496d-99bf-7bc6d3caedb3" 14 | } 15 | ], 16 | "nodes": [ 17 | { 18 | "id": "a2e9b2aa-fbc5-46c1-bbe3-8b4ff45e2f59", 19 | "type": "inputNode", 20 | "position": { 21 | "x": 160, 22 | "y": 220 23 | }, 24 | "name": "Request" 25 | }, 26 | { 27 | "id": "e0fd96d0-44dc-4f0e-b825-06e56b442d78", 28 | "type": "functionNode", 29 | "position": { 30 | "x": 440, 31 | "y": 220 32 | }, 33 | "name": "functionNode 1", 34 | "content": "/**\n* @param {import('gorules').Input} input\n* @param {{\n* moment: import('dayjs')\n* env: Record\n* }} helpers\n*/\nconst handler = (input, { moment, env }) => {\n while (true) {}\n return input;\n}" 35 | }, 36 | { 37 | "id": "b8b98fa5-3a16-496d-99bf-7bc6d3caedb3", 38 | "type": "outputNode", 39 | "position": { 40 | "x": 750, 41 | "y": 220 42 | }, 43 | "name": "Response" 44 | } 45 | ] 46 | } -------------------------------------------------------------------------------- /bindings/python/src/lib.rs: -------------------------------------------------------------------------------- 1 | use crate::content::PyZenDecisionContent; 2 | use crate::decision::PyZenDecision; 3 | use crate::engine::PyZenEngine; 4 | use crate::expression::{ 5 | compile_expression, compile_unary_expression, evaluate_expression, evaluate_unary_expression, 6 | render_template, validate_expression, validate_unary_expression, PyExpression, 7 | }; 8 | use pyo3::prelude::PyModuleMethods; 9 | use pyo3::types::PyModule; 10 | use pyo3::{pymodule, wrap_pyfunction, Bound, PyResult, Python}; 11 | 12 | mod content; 13 | mod custom_node; 14 | mod decision; 15 | mod engine; 16 | mod expression; 17 | mod loader; 18 | mod mt; 19 | mod types; 20 | mod value; 21 | mod variable; 22 | 23 | #[pymodule] 24 | fn zen(_py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> { 25 | m.add_class::()?; 26 | m.add_class::()?; 27 | m.add_class::()?; 28 | m.add_class::()?; 29 | m.add_function(wrap_pyfunction!(evaluate_expression, m)?)?; 30 | m.add_function(wrap_pyfunction!(evaluate_unary_expression, m)?)?; 31 | m.add_function(wrap_pyfunction!(render_template, m)?)?; 32 | m.add_function(wrap_pyfunction!(compile_expression, m)?)?; 33 | m.add_function(wrap_pyfunction!(compile_unary_expression, m)?)?; 34 | m.add_function(wrap_pyfunction!(validate_expression, m)?)?; 35 | m.add_function(wrap_pyfunction!(validate_unary_expression, m)?)?; 36 | 37 | Ok(()) 38 | } 39 | -------------------------------------------------------------------------------- /test-data/error-missing-output.json: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": [ 3 | { 4 | "id": "115975ef-2f43-4e22-b553-0da6f4cc7f68", 5 | "type": "inputNode", 6 | "position": { 7 | "x": 180, 8 | "y": 240 9 | }, 10 | "name": "Request" 11 | }, 12 | { 13 | "id": "138b3b11-ff46-450f-9704-3f3c712067b2", 14 | "type": "expressionNode", 15 | "position": { 16 | "x": 470, 17 | "y": 240 18 | }, 19 | "name": "expressionNode 1", 20 | "content": { 21 | "expressions": [ 22 | { 23 | "id": "xWauegxfG7", 24 | "key": "deep.nested.sum", 25 | "value": "sum(numbers)" 26 | }, 27 | { 28 | "id": "qGAHmak0xj", 29 | "key": "fullName", 30 | "value": "firstName + ' ' + lastName" 31 | }, 32 | { 33 | "id": "5ZnYGPFT-N", 34 | "key": "largeNumbers", 35 | "value": "filter(numbers, # >= 10)" 36 | }, 37 | { 38 | "id": "pSg-vIQR5Q", 39 | "key": "smallNumbers", 40 | "value": "filter(numbers, # < 10)" 41 | } 42 | ] 43 | } 44 | } 45 | ], 46 | "edges": [ 47 | { 48 | "id": "05740fa7-3755-4756-b85e-bc1af2f6773b", 49 | "sourceId": "115975ef-2f43-4e22-b553-0da6f4cc7f68", 50 | "type": "edge", 51 | "targetId": "138b3b11-ff46-450f-9704-3f3c712067b2" 52 | } 53 | ] 54 | } -------------------------------------------------------------------------------- /test-data/error-missing-input.json: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": [ 3 | { 4 | "id": "138b3b11-ff46-450f-9704-3f3c712067b2", 5 | "type": "expressionNode", 6 | "position": { 7 | "x": 470, 8 | "y": 240 9 | }, 10 | "name": "expressionNode 1", 11 | "content": { 12 | "expressions": [ 13 | { 14 | "id": "xWauegxfG7", 15 | "key": "deep.nested.sum", 16 | "value": "sum(numbers)" 17 | }, 18 | { 19 | "id": "qGAHmak0xj", 20 | "key": "fullName", 21 | "value": "firstName + ' ' + lastName" 22 | }, 23 | { 24 | "id": "5ZnYGPFT-N", 25 | "key": "largeNumbers", 26 | "value": "filter(numbers, # >= 10)" 27 | }, 28 | { 29 | "id": "pSg-vIQR5Q", 30 | "key": "smallNumbers", 31 | "value": "filter(numbers, # < 10)" 32 | } 33 | ] 34 | } 35 | }, 36 | { 37 | "id": "db8797b1-bcc1-4fbf-a5d8-e7d43a181d5e", 38 | "type": "outputNode", 39 | "position": { 40 | "x": 780, 41 | "y": 240 42 | }, 43 | "name": "Response" 44 | } 45 | ], 46 | "edges": [ 47 | { 48 | "id": "5d89c1d6-e894-4e8a-bd13-22368c2a6bc7", 49 | "sourceId": "138b3b11-ff46-450f-9704-3f3c712067b2", 50 | "type": "edge", 51 | "targetId": "db8797b1-bcc1-4fbf-a5d8-e7d43a181d5e" 52 | } 53 | ] 54 | } -------------------------------------------------------------------------------- /core/engine/tests/snapshots/engine__table-loop_0.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: core/engine/tests/engine.rs 3 | expression: serialized_result 4 | --- 5 | performance: "[perf]" 6 | result: 7 | res: 8 | - world: bar 9 | - world: error 10 | trace: 11 | 668e8acb-5b42-4bd6-b399-cbe790df7532: 12 | id: 668e8acb-5b42-4bd6-b399-cbe790df7532 13 | input: 14 | items: 15 | - hello: foo 16 | - hello: koo 17 | name: decisionTable1 18 | order: 19 | "$serde_json::private::Number": "1" 20 | output: 21 | res: 22 | - world: bar 23 | - world: error 24 | performance: "[perf]" 25 | traceData: 26 | - index: 27 | "$serde_json::private::Number": "0" 28 | reference_map: 29 | hello: foo 30 | rule: 31 | _id: 78f3629d-f7eb-4fdb-94b1-c4be9f7bd534 32 | "hello[6d27354e-56ff-4b25-9160-5fb428c98a5d]": "\"foo\"" 33 | - index: 34 | "$serde_json::private::Number": "1" 35 | reference_map: 36 | hello: koo 37 | rule: 38 | _id: 582b14c8-df87-4c83-8954-4904dc811a98 39 | "hello[6d27354e-56ff-4b25-9160-5fb428c98a5d]": "" 40 | da3ed838-ab74-4f55-a703-2d1b7e507722: 41 | id: da3ed838-ab74-4f55-a703-2d1b7e507722 42 | input: ~ 43 | name: request 44 | order: 45 | "$serde_json::private::Number": "0" 46 | output: 47 | items: 48 | - hello: foo 49 | - hello: koo 50 | performance: "[perf]" 51 | traceData: ~ 52 | -------------------------------------------------------------------------------- /core/engine/tests/snapshots/engine__empty-column-with-space_1.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: core/engine/tests/engine.rs 3 | expression: serialized_result 4 | --- 5 | performance: "[perf]" 6 | result: 7 | output: false 8 | trace: 9 | 46fbad36-4bbe-44ac-833f-d30e0d37d8d7: 10 | id: 46fbad36-4bbe-44ac-833f-d30e0d37d8d7 11 | input: 12 | amount: 13 | "$serde_json::private::Number": "10" 14 | name: Amount Check 15 | order: 16 | "$serde_json::private::Number": "1" 17 | output: 18 | output: false 19 | performance: "[perf]" 20 | traceData: 21 | index: 22 | "$serde_json::private::Number": "1" 23 | reference_map: 24 | amount: 25 | "$serde_json::private::Number": "10" 26 | second: ~ 27 | rule: 28 | _id: SY7uwJEPqS 29 | "amount[6xj5CMIFv9]": "" 30 | "second[07795ded-cb9b-4165-9b5e-783b066dda61]": "" 31 | 4e7e6bb9-f128-41e7-8cc5-b9d79670b96a: 32 | id: 4e7e6bb9-f128-41e7-8cc5-b9d79670b96a 33 | input: ~ 34 | name: Request 35 | order: 36 | "$serde_json::private::Number": "0" 37 | output: 38 | amount: 39 | "$serde_json::private::Number": "10" 40 | performance: "[perf]" 41 | traceData: ~ 42 | 95aa8f3c-f371-4e48-beb3-0b5775d2a814: 43 | id: 95aa8f3c-f371-4e48-beb3-0b5775d2a814 44 | input: 45 | output: false 46 | name: Response 47 | order: 48 | "$serde_json::private::Number": "2" 49 | output: ~ 50 | performance: "[perf]" 51 | traceData: ~ 52 | -------------------------------------------------------------------------------- /core/engine/tests/snapshots/engine__empty-column-without-space_1.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: core/engine/tests/engine.rs 3 | expression: serialized_result 4 | --- 5 | performance: "[perf]" 6 | result: 7 | output: false 8 | trace: 9 | 46fbad36-4bbe-44ac-833f-d30e0d37d8d7: 10 | id: 46fbad36-4bbe-44ac-833f-d30e0d37d8d7 11 | input: 12 | amount: 13 | "$serde_json::private::Number": "10" 14 | name: Amount Check 15 | order: 16 | "$serde_json::private::Number": "1" 17 | output: 18 | output: false 19 | performance: "[perf]" 20 | traceData: 21 | index: 22 | "$serde_json::private::Number": "1" 23 | reference_map: 24 | amount: 25 | "$serde_json::private::Number": "10" 26 | second: ~ 27 | rule: 28 | _id: SY7uwJEPqS 29 | "amount[6xj5CMIFv9]": "" 30 | "second[07795ded-cb9b-4165-9b5e-783b066dda61]": "" 31 | 4e7e6bb9-f128-41e7-8cc5-b9d79670b96a: 32 | id: 4e7e6bb9-f128-41e7-8cc5-b9d79670b96a 33 | input: ~ 34 | name: Request 35 | order: 36 | "$serde_json::private::Number": "0" 37 | output: 38 | amount: 39 | "$serde_json::private::Number": "10" 40 | performance: "[perf]" 41 | traceData: ~ 42 | 95aa8f3c-f371-4e48-beb3-0b5775d2a814: 43 | id: 95aa8f3c-f371-4e48-beb3-0b5775d2a814 44 | input: 45 | output: false 46 | name: Response 47 | order: 48 | "$serde_json::private::Number": "2" 49 | output: ~ 50 | performance: "[perf]" 51 | traceData: ~ 52 | -------------------------------------------------------------------------------- /core/engine/tests/snapshots/engine__expression-passthrough_1.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: core/engine/tests/engine.rs 3 | expression: serialized_result 4 | --- 5 | performance: "[perf]" 6 | result: 7 | a: 8 | "$serde_json::private::Number": "5" 9 | b: 10 | "$serde_json::private::Number": "3" 11 | sum: 12 | "$serde_json::private::Number": "8" 13 | trace: 14 | 6b9cfc7e-4776-4b3f-8a19-c2a4d7874770: 15 | id: 6b9cfc7e-4776-4b3f-8a19-c2a4d7874770 16 | input: 17 | a: 18 | "$serde_json::private::Number": "5" 19 | b: 20 | "$serde_json::private::Number": "3" 21 | sum: 22 | "$serde_json::private::Number": "100" 23 | name: expression1 24 | order: 25 | "$serde_json::private::Number": "1" 26 | output: 27 | a: 28 | "$serde_json::private::Number": "5" 29 | b: 30 | "$serde_json::private::Number": "3" 31 | sum: 32 | "$serde_json::private::Number": "8" 33 | performance: "[perf]" 34 | traceData: 35 | sum: 36 | result: 37 | "$serde_json::private::Number": "8" 38 | deced339-bace-452a-8db0-777f038bffe8: 39 | id: deced339-bace-452a-8db0-777f038bffe8 40 | input: ~ 41 | name: request 42 | order: 43 | "$serde_json::private::Number": "0" 44 | output: 45 | a: 46 | "$serde_json::private::Number": "5" 47 | b: 48 | "$serde_json::private::Number": "3" 49 | sum: 50 | "$serde_json::private::Number": "100" 51 | performance: "[perf]" 52 | traceData: ~ 53 | -------------------------------------------------------------------------------- /test-data/customer-input-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "contentType": "application/vnd.gorules.decision", 3 | "nodes": [ 4 | { 5 | "type": "inputNode", 6 | "id": "4354dede-b4ed-4a57-9b80-45c1e33e2326", 7 | "name": "request", 8 | "content": { 9 | "schema": "{\"$schema\":\"http://json-schema.org/draft-07/schema#\",\"title\":\"Generated schema for Root\",\"type\":\"object\",\"properties\":{\"color\":{\"enum\":[\"red\",\"blue\",\"green\"]},\"customer\":{\"type\":\"object\",\"properties\":{\"firstName\":{\"description\":\"Customer first name\",\"type\":\"string\",\"minimum\":1},\"lastName\":{\"description\":\"Customer last name\",\"type\":\"string\",\"minimum\":1},\"email\":{\"description\":\"Customer email\",\"type\":\"string\",\"format\":\"email\"},\"age\":{\"description\":\"Customer age\",\"type\":\"number\",\"minimum\":18}},\"required\":[\"firstName\",\"lastName\",\"email\",\"age\"]}},\"required\":[\"color\",\"customer\"]}" 10 | }, 11 | "position": { 12 | "x": 90, 13 | "y": 200 14 | } 15 | }, 16 | { 17 | "type": "outputNode", 18 | "id": "27e18970-f565-43eb-859e-568c9f53b7a8", 19 | "name": "response", 20 | "position": { 21 | "x": 510, 22 | "y": 200 23 | } 24 | } 25 | ], 26 | "edges": [ 27 | { 28 | "id": "4c036317-bc39-4ad2-a825-adbfd4ca2df6", 29 | "sourceId": "4354dede-b4ed-4a57-9b80-45c1e33e2326", 30 | "type": "edge", 31 | "targetId": "27e18970-f565-43eb-859e-568c9f53b7a8" 32 | } 33 | ] 34 | } -------------------------------------------------------------------------------- /test-data/customer-output-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "contentType": "application/vnd.gorules.decision", 3 | "nodes": [ 4 | { 5 | "type": "inputNode", 6 | "id": "4354dede-b4ed-4a57-9b80-45c1e33e2326", 7 | "name": "request", 8 | "position": { 9 | "x": 90, 10 | "y": 200 11 | } 12 | }, 13 | { 14 | "type": "outputNode", 15 | "id": "27e18970-f565-43eb-859e-568c9f53b7a8", 16 | "name": "response", 17 | "content": { 18 | "schema": "{\"$schema\":\"http://json-schema.org/draft-07/schema#\",\"title\":\"Generated schema for Root\",\"type\":\"object\",\"properties\":{\"color\":{\"enum\":[\"red\",\"blue\",\"green\"]},\"customer\":{\"type\":\"object\",\"properties\":{\"firstName\":{\"description\":\"Customer first name\",\"type\":\"string\",\"minimum\":1},\"lastName\":{\"description\":\"Customer last name\",\"type\":\"string\",\"minimum\":1},\"email\":{\"description\":\"Customer email\",\"type\":\"string\",\"format\":\"email\"},\"age\":{\"description\":\"Customer age\",\"type\":\"number\",\"minimum\":18}},\"required\":[\"firstName\",\"lastName\",\"email\",\"age\"]}},\"required\":[\"color\",\"customer\"]}" 19 | }, 20 | "position": { 21 | "x": 510, 22 | "y": 200 23 | } 24 | } 25 | ], 26 | "edges": [ 27 | { 28 | "id": "4c036317-bc39-4ad2-a825-adbfd4ca2df6", 29 | "sourceId": "4354dede-b4ed-4a57-9b80-45c1e33e2326", 30 | "type": "edge", 31 | "targetId": "27e18970-f565-43eb-859e-568c9f53b7a8" 32 | } 33 | ] 34 | } -------------------------------------------------------------------------------- /core/engine/tests/snapshots/engine__empty-column-with-space_0.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: core/engine/tests/engine.rs 3 | expression: serialized_result 4 | --- 5 | performance: "[perf]" 6 | result: 7 | output: true 8 | trace: 9 | 46fbad36-4bbe-44ac-833f-d30e0d37d8d7: 10 | id: 46fbad36-4bbe-44ac-833f-d30e0d37d8d7 11 | input: 12 | amount: 13 | "$serde_json::private::Number": "10000000" 14 | name: Amount Check 15 | order: 16 | "$serde_json::private::Number": "1" 17 | output: 18 | output: true 19 | performance: "[perf]" 20 | traceData: 21 | index: 22 | "$serde_json::private::Number": "0" 23 | reference_map: 24 | amount: 25 | "$serde_json::private::Number": "10000000" 26 | second: ~ 27 | rule: 28 | _id: fJxWqBVUNk 29 | "amount[6xj5CMIFv9]": "> 1_000_000" 30 | "second[07795ded-cb9b-4165-9b5e-783b066dda61]": "" 31 | 4e7e6bb9-f128-41e7-8cc5-b9d79670b96a: 32 | id: 4e7e6bb9-f128-41e7-8cc5-b9d79670b96a 33 | input: ~ 34 | name: Request 35 | order: 36 | "$serde_json::private::Number": "0" 37 | output: 38 | amount: 39 | "$serde_json::private::Number": "10000000" 40 | performance: "[perf]" 41 | traceData: ~ 42 | 95aa8f3c-f371-4e48-beb3-0b5775d2a814: 43 | id: 95aa8f3c-f371-4e48-beb3-0b5775d2a814 44 | input: 45 | output: true 46 | name: Response 47 | order: 48 | "$serde_json::private::Number": "2" 49 | output: ~ 50 | performance: "[perf]" 51 | traceData: ~ 52 | -------------------------------------------------------------------------------- /core/engine/tests/snapshots/engine__empty-column-without-space_0.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: core/engine/tests/engine.rs 3 | expression: serialized_result 4 | --- 5 | performance: "[perf]" 6 | result: 7 | output: true 8 | trace: 9 | 46fbad36-4bbe-44ac-833f-d30e0d37d8d7: 10 | id: 46fbad36-4bbe-44ac-833f-d30e0d37d8d7 11 | input: 12 | amount: 13 | "$serde_json::private::Number": "10000000" 14 | name: Amount Check 15 | order: 16 | "$serde_json::private::Number": "1" 17 | output: 18 | output: true 19 | performance: "[perf]" 20 | traceData: 21 | index: 22 | "$serde_json::private::Number": "0" 23 | reference_map: 24 | amount: 25 | "$serde_json::private::Number": "10000000" 26 | second: ~ 27 | rule: 28 | _id: fJxWqBVUNk 29 | "amount[6xj5CMIFv9]": "> 1_000_000" 30 | "second[07795ded-cb9b-4165-9b5e-783b066dda61]": "" 31 | 4e7e6bb9-f128-41e7-8cc5-b9d79670b96a: 32 | id: 4e7e6bb9-f128-41e7-8cc5-b9d79670b96a 33 | input: ~ 34 | name: Request 35 | order: 36 | "$serde_json::private::Number": "0" 37 | output: 38 | amount: 39 | "$serde_json::private::Number": "10000000" 40 | performance: "[perf]" 41 | traceData: ~ 42 | 95aa8f3c-f371-4e48-beb3-0b5775d2a814: 43 | id: 95aa8f3c-f371-4e48-beb3-0b5775d2a814 44 | input: 45 | output: true 46 | name: Response 47 | order: 48 | "$serde_json::private::Number": "2" 49 | output: ~ 50 | performance: "[perf]" 51 | traceData: ~ 52 | -------------------------------------------------------------------------------- /core/engine/tests/snapshots/engine__expression-passthrough_3.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: core/engine/tests/engine.rs 3 | expression: serialized_result 4 | --- 5 | performance: "[perf]" 6 | result: 7 | a: 8 | "$serde_json::private::Number": "7" 9 | b: 10 | "$serde_json::private::Number": "3" 11 | passThrough: Test 12 | sum: 13 | "$serde_json::private::Number": "10" 14 | trace: 15 | 6b9cfc7e-4776-4b3f-8a19-c2a4d7874770: 16 | id: 6b9cfc7e-4776-4b3f-8a19-c2a4d7874770 17 | input: 18 | a: 19 | "$serde_json::private::Number": "7" 20 | b: 21 | "$serde_json::private::Number": "3" 22 | passThrough: Test 23 | sum: Original 24 | name: expression1 25 | order: 26 | "$serde_json::private::Number": "1" 27 | output: 28 | a: 29 | "$serde_json::private::Number": "7" 30 | b: 31 | "$serde_json::private::Number": "3" 32 | passThrough: Test 33 | sum: 34 | "$serde_json::private::Number": "10" 35 | performance: "[perf]" 36 | traceData: 37 | sum: 38 | result: 39 | "$serde_json::private::Number": "10" 40 | deced339-bace-452a-8db0-777f038bffe8: 41 | id: deced339-bace-452a-8db0-777f038bffe8 42 | input: ~ 43 | name: request 44 | order: 45 | "$serde_json::private::Number": "0" 46 | output: 47 | a: 48 | "$serde_json::private::Number": "7" 49 | b: 50 | "$serde_json::private::Number": "3" 51 | passThrough: Test 52 | sum: Original 53 | performance: "[perf]" 54 | traceData: ~ 55 | -------------------------------------------------------------------------------- /.github/workflows/python-version-bump.yaml: -------------------------------------------------------------------------------- 1 | name: "Release: Python" 2 | 3 | env: 4 | WORKING_DIRECTORY: bindings/python 5 | 6 | on: 7 | workflow_dispatch: 8 | inputs: 9 | version: 10 | description: 'Version' 11 | required: true 12 | default: 'patch' 13 | type: choice 14 | options: 15 | - patch 16 | - minor 17 | - major 18 | 19 | permissions: 20 | contents: write 21 | 22 | jobs: 23 | publish: 24 | name: Version 25 | runs-on: ubuntu-latest 26 | defaults: 27 | run: 28 | working-directory: ${{ env.WORKING_DIRECTORY }} 29 | steps: 30 | - uses: actions/checkout@v3 31 | with: 32 | persist-credentials: false 33 | 34 | - name: Set Git author 35 | run: | 36 | git config user.name "Bot" 37 | git config user.email "bot@gorules.io" 38 | 39 | - uses: actions/setup-python@v4 40 | with: 41 | python-version: '3.10' 42 | 43 | - name: 'Install dependencies' 44 | run: pip install --upgrade bump2version 45 | 46 | - name: Bumpversion 47 | run: bumpversion ${{ github.event.inputs.version }} --allow-dirty --tag-name "python-v{new_version}" 48 | env: 49 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 50 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 51 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 52 | 53 | - name: Push changes 54 | uses: ad-m/github-push-action@v0.6.0 55 | with: 56 | github_token: ${{ secrets.PAT}} 57 | -------------------------------------------------------------------------------- /.github/workflows/uniffi-version-bump.yaml: -------------------------------------------------------------------------------- 1 | name: "Release: Uniffi" 2 | 3 | env: 4 | WORKING_DIRECTORY: bindings/uniffi 5 | 6 | on: 7 | workflow_dispatch: 8 | inputs: 9 | version: 10 | description: 'Version' 11 | required: true 12 | default: 'patch' 13 | type: choice 14 | options: 15 | - patch 16 | - minor 17 | - major 18 | 19 | permissions: 20 | contents: write 21 | 22 | jobs: 23 | publish: 24 | name: Version 25 | runs-on: ubuntu-latest 26 | defaults: 27 | run: 28 | working-directory: ${{ env.WORKING_DIRECTORY }} 29 | steps: 30 | - uses: actions/checkout@v3 31 | with: 32 | persist-credentials: false 33 | 34 | - name: Set Git author 35 | run: | 36 | git config user.name "Bot" 37 | git config user.email "bot@gorules.io" 38 | 39 | - uses: actions/setup-python@v4 40 | with: 41 | python-version: '3.10' 42 | 43 | - name: 'Install dependencies' 44 | run: pip install --upgrade bump2version 45 | 46 | - name: Bumpversion 47 | run: bumpversion ${{ github.event.inputs.version }} --allow-dirty --tag-name "uniffi-v{new_version}" 48 | env: 49 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 50 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 51 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 52 | 53 | - name: Push changes 54 | uses: ad-m/github-push-action@v0.6.0 55 | with: 56 | github_token: ${{ secrets.PAT}} 57 | -------------------------------------------------------------------------------- /core/engine/tests/snapshots/engine__expression-passthrough_2.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: core/engine/tests/engine.rs 3 | expression: serialized_result 4 | --- 5 | performance: "[perf]" 6 | result: 7 | a: 8 | "$serde_json::private::Number": "10" 9 | b: 10 | "$serde_json::private::Number": "20" 11 | extra: This should pass through 12 | sum: 13 | "$serde_json::private::Number": "30" 14 | trace: 15 | 6b9cfc7e-4776-4b3f-8a19-c2a4d7874770: 16 | id: 6b9cfc7e-4776-4b3f-8a19-c2a4d7874770 17 | input: 18 | a: 19 | "$serde_json::private::Number": "10" 20 | b: 21 | "$serde_json::private::Number": "20" 22 | extra: This should pass through 23 | name: expression1 24 | order: 25 | "$serde_json::private::Number": "1" 26 | output: 27 | a: 28 | "$serde_json::private::Number": "10" 29 | b: 30 | "$serde_json::private::Number": "20" 31 | extra: This should pass through 32 | sum: 33 | "$serde_json::private::Number": "30" 34 | performance: "[perf]" 35 | traceData: 36 | sum: 37 | result: 38 | "$serde_json::private::Number": "30" 39 | deced339-bace-452a-8db0-777f038bffe8: 40 | id: deced339-bace-452a-8db0-777f038bffe8 41 | input: ~ 42 | name: request 43 | order: 44 | "$serde_json::private::Number": "0" 45 | output: 46 | a: 47 | "$serde_json::private::Number": "10" 48 | b: 49 | "$serde_json::private::Number": "20" 50 | extra: This should pass through 51 | performance: "[perf]" 52 | traceData: ~ 53 | -------------------------------------------------------------------------------- /core/engine/src/loader/memory.rs: -------------------------------------------------------------------------------- 1 | use crate::loader::{DecisionLoader, LoaderError, LoaderResponse}; 2 | use crate::model::DecisionContent; 3 | use ahash::HashMap; 4 | use std::future::Future; 5 | use std::pin::Pin; 6 | use std::sync::{Arc, RwLock}; 7 | 8 | /// Loads decisions from in-memory hashmap 9 | #[derive(Debug, Default)] 10 | pub struct MemoryLoader { 11 | memory_refs: RwLock>>, 12 | } 13 | 14 | impl MemoryLoader { 15 | pub fn add(&self, key: K, content: D) 16 | where 17 | K: Into, 18 | D: Into, 19 | { 20 | let mut mref = self.memory_refs.write().unwrap(); 21 | mref.insert(key.into(), Arc::new(content.into())); 22 | } 23 | 24 | pub fn get(&self, key: K) -> Option> 25 | where 26 | K: AsRef, 27 | { 28 | let mref = self.memory_refs.read().unwrap(); 29 | mref.get(key.as_ref()).map(|r| r.clone()) 30 | } 31 | 32 | pub fn remove(&self, key: K) -> bool 33 | where 34 | K: AsRef, 35 | { 36 | let mut mref = self.memory_refs.write().unwrap(); 37 | mref.remove(key.as_ref()).is_some() 38 | } 39 | } 40 | 41 | impl DecisionLoader for MemoryLoader { 42 | fn load<'a>( 43 | &'a self, 44 | key: &'a str, 45 | ) -> Pin + 'a + Send>> { 46 | Box::pin(async move { 47 | self.get(key) 48 | .ok_or_else(|| LoaderError::NotFound(key.to_string()).into()) 49 | }) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /bindings/python/src/content.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Context; 2 | use pyo3::prelude::{PyAnyMethods, PyStringMethods}; 3 | use pyo3::types::PyString; 4 | use pyo3::{pyclass, pymethods, Bound, FromPyObject, PyAny, PyResult}; 5 | use pythonize::depythonize; 6 | use std::sync::Arc; 7 | use zen_engine::model::DecisionContent; 8 | 9 | #[pyclass] 10 | #[pyo3(name = "ZenDecisionContent")] 11 | pub struct PyZenDecisionContent(pub Arc); 12 | 13 | #[pymethods] 14 | impl PyZenDecisionContent { 15 | #[new] 16 | pub fn new(data: &str) -> PyResult { 17 | let mut content: DecisionContent = 18 | serde_json::from_str(data).context("Failed to parse JSON")?; 19 | content.compile(); 20 | Ok(Self(Arc::new(content))) 21 | } 22 | } 23 | 24 | pub struct PyZenDecisionContentJson(pub PyZenDecisionContent); 25 | 26 | impl<'py> FromPyObject<'py> for PyZenDecisionContentJson { 27 | fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult { 28 | if let Ok(s) = ob.downcast::() { 29 | let borrow_ref = s.borrow(); 30 | let content = borrow_ref.0.clone(); 31 | 32 | return Ok(Self(PyZenDecisionContent(content))); 33 | } 34 | 35 | if let Ok(b) = ob.downcast::() { 36 | let str = b.to_str()?; 37 | let content = serde_json::from_str(str).context("Invalid JSON")?; 38 | 39 | return Ok(Self(PyZenDecisionContent(Arc::new(content)))); 40 | } 41 | 42 | let content = depythonize(ob)?; 43 | Ok(Self(PyZenDecisionContent(Arc::new(content)))) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /core/engine/src/decision_graph/error.rs: -------------------------------------------------------------------------------- 1 | use serde::ser::SerializeMap; 2 | use serde::{Serialize, Serializer}; 3 | use thiserror::Error; 4 | 5 | #[derive(Debug, Error)] 6 | pub enum DecisionGraphValidationError { 7 | #[error("Invalid input node count: {0}")] 8 | InvalidInputCount(u32), 9 | 10 | #[error("Invalid output node count: {0}")] 11 | InvalidOutputCount(u32), 12 | 13 | #[error("Cyclic graph detected")] 14 | CyclicGraph, 15 | 16 | #[error("Missing node")] 17 | MissingNode(String), 18 | } 19 | 20 | impl Serialize for DecisionGraphValidationError { 21 | fn serialize(&self, serializer: S) -> Result 22 | where 23 | S: Serializer, 24 | { 25 | let mut map = serializer.serialize_map(None)?; 26 | 27 | match &self { 28 | DecisionGraphValidationError::InvalidInputCount(count) => { 29 | map.serialize_entry("type", "invalidInputCount")?; 30 | map.serialize_entry("nodeCount", count)?; 31 | } 32 | DecisionGraphValidationError::InvalidOutputCount(count) => { 33 | map.serialize_entry("type", "invalidOutputCount")?; 34 | map.serialize_entry("nodeCount", count)?; 35 | } 36 | DecisionGraphValidationError::MissingNode(node_id) => { 37 | map.serialize_entry("type", "missingNode")?; 38 | map.serialize_entry("nodeId", node_id)?; 39 | } 40 | DecisionGraphValidationError::CyclicGraph => { 41 | map.serialize_entry("type", "cyclicGraph")?; 42 | } 43 | } 44 | 45 | map.end() 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /core/engine/src/nodes/function/v1/mod.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Deref; 2 | use std::sync::Arc; 3 | use std::time::{Duration, Instant}; 4 | 5 | use crate::nodes::definition::NodeHandler; 6 | use crate::nodes::function::v1::runtime::create_runtime; 7 | use crate::nodes::function::v1::script::Script; 8 | use crate::nodes::result::NodeResult; 9 | use crate::nodes::{NodeContext, NodeContextExt}; 10 | use serde_json::Value; 11 | use zen_expression::variable::ToVariable; 12 | 13 | pub(crate) mod runtime; 14 | mod script; 15 | 16 | #[derive(Debug, Clone)] 17 | pub struct FunctionV1NodeHandler; 18 | 19 | const MAX_DURATION: Duration = Duration::from_millis(500); 20 | 21 | impl NodeHandler for FunctionV1NodeHandler { 22 | type NodeData = Arc; 23 | type TraceData = FunctionV1Trace; 24 | 25 | async fn handle(&self, ctx: NodeContext) -> NodeResult { 26 | let start = Instant::now(); 27 | let runtime = create_runtime().node_context_message(&ctx, "Failed to create JS Runtime")?; 28 | let interrupt_handler = Box::new(move || start.elapsed() > MAX_DURATION); 29 | 30 | runtime.set_interrupt_handler(Some(interrupt_handler)); 31 | 32 | let mut script = Script::new(runtime.clone()); 33 | let result_response = script.call(ctx.node.deref(), &ctx.input).await; 34 | 35 | runtime.set_interrupt_handler(None); 36 | 37 | let response = result_response.node_context(&ctx)?; 38 | ctx.trace(|t| { 39 | t.log = response.log; 40 | }); 41 | 42 | ctx.success(response.output) 43 | } 44 | } 45 | 46 | #[derive(Debug, Clone, Default, ToVariable)] 47 | pub struct FunctionV1Trace { 48 | log: Vec, 49 | } 50 | -------------------------------------------------------------------------------- /.github/workflows/node-version-bump.yaml: -------------------------------------------------------------------------------- 1 | name: "Release: NodeJS" 2 | 3 | env: 4 | WORKING_DIRECTORY: bindings/nodejs 5 | 6 | on: 7 | workflow_dispatch: 8 | inputs: 9 | version: 10 | description: 'Version' 11 | required: true 12 | default: 'patch' 13 | type: choice 14 | options: 15 | - patch 16 | - minor 17 | - major 18 | 19 | permissions: 20 | contents: write 21 | 22 | jobs: 23 | publish: 24 | name: Version 25 | runs-on: ubuntu-latest 26 | defaults: 27 | run: 28 | working-directory: ${{ env.WORKING_DIRECTORY }} 29 | steps: 30 | - uses: actions/checkout@v3 31 | with: 32 | persist-credentials: false 33 | 34 | - name: Set Git author 35 | run: | 36 | git config user.name "Bot" 37 | git config user.email "bot@gorules.io" 38 | 39 | - name: Setup node 40 | uses: actions/setup-node@v3 41 | with: 42 | node-version: 22 43 | check-latest: true 44 | cache: yarn 45 | cache-dependency-path: 'bindings/nodejs/yarn.lock' 46 | 47 | - name: 'Install dependencies' 48 | run: yarn install --immutable --mode=skip-build 49 | 50 | - name: Lerna version 51 | run: yarn lerna version ${{ github.event.inputs.version }} --tag-version-prefix="nodejs-" --no-push --yes 52 | env: 53 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 54 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 55 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 56 | 57 | - name: Push changes 58 | uses: ad-m/github-push-action@v0.6.0 59 | with: 60 | github_token: ${{ secrets.PAT}} -------------------------------------------------------------------------------- /core/engine/tests/snapshots/engine__expression-fields_0.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: core/engine/tests/engine.rs 3 | expression: serialized_result 4 | --- 5 | performance: "[perf]" 6 | result: 7 | customer: 8 | age: 9 | "$serde_json::private::Number": "30" 10 | firstName: John 11 | fullName: John Doe 12 | lastName: Doe 13 | order: 14 | id: ORD-001 15 | total: 16 | "$serde_json::private::Number": "100" 17 | trace: 18 | expression-node-1: 19 | id: expression-node-1 20 | input: 21 | customer: 22 | age: 23 | "$serde_json::private::Number": "30" 24 | firstName: John 25 | lastName: Doe 26 | order: 27 | id: ORD-001 28 | total: 29 | "$serde_json::private::Number": "100" 30 | name: customerFullName 31 | order: 32 | "$serde_json::private::Number": "1" 33 | output: 34 | customer: 35 | age: 36 | "$serde_json::private::Number": "30" 37 | firstName: John 38 | fullName: John Doe 39 | lastName: Doe 40 | order: 41 | id: ORD-001 42 | total: 43 | "$serde_json::private::Number": "100" 44 | performance: "[perf]" 45 | traceData: 46 | fullName: 47 | result: John Doe 48 | input-node: 49 | id: input-node 50 | input: ~ 51 | name: request 52 | order: 53 | "$serde_json::private::Number": "0" 54 | output: 55 | customer: 56 | age: 57 | "$serde_json::private::Number": "30" 58 | firstName: John 59 | lastName: Doe 60 | order: 61 | id: ORD-001 62 | total: 63 | "$serde_json::private::Number": "100" 64 | performance: "[perf]" 65 | traceData: ~ 66 | -------------------------------------------------------------------------------- /core/engine/tests/snapshots/engine__merch-bags_0.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: core/engine/tests/engine.rs 3 | expression: serialized_result 4 | --- 5 | performance: "[perf]" 6 | result: 7 | code: BAG10 8 | price: 9 | "$serde_json::private::Number": "12" 10 | trace: 11 | 36aa02a4-df14-4c54-a852-175e749d5860: 12 | id: 36aa02a4-df14-4c54-a852-175e749d5860 13 | input: ~ 14 | name: Request 15 | order: 16 | "$serde_json::private::Number": "0" 17 | output: 18 | class: economy 19 | origin: JFK 20 | performance: "[perf]" 21 | traceData: ~ 22 | 5d03a351-786f-4f7d-9b2b-709dc0d81460: 23 | id: 5d03a351-786f-4f7d-9b2b-709dc0d81460 24 | input: 25 | class: economy 26 | origin: JFK 27 | name: Bags 28 | order: 29 | "$serde_json::private::Number": "1" 30 | output: 31 | code: BAG10 32 | price: 33 | "$serde_json::private::Number": "12" 34 | performance: "[perf]" 35 | traceData: 36 | index: 37 | "$serde_json::private::Number": "1" 38 | reference_map: 39 | class: economy 40 | destination: ~ 41 | origin: JFK 42 | rule: 43 | _id: a6083cab-8dfe-46ae-acf3-823e02ca3f3b 44 | "class[b2e2476f-5340-4d66-aec0-9a282be0e716]": "\"economy\"" 45 | "destination[UmZtXogtD7]": "" 46 | "origin[a9711b12-1d29-40a3-a48f-2c03df5c8aff]": "\"JFK\"" 47 | f42391bc-6183-4b12-8eaa-6b56510c17ef: 48 | id: f42391bc-6183-4b12-8eaa-6b56510c17ef 49 | input: 50 | code: BAG10 51 | price: 52 | "$serde_json::private::Number": "12" 53 | name: Response 54 | order: 55 | "$serde_json::private::Number": "2" 56 | output: ~ 57 | performance: "[perf]" 58 | traceData: ~ 59 | -------------------------------------------------------------------------------- /core/engine/tests/snapshots/engine__expression-fields_1.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: core/engine/tests/engine.rs 3 | expression: serialized_result 4 | --- 5 | performance: "[perf]" 6 | result: 7 | customer: 8 | age: 9 | "$serde_json::private::Number": "25" 10 | firstName: Jane 11 | fullName: Jane Smith 12 | lastName: Smith 13 | order: 14 | id: ORD-002 15 | total: 16 | "$serde_json::private::Number": "150" 17 | trace: 18 | expression-node-1: 19 | id: expression-node-1 20 | input: 21 | customer: 22 | age: 23 | "$serde_json::private::Number": "25" 24 | firstName: Jane 25 | lastName: Smith 26 | order: 27 | id: ORD-002 28 | total: 29 | "$serde_json::private::Number": "150" 30 | name: customerFullName 31 | order: 32 | "$serde_json::private::Number": "1" 33 | output: 34 | customer: 35 | age: 36 | "$serde_json::private::Number": "25" 37 | firstName: Jane 38 | fullName: Jane Smith 39 | lastName: Smith 40 | order: 41 | id: ORD-002 42 | total: 43 | "$serde_json::private::Number": "150" 44 | performance: "[perf]" 45 | traceData: 46 | fullName: 47 | result: Jane Smith 48 | input-node: 49 | id: input-node 50 | input: ~ 51 | name: request 52 | order: 53 | "$serde_json::private::Number": "0" 54 | output: 55 | customer: 56 | age: 57 | "$serde_json::private::Number": "25" 58 | firstName: Jane 59 | lastName: Smith 60 | order: 61 | id: ORD-002 62 | total: 63 | "$serde_json::private::Number": "150" 64 | performance: "[perf]" 65 | traceData: ~ 66 | -------------------------------------------------------------------------------- /bindings/nodejs/wasi-worker.mjs: -------------------------------------------------------------------------------- 1 | import fs from "node:fs"; 2 | import { createRequire } from "node:module"; 3 | import { parse } from "node:path"; 4 | import { WASI } from "node:wasi"; 5 | import { parentPort, Worker } from "node:worker_threads"; 6 | 7 | const require = createRequire(import.meta.url); 8 | 9 | const { instantiateNapiModuleSync, MessageHandler, getDefaultContext } = require("@napi-rs/wasm-runtime"); 10 | 11 | if (parentPort) { 12 | parentPort.on("message", (data) => { 13 | globalThis.onmessage({ data }); 14 | }); 15 | } 16 | 17 | Object.assign(globalThis, { 18 | self: globalThis, 19 | require, 20 | Worker, 21 | importScripts: function (f) { 22 | ;(0, eval)(fs.readFileSync(f, "utf8") + "//# sourceURL=" + f); 23 | }, 24 | postMessage: function (msg) { 25 | if (parentPort) { 26 | parentPort.postMessage(msg); 27 | } 28 | }, 29 | }); 30 | 31 | const emnapiContext = getDefaultContext(); 32 | 33 | const __rootDir = parse(process.cwd()).root; 34 | 35 | const handler = new MessageHandler({ 36 | onLoad({ wasmModule, wasmMemory }) { 37 | const wasi = new WASI({ 38 | version: 'preview1', 39 | env: process.env, 40 | preopens: { 41 | [__rootDir]: __rootDir, 42 | }, 43 | }); 44 | 45 | return instantiateNapiModuleSync(wasmModule, { 46 | childThread: true, 47 | wasi, 48 | context: emnapiContext, 49 | overwriteImports(importObject) { 50 | importObject.env = { 51 | ...importObject.env, 52 | ...importObject.napi, 53 | ...importObject.emnapi, 54 | memory: wasmMemory 55 | }; 56 | }, 57 | }); 58 | }, 59 | }); 60 | 61 | globalThis.onmessage = function (e) { 62 | handler.handle(e); 63 | }; 64 | -------------------------------------------------------------------------------- /core/engine/src/nodes/function/v2/module/http/backend/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::nodes::function::v2::error::ResultExt; 2 | use crate::nodes::function::v2::module::http::{HttpMethod, HttpRequestConfig}; 3 | use rquickjs::{Ctx, FromJs, IntoJs, Object, Value}; 4 | use std::future::Future; 5 | use std::pin::Pin; 6 | 7 | #[cfg(not(target_family = "wasm"))] 8 | pub(crate) mod native; 9 | 10 | pub(crate) mod callback; 11 | 12 | #[derive(Debug)] 13 | pub(crate) struct HttpResponse<'js> { 14 | pub data: Value<'js>, 15 | pub headers: Value<'js>, 16 | pub status: u16, 17 | } 18 | 19 | impl<'js> IntoJs<'js> for HttpResponse<'js> { 20 | fn into_js(self, ctx: &Ctx<'js>) -> rquickjs::Result> { 21 | let object = Object::new(ctx.clone())?; 22 | object.set("data", self.data)?; 23 | object.set("headers", self.headers)?; 24 | object.set("status", self.status)?; 25 | 26 | Ok(object.into_value()) 27 | } 28 | } 29 | 30 | impl<'js> FromJs<'js> for HttpResponse<'js> { 31 | fn from_js(ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result { 32 | let object = value.as_object().or_throw(&ctx)?; 33 | 34 | Ok(HttpResponse { 35 | status: object.get("status").or_throw(&ctx)?, 36 | data: object.get("data").or_throw(&ctx)?, 37 | headers: object.get("headers").or_throw(&ctx)?, 38 | }) 39 | } 40 | } 41 | 42 | /// Trait for HTTP backend implementations 43 | pub(crate) trait HttpBackend { 44 | fn execute_http<'js>( 45 | &self, 46 | ctx: Ctx<'js>, 47 | method: HttpMethod, 48 | url: String, 49 | config: HttpRequestConfig, 50 | ) -> Pin>> + 'js>>; 51 | } 52 | -------------------------------------------------------------------------------- /core/expression/src/exports.rs: -------------------------------------------------------------------------------- 1 | use crate::expression::{Standard, Unary}; 2 | use crate::variable::Variable; 3 | use crate::{Expression, Isolate, IsolateError}; 4 | 5 | /// Evaluates a standard expression 6 | pub fn evaluate_expression(expression: &str, context: Variable) -> Result { 7 | Isolate::with_environment(context).run_standard(expression) 8 | } 9 | 10 | /// Evaluates a unary expression; Required: context must be an object with "$" key. 11 | pub fn evaluate_unary_expression( 12 | expression: &str, 13 | context: Variable, 14 | ) -> Result { 15 | let Some(context_object_ref) = context.as_object() else { 16 | return Err(IsolateError::MissingContextReference); 17 | }; 18 | 19 | let context_object = context_object_ref.borrow(); 20 | if !context_object.contains_key("$") { 21 | return Err(IsolateError::MissingContextReference); 22 | } 23 | 24 | Isolate::with_environment(context).run_unary(expression) 25 | } 26 | 27 | /// Compiles a standard expression 28 | pub fn compile_expression(expression: &str) -> Result, IsolateError> { 29 | Isolate::new().compile_standard(expression) 30 | } 31 | 32 | /// Compiles an unary expression 33 | pub fn compile_unary_expression(expression: &str) -> Result, IsolateError> { 34 | Isolate::new().compile_unary(expression) 35 | } 36 | 37 | #[cfg(test)] 38 | mod test { 39 | use crate::evaluate_expression; 40 | use serde_json::json; 41 | 42 | #[test] 43 | fn example() { 44 | let context = json!({ "tax": { "percentage": 10 } }); 45 | let tax_amount = evaluate_expression("50 * tax.percentage / 100", context.into()).unwrap(); 46 | 47 | assert_eq!(tax_amount, json!(5).into()); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /core/expression/src/functions/method.rs: -------------------------------------------------------------------------------- 1 | use crate::functions::date_method::DateMethod; 2 | use crate::functions::defs::FunctionDefinition; 3 | use nohash_hasher::{BuildNoHashHasher, IsEnabled}; 4 | use std::cell::RefCell; 5 | use std::collections::HashMap; 6 | use std::fmt::Display; 7 | use std::rc::Rc; 8 | use strum::IntoEnumIterator; 9 | 10 | impl IsEnabled for DateMethod {} 11 | 12 | #[derive(Debug, PartialEq, Eq, Clone)] 13 | pub enum MethodKind { 14 | DateMethod(DateMethod), 15 | } 16 | 17 | impl TryFrom<&str> for MethodKind { 18 | type Error = strum::ParseError; 19 | 20 | fn try_from(value: &str) -> Result { 21 | DateMethod::try_from(value).map(MethodKind::DateMethod) 22 | } 23 | } 24 | 25 | impl Display for MethodKind { 26 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 27 | match self { 28 | MethodKind::DateMethod(d) => write!(f, "{d}"), 29 | } 30 | } 31 | } 32 | 33 | pub struct MethodRegistry { 34 | date_methods: HashMap, BuildNoHashHasher>, 35 | } 36 | 37 | impl MethodRegistry { 38 | thread_local!( 39 | static INSTANCE: RefCell = RefCell::new(MethodRegistry::new_internal()) 40 | ); 41 | 42 | pub fn get_definition(kind: &MethodKind) -> Option> { 43 | match kind { 44 | MethodKind::DateMethod(dm) => { 45 | Self::INSTANCE.with_borrow(|i| i.date_methods.get(&dm).cloned()) 46 | } 47 | } 48 | } 49 | 50 | fn new_internal() -> Self { 51 | let date_methods = DateMethod::iter() 52 | .map(|i| (i.clone(), (&i).into())) 53 | .collect(); 54 | 55 | Self { date_methods } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /core/engine/tests/snapshots/engine__merch-bags_1.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: core/engine/tests/engine.rs 3 | expression: serialized_result 4 | --- 5 | performance: "[perf]" 6 | result: 7 | code: BAG10 8 | price: 9 | "$serde_json::private::Number": "10" 10 | trace: 11 | 36aa02a4-df14-4c54-a852-175e749d5860: 12 | id: 36aa02a4-df14-4c54-a852-175e749d5860 13 | input: ~ 14 | name: Request 15 | order: 16 | "$serde_json::private::Number": "0" 17 | output: 18 | class: economy 19 | destination: LAX 20 | origin: JFK 21 | performance: "[perf]" 22 | traceData: ~ 23 | 5d03a351-786f-4f7d-9b2b-709dc0d81460: 24 | id: 5d03a351-786f-4f7d-9b2b-709dc0d81460 25 | input: 26 | class: economy 27 | destination: LAX 28 | origin: JFK 29 | name: Bags 30 | order: 31 | "$serde_json::private::Number": "1" 32 | output: 33 | code: BAG10 34 | price: 35 | "$serde_json::private::Number": "10" 36 | performance: "[perf]" 37 | traceData: 38 | index: 39 | "$serde_json::private::Number": "0" 40 | reference_map: 41 | class: economy 42 | destination: LAX 43 | origin: JFK 44 | rule: 45 | _id: a882b24f-b796-435e-a116-d7e6e5214ff6 46 | "class[b2e2476f-5340-4d66-aec0-9a282be0e716]": "\"economy\"" 47 | "destination[UmZtXogtD7]": "\"LAX\"" 48 | "origin[a9711b12-1d29-40a3-a48f-2c03df5c8aff]": "\"JFK\"" 49 | f42391bc-6183-4b12-8eaa-6b56510c17ef: 50 | id: f42391bc-6183-4b12-8eaa-6b56510c17ef 51 | input: 52 | code: BAG10 53 | price: 54 | "$serde_json::private::Number": "10" 55 | name: Response 56 | order: 57 | "$serde_json::private::Number": "2" 58 | output: ~ 59 | performance: "[perf]" 60 | traceData: ~ 61 | -------------------------------------------------------------------------------- /core/engine/tests/snapshots/engine__nested-request_0.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: core/engine/tests/engine.rs 3 | expression: serialized_result 4 | --- 5 | performance: "[perf]" 6 | result: 7 | nodeData: {} 8 | trace: 9 | 07f6fe29-bfcc-457f-8c84-a7594e5627e2: 10 | id: 07f6fe29-bfcc-457f-8c84-a7594e5627e2 11 | input: 12 | L1: 13 | carbon_accounting: 14 | score_commentary: There is likely to be 15 | score_label: Moderate risk 16 | name: expression3 17 | order: 18 | "$serde_json::private::Number": "3" 19 | output: 20 | nodeData: {} 21 | performance: "[perf]" 22 | traceData: 23 | nodeData: 24 | result: {} 25 | 2a8241f2-a808-4030-afd3-94ba60d93291: 26 | id: 2a8241f2-a808-4030-afd3-94ba60d93291 27 | input: ~ 28 | name: request 29 | order: 30 | "$serde_json::private::Number": "0" 31 | output: {} 32 | performance: "[perf]" 33 | traceData: ~ 34 | d0435b70-ad45-4de8-9086-2477d66344b9: 35 | id: d0435b70-ad45-4de8-9086-2477d66344b9 36 | input: {} 37 | name: expression1 38 | order: 39 | "$serde_json::private::Number": "1" 40 | output: 41 | L1: 42 | carbon_accounting: 43 | score_commentary: There is likely to be 44 | score_label: Moderate risk 45 | performance: "[perf]" 46 | traceData: 47 | L1.carbon_accounting.score_commentary: 48 | result: There is likely to be 49 | L1.carbon_accounting.score_label: 50 | result: Moderate risk 51 | d438419f-c54a-49b4-b2a0-3bb626b0617f: 52 | id: d438419f-c54a-49b4-b2a0-3bb626b0617f 53 | input: {} 54 | name: expression2 55 | order: 56 | "$serde_json::private::Number": "2" 57 | output: 58 | L1: {} 59 | performance: "[perf]" 60 | traceData: 61 | L1: 62 | result: {} 63 | -------------------------------------------------------------------------------- /core/expression/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["GoRules Team "] 3 | description = "Zen Expression Language" 4 | name = "zen-expression" 5 | license = "MIT" 6 | version = "0.52.2" 7 | edition = "2021" 8 | repository = "https://github.com/gorules/zen.git" 9 | 10 | [dependencies] 11 | anyhow = { workspace = true } 12 | ahash = { workspace = true } 13 | bumpalo = { workspace = true, features = ["collections"] } 14 | chrono = { workspace = true } 15 | chrono-tz = "0.10" 16 | humantime = { workspace = true } 17 | fastrand = { workspace = true } 18 | once_cell = { workspace = true } 19 | regex = { workspace = true, optional = true } 20 | regex-lite = { workspace = true, optional = true } 21 | serde = { workspace = true, features = ["rc", "derive"] } 22 | serde_json = { workspace = true, features = ["arbitrary_precision"] } 23 | strum = { workspace = true } 24 | strum_macros = { workspace = true } 25 | thiserror = { workspace = true } 26 | rust_decimal = { workspace = true, features = ["maths-nopanic"] } 27 | rust_decimal_macros = { workspace = true } 28 | nohash-hasher = { workspace = true } 29 | strsim = "0.11" 30 | iana-time-zone = "0.1" 31 | 32 | zen-macros = { path = "../macros", version = "0.52.2" } 33 | zen-types = { path = "../types", version = "0.52.2" } 34 | 35 | [dev-dependencies] 36 | criterion = { workspace = true } 37 | csv = "1" 38 | serde_json5 = "0.2" 39 | 40 | [target.'cfg(not(target_family = "wasm"))'.dependencies] 41 | recursive = { workspace = true } 42 | 43 | [features] 44 | default = ["regex-deprecated"] 45 | regex-lite = ["dep:regex-lite"] 46 | regex-deprecated = ["dep:regex"] 47 | 48 | [[bench]] 49 | harness = false 50 | name = "lexer" 51 | 52 | [[bench]] 53 | harness = false 54 | name = "standard" 55 | 56 | [[bench]] 57 | harness = false 58 | name = "unary" 59 | 60 | [[bench]] 61 | harness = false 62 | name = "isolate" -------------------------------------------------------------------------------- /core/engine/tests/snapshots/engine__merch-bags_2.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: core/engine/tests/engine.rs 3 | expression: serialized_result 4 | --- 5 | performance: "[perf]" 6 | result: 7 | code: BAG23 8 | included: true 9 | price: 10 | "$serde_json::private::Number": "0" 11 | trace: 12 | 36aa02a4-df14-4c54-a852-175e749d5860: 13 | id: 36aa02a4-df14-4c54-a852-175e749d5860 14 | input: ~ 15 | name: Request 16 | order: 17 | "$serde_json::private::Number": "0" 18 | output: 19 | class: business 20 | destination: LAX 21 | origin: JFK 22 | performance: "[perf]" 23 | traceData: ~ 24 | 5d03a351-786f-4f7d-9b2b-709dc0d81460: 25 | id: 5d03a351-786f-4f7d-9b2b-709dc0d81460 26 | input: 27 | class: business 28 | destination: LAX 29 | origin: JFK 30 | name: Bags 31 | order: 32 | "$serde_json::private::Number": "1" 33 | output: 34 | code: BAG23 35 | included: true 36 | price: 37 | "$serde_json::private::Number": "0" 38 | performance: "[perf]" 39 | traceData: 40 | index: 41 | "$serde_json::private::Number": "4" 42 | reference_map: 43 | class: business 44 | destination: LAX 45 | origin: JFK 46 | rule: 47 | _id: 731388b9-9e94-499a-b201-3f107e7e8a54 48 | "class[b2e2476f-5340-4d66-aec0-9a282be0e716]": "\"business\", \"first\"" 49 | "destination[UmZtXogtD7]": "" 50 | "origin[a9711b12-1d29-40a3-a48f-2c03df5c8aff]": "" 51 | f42391bc-6183-4b12-8eaa-6b56510c17ef: 52 | id: f42391bc-6183-4b12-8eaa-6b56510c17ef 53 | input: 54 | code: BAG23 55 | included: true 56 | price: 57 | "$serde_json::private::Number": "0" 58 | name: Response 59 | order: 60 | "$serde_json::private::Number": "2" 61 | output: ~ 62 | performance: "[perf]" 63 | traceData: ~ 64 | -------------------------------------------------------------------------------- /core/template/src/error.rs: -------------------------------------------------------------------------------- 1 | use serde::ser::SerializeMap; 2 | use serde::{Serialize, Serializer}; 3 | use thiserror::Error; 4 | use zen_expression::IsolateError; 5 | 6 | #[derive(Debug, Error)] 7 | pub enum TemplateRenderError { 8 | #[error("isolate error: {0}")] 9 | IsolateError(IsolateError), 10 | 11 | #[error("parser error: {0}")] 12 | ParserError(ParserError), 13 | } 14 | 15 | impl From for TemplateRenderError { 16 | fn from(value: IsolateError) -> Self { 17 | Self::IsolateError(value) 18 | } 19 | } 20 | 21 | impl From for TemplateRenderError { 22 | fn from(value: ParserError) -> Self { 23 | Self::ParserError(value) 24 | } 25 | } 26 | 27 | impl Serialize for TemplateRenderError { 28 | fn serialize(&self, serializer: S) -> Result 29 | where 30 | S: Serializer, 31 | { 32 | match self { 33 | TemplateRenderError::IsolateError(isolate) => isolate.serialize(serializer), 34 | TemplateRenderError::ParserError(parser) => parser.serialize(serializer), 35 | } 36 | } 37 | } 38 | 39 | #[derive(Debug, Error)] 40 | pub enum ParserError { 41 | #[error("Open bracket")] 42 | OpenBracket, 43 | 44 | #[error("Close bracket")] 45 | CloseBracket, 46 | } 47 | 48 | impl Serialize for ParserError { 49 | fn serialize(&self, serializer: S) -> Result 50 | where 51 | S: Serializer, 52 | { 53 | let mut map = serializer.serialize_map(None)?; 54 | 55 | map.serialize_entry("type", "templateParserError")?; 56 | 57 | match self { 58 | ParserError::OpenBracket => map.serialize_entry("value", "openBracket")?, 59 | ParserError::CloseBracket => map.serialize_entry("value", "closeBracket")?, 60 | } 61 | 62 | map.end() 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /core/engine/tests/snapshots/engine__expression-passthrough_4.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: core/engine/tests/engine.rs 3 | expression: serialized_result 4 | --- 5 | performance: "[perf]" 6 | result: 7 | a: 8 | "$serde_json::private::Number": "1" 9 | b: 10 | "$serde_json::private::Number": "1" 11 | c: 12 | "$serde_json::private::Number": "3" 13 | d: 14 | "$serde_json::private::Number": "4" 15 | sum: 16 | "$serde_json::private::Number": "2" 17 | trace: 18 | 6b9cfc7e-4776-4b3f-8a19-c2a4d7874770: 19 | id: 6b9cfc7e-4776-4b3f-8a19-c2a4d7874770 20 | input: 21 | a: 22 | "$serde_json::private::Number": "1" 23 | b: 24 | "$serde_json::private::Number": "1" 25 | c: 26 | "$serde_json::private::Number": "3" 27 | d: 28 | "$serde_json::private::Number": "4" 29 | name: expression1 30 | order: 31 | "$serde_json::private::Number": "1" 32 | output: 33 | a: 34 | "$serde_json::private::Number": "1" 35 | b: 36 | "$serde_json::private::Number": "1" 37 | c: 38 | "$serde_json::private::Number": "3" 39 | d: 40 | "$serde_json::private::Number": "4" 41 | sum: 42 | "$serde_json::private::Number": "2" 43 | performance: "[perf]" 44 | traceData: 45 | sum: 46 | result: 47 | "$serde_json::private::Number": "2" 48 | deced339-bace-452a-8db0-777f038bffe8: 49 | id: deced339-bace-452a-8db0-777f038bffe8 50 | input: ~ 51 | name: request 52 | order: 53 | "$serde_json::private::Number": "0" 54 | output: 55 | a: 56 | "$serde_json::private::Number": "1" 57 | b: 58 | "$serde_json::private::Number": "1" 59 | c: 60 | "$serde_json::private::Number": "3" 61 | d: 62 | "$serde_json::private::Number": "4" 63 | performance: "[perf]" 64 | traceData: ~ 65 | -------------------------------------------------------------------------------- /test-data/expression.json: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": [ 3 | { 4 | "id": "115975ef-2f43-4e22-b553-0da6f4cc7f68", 5 | "type": "inputNode", 6 | "position": { 7 | "x": 180, 8 | "y": 240 9 | }, 10 | "name": "Request" 11 | }, 12 | { 13 | "id": "138b3b11-ff46-450f-9704-3f3c712067b2", 14 | "type": "expressionNode", 15 | "position": { 16 | "x": 470, 17 | "y": 240 18 | }, 19 | "name": "expressionNode 1", 20 | "content": { 21 | "expressions": [ 22 | { 23 | "id": "xWauegxfG7", 24 | "key": "deep.nested.sum", 25 | "value": "sum(numbers)" 26 | }, 27 | { 28 | "id": "qGAHmak0xj", 29 | "key": "fullName", 30 | "value": "firstName + ' ' + lastName" 31 | }, 32 | { 33 | "id": "5ZnYGPFT-N", 34 | "key": "largeNumbers", 35 | "value": "filter(numbers, # >= 10)" 36 | }, 37 | { 38 | "id": "pSg-vIQR5Q", 39 | "key": "smallNumbers", 40 | "value": "filter(numbers, # < 10)" 41 | } 42 | ] 43 | } 44 | }, 45 | { 46 | "id": "db8797b1-bcc1-4fbf-a5d8-e7d43a181d5e", 47 | "type": "outputNode", 48 | "position": { 49 | "x": 780, 50 | "y": 240 51 | }, 52 | "name": "Response" 53 | } 54 | ], 55 | "edges": [ 56 | { 57 | "id": "05740fa7-3755-4756-b85e-bc1af2f6773b", 58 | "sourceId": "115975ef-2f43-4e22-b553-0da6f4cc7f68", 59 | "type": "edge", 60 | "targetId": "138b3b11-ff46-450f-9704-3f3c712067b2" 61 | }, 62 | { 63 | "id": "5d89c1d6-e894-4e8a-bd13-22368c2a6bc7", 64 | "sourceId": "138b3b11-ff46-450f-9704-3f3c712067b2", 65 | "type": "edge", 66 | "targetId": "db8797b1-bcc1-4fbf-a5d8-e7d43a181d5e" 67 | } 68 | ] 69 | } -------------------------------------------------------------------------------- /core/expression/src/compiler/opcode.rs: -------------------------------------------------------------------------------- 1 | use crate::functions::{FunctionKind, MethodKind}; 2 | use crate::lexer::Bracket; 3 | use rust_decimal::Decimal; 4 | use std::sync::Arc; 5 | use strum_macros::Display; 6 | 7 | #[derive(Debug, Clone, PartialEq, Eq)] 8 | pub enum FetchFastTarget { 9 | Root, 10 | Begin, 11 | String(Arc), 12 | Number(u32), 13 | } 14 | 15 | /// Machine code interpreted by VM 16 | #[derive(Debug, PartialEq, Eq, Clone, Display)] 17 | pub enum Opcode { 18 | PushNull, 19 | PushBool(bool), 20 | PushString(Arc), 21 | PushNumber(Decimal), 22 | Pop, 23 | Flatten, 24 | Join, 25 | Fetch, 26 | FetchRootEnv, 27 | FetchEnv(Arc), 28 | FetchFast(Vec), 29 | Negate, 30 | Not, 31 | Equal, 32 | Jump(Jump, u32), 33 | In, 34 | Compare(Compare), 35 | Add, 36 | Subtract, 37 | Multiply, 38 | Divide, 39 | Modulo, 40 | Exponent, 41 | Slice, 42 | Array, 43 | Object, 44 | AssignedObjectBegin, 45 | AssignedObjectStep, 46 | AssignedObjectEnd { 47 | with_return: bool, 48 | }, 49 | Len, 50 | IncrementIt, 51 | IncrementCount, 52 | GetCount, 53 | GetLen, 54 | Pointer, 55 | Begin, 56 | End, 57 | CallFunction { 58 | kind: FunctionKind, 59 | arg_count: u32, 60 | }, 61 | CallMethod { 62 | kind: MethodKind, 63 | arg_count: u32, 64 | }, 65 | Interval { 66 | left_bracket: Bracket, 67 | right_bracket: Bracket, 68 | }, 69 | } 70 | 71 | #[derive(Debug, PartialEq, Eq, Clone, Copy, Display)] 72 | pub enum Jump { 73 | Forward, 74 | Backward, 75 | IfTrue, 76 | IfFalse, 77 | IfNotNull, 78 | IfEnd, 79 | } 80 | 81 | #[derive(Debug, PartialEq, Eq, Clone, Copy, Display)] 82 | pub enum Compare { 83 | More, 84 | Less, 85 | MoreOrEqual, 86 | LessOrEqual, 87 | } 88 | -------------------------------------------------------------------------------- /core/template/src/lexer.rs: -------------------------------------------------------------------------------- 1 | use std::iter::{Enumerate, Peekable}; 2 | use std::str::Chars; 3 | 4 | #[derive(Debug, PartialOrd, PartialEq)] 5 | pub(crate) enum Token<'source> { 6 | Text(&'source str), 7 | OpenBracket, 8 | CloseBracket, 9 | } 10 | 11 | pub(crate) struct Lexer<'source> { 12 | cursor: Peekable>>, 13 | source: &'source str, 14 | tokens: Vec>, 15 | text_start: Option, 16 | } 17 | 18 | impl<'source, T> From for Lexer<'source> 19 | where 20 | T: Into<&'source str>, 21 | { 22 | fn from(value: T) -> Self { 23 | let source: &'source str = value.into(); 24 | 25 | Self { 26 | source, 27 | cursor: source.chars().enumerate().peekable(), 28 | tokens: Default::default(), 29 | text_start: None, 30 | } 31 | } 32 | } 33 | 34 | impl<'source> Lexer<'source> { 35 | pub fn collect(mut self) -> Vec> { 36 | while let Some((index, char)) = self.cursor.next() { 37 | if char == '{' && matches!(self.cursor.peek(), Some((_, '{'))) { 38 | self.flush(index); 39 | 40 | self.cursor.next(); 41 | self.tokens.push(Token::OpenBracket); 42 | } else if char == '}' && matches!(self.cursor.peek(), Some((_, '}'))) { 43 | self.flush(index); 44 | 45 | self.cursor.next(); 46 | self.tokens.push(Token::CloseBracket); 47 | } else { 48 | self.text_start.get_or_insert(index); 49 | } 50 | } 51 | 52 | self.flush(self.source.len()); 53 | self.tokens 54 | } 55 | 56 | fn flush(&mut self, index: usize) { 57 | if let Some(start) = self.text_start { 58 | self.tokens.push(Token::Text(&self.source[start..index])); 59 | self.text_start = None; 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /core/expression/src/functions/registry.rs: -------------------------------------------------------------------------------- 1 | use crate::functions::defs::FunctionDefinition; 2 | use crate::functions::{DeprecatedFunction, FunctionKind, InternalFunction}; 3 | use nohash_hasher::{BuildNoHashHasher, IsEnabled}; 4 | use std::cell::RefCell; 5 | use std::collections::HashMap; 6 | use std::rc::Rc; 7 | use strum::IntoEnumIterator; 8 | 9 | impl IsEnabled for InternalFunction {} 10 | impl IsEnabled for DeprecatedFunction {} 11 | 12 | pub struct FunctionRegistry { 13 | internal_functions: 14 | HashMap, BuildNoHashHasher>, 15 | deprecated_functions: HashMap< 16 | DeprecatedFunction, 17 | Rc, 18 | BuildNoHashHasher, 19 | >, 20 | } 21 | 22 | impl FunctionRegistry { 23 | thread_local!( 24 | static INSTANCE: RefCell = RefCell::new(FunctionRegistry::new_internal()) 25 | ); 26 | 27 | pub fn get_definition(kind: &FunctionKind) -> Option> { 28 | match kind { 29 | FunctionKind::Internal(internal) => { 30 | Self::INSTANCE.with_borrow(|i| i.internal_functions.get(&internal).cloned()) 31 | } 32 | FunctionKind::Deprecated(deprecated) => { 33 | Self::INSTANCE.with_borrow(|i| i.deprecated_functions.get(&deprecated).cloned()) 34 | } 35 | FunctionKind::Closure(_) => None, 36 | } 37 | } 38 | 39 | fn new_internal() -> Self { 40 | let internal_functions = InternalFunction::iter() 41 | .map(|i| (i.clone(), (&i).into())) 42 | .collect(); 43 | 44 | let deprecated_functions = DeprecatedFunction::iter() 45 | .map(|i| (i.clone(), (&i).into())) 46 | .collect(); 47 | 48 | Self { 49 | internal_functions, 50 | deprecated_functions, 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /core/expression/src/functions/mod.rs: -------------------------------------------------------------------------------- 1 | pub use crate::functions::date_method::DateMethod; 2 | pub use crate::functions::defs::FunctionTypecheck; 3 | pub use crate::functions::deprecated::DeprecatedFunction; 4 | pub use crate::functions::internal::InternalFunction; 5 | pub use crate::functions::method::{MethodKind, MethodRegistry}; 6 | pub use crate::functions::registry::FunctionRegistry; 7 | 8 | use std::fmt::Display; 9 | use strum_macros::{Display, EnumIter, EnumString, IntoStaticStr}; 10 | 11 | pub(crate) mod arguments; 12 | mod date_method; 13 | pub(crate) mod defs; 14 | mod deprecated; 15 | pub(crate) mod internal; 16 | mod method; 17 | pub(crate) mod registry; 18 | 19 | #[derive(Debug, PartialEq, Eq, Clone)] 20 | pub enum FunctionKind { 21 | Internal(InternalFunction), 22 | Deprecated(DeprecatedFunction), 23 | Closure(ClosureFunction), 24 | } 25 | 26 | impl TryFrom<&str> for FunctionKind { 27 | type Error = strum::ParseError; 28 | 29 | fn try_from(value: &str) -> Result { 30 | InternalFunction::try_from(value) 31 | .map(FunctionKind::Internal) 32 | .or_else(|_| DeprecatedFunction::try_from(value).map(FunctionKind::Deprecated)) 33 | .or_else(|_| ClosureFunction::try_from(value).map(FunctionKind::Closure)) 34 | } 35 | } 36 | 37 | impl Display for FunctionKind { 38 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 39 | match self { 40 | FunctionKind::Internal(i) => write!(f, "{i}"), 41 | FunctionKind::Deprecated(d) => write!(f, "{d}"), 42 | FunctionKind::Closure(c) => write!(f, "{c}"), 43 | } 44 | } 45 | } 46 | 47 | #[derive(Debug, PartialEq, Eq, Hash, Display, EnumString, EnumIter, IntoStaticStr, Clone, Copy)] 48 | #[strum(serialize_all = "camelCase")] 49 | pub enum ClosureFunction { 50 | All, 51 | None, 52 | Some, 53 | One, 54 | Filter, 55 | Map, 56 | FlatMap, 57 | Count, 58 | } 59 | -------------------------------------------------------------------------------- /test-data/table.json: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": [ 3 | { 4 | "id": "3e3f5093-c969-4c3a-97e1-560e4b769a12", 5 | "type": "inputNode", 6 | "position": { 7 | "x": 150, 8 | "y": 210 9 | }, 10 | "name": "Request" 11 | }, 12 | { 13 | "id": "0624d5fd-1944-4781-92bb-e32873ce91e2", 14 | "type": "decisionTableNode", 15 | "position": { 16 | "x": 410, 17 | "y": 210 18 | }, 19 | "name": "Hello", 20 | "content": { 21 | "hitPolicy": "first", 22 | "inputs": [ 23 | { 24 | "field": "input", 25 | "id": "xWauegxfG7", 26 | "name": "Input", 27 | "type": "expression" 28 | } 29 | ], 30 | "outputs": [ 31 | { 32 | "field": "output", 33 | "id": "qGAHmak0xj", 34 | "name": "Output", 35 | "type": "expression" 36 | } 37 | ], 38 | "rules": [ 39 | { 40 | "_id": "5ZnYGPFT-N", 41 | "xWauegxfG7": "> 10", 42 | "qGAHmak0xj": "10" 43 | }, 44 | { 45 | "_id": "pSg-vIQR5Q", 46 | "xWauegxfG7": "", 47 | "qGAHmak0xj": "0" 48 | } 49 | ] 50 | } 51 | }, 52 | { 53 | "id": "e0438c6b-dee0-405e-a941-9b4c3d9c4b83", 54 | "type": "outputNode", 55 | "position": { 56 | "x": 660, 57 | "y": 210 58 | }, 59 | "name": "Response" 60 | } 61 | ], 62 | "edges": [ 63 | { 64 | "id": "c30b9bfd-2da6-445f-a31a-31eeb4bfa803", 65 | "sourceId": "3e3f5093-c969-4c3a-97e1-560e4b769a12", 66 | "type": "edge", 67 | "targetId": "0624d5fd-1944-4781-92bb-e32873ce91e2" 68 | }, 69 | { 70 | "id": "dbda85da-4c1d-4e0b-b4e7-9bb475bd00b9", 71 | "sourceId": "0624d5fd-1944-4781-92bb-e32873ce91e2", 72 | "type": "edge", 73 | "targetId": "e0438c6b-dee0-405e-a941-9b4c3d9c4b83" 74 | } 75 | ] 76 | } -------------------------------------------------------------------------------- /core/expression/src/vm/date/duration.rs: -------------------------------------------------------------------------------- 1 | use crate::vm::date::duration_parser::{DurationParseError, DurationParser}; 2 | use crate::vm::date::duration_unit::DurationUnit; 3 | use rust_decimal::prelude::{FromPrimitive, ToPrimitive}; 4 | use rust_decimal::Decimal; 5 | use std::ops::Neg; 6 | 7 | #[derive(Debug, Clone, Default)] 8 | pub(crate) struct Duration { 9 | pub seconds: i64, 10 | pub months: i32, 11 | pub years: i32, 12 | } 13 | 14 | impl Duration { 15 | pub fn parse(s: &str) -> Result { 16 | DurationParser { 17 | iter: s.chars(), 18 | src: s, 19 | duration: Duration::default(), 20 | } 21 | .parse() 22 | } 23 | 24 | pub fn from_unit(n: Decimal, unit: DurationUnit) -> Option { 25 | if let Some(secs) = unit.as_secs() { 26 | return Some(Self { 27 | seconds: n.checked_mul(Decimal::from_u64(secs)?)?.to_i64()?, 28 | ..Default::default() 29 | }); 30 | }; 31 | 32 | match unit { 33 | DurationUnit::Month => Some(Duration { 34 | months: n.to_i32()?, 35 | ..Default::default() 36 | }), 37 | DurationUnit::Quarter => Some(Duration { 38 | months: n.to_i32()? * 3, 39 | ..Default::default() 40 | }), 41 | DurationUnit::Year => Some(Duration { 42 | years: n.to_i32()?, 43 | ..Default::default() 44 | }), 45 | _ => None, 46 | } 47 | } 48 | 49 | pub fn negate(self) -> Self { 50 | Self { 51 | years: self.years.neg(), 52 | months: self.months.neg(), 53 | seconds: self.seconds.neg(), 54 | } 55 | } 56 | 57 | pub fn day() -> Self { 58 | Self { 59 | seconds: DurationUnit::Day.as_secs().unwrap_or_default() as i64, 60 | ..Default::default() 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /test-data/graphs/expression-default.json: -------------------------------------------------------------------------------- 1 | { 2 | "tests": [ 3 | { 4 | "input": { 5 | "a": 1, 6 | "b": 2, 7 | "extra": "This should not pass through" 8 | }, 9 | "output": { 10 | "sum": 3 11 | } 12 | }, 13 | { 14 | "input": { 15 | "a": 5, 16 | "b": 3, 17 | "sum": 100 18 | }, 19 | "output": { 20 | "sum": 8 21 | } 22 | }, 23 | { 24 | "input": { 25 | "a": 10, 26 | "b": 20, 27 | "c": 30, 28 | "d": 40 29 | }, 30 | "output": { 31 | "sum": 30 32 | } 33 | }, 34 | { 35 | "input": { 36 | "a": 7, 37 | "b": 3, 38 | "passThrough": "This should not appear in output" 39 | }, 40 | "output": { 41 | "sum": 10 42 | } 43 | }, 44 | { 45 | "input": { 46 | "a": -5, 47 | "b": 5, 48 | "negative": true 49 | }, 50 | "output": { 51 | "sum": 0 52 | } 53 | } 54 | ], 55 | "nodes": [ 56 | { 57 | "type": "inputNode", 58 | "id": "deced339-bace-452a-8db0-777f038bffe8", 59 | "name": "request", 60 | "position": { 61 | "x": 145, 62 | "y": 235 63 | } 64 | }, 65 | { 66 | "type": "expressionNode", 67 | "content": { 68 | "expressions": [ 69 | { 70 | "id": "d54641c2-5f24-4140-a9c4-8453542a622a", 71 | "key": "sum", 72 | "value": "a + b" 73 | } 74 | ], 75 | "passThrough": false 76 | }, 77 | "id": "6b9cfc7e-4776-4b3f-8a19-c2a4d7874770", 78 | "name": "expression1", 79 | "position": { 80 | "x": 475, 81 | "y": 235 82 | } 83 | } 84 | ], 85 | "edges": [ 86 | { 87 | "id": "639f3a3a-7545-4e98-aa79-7a9bd9644af1", 88 | "sourceId": "deced339-bace-452a-8db0-777f038bffe8", 89 | "type": "edge", 90 | "targetId": "6b9cfc7e-4776-4b3f-8a19-c2a4d7874770" 91 | } 92 | ] 93 | } -------------------------------------------------------------------------------- /core/engine/src/nodes/function/v2/error.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Display, Formatter}; 2 | 3 | use rquickjs::{CaughtError, Ctx, Error, Exception}; 4 | use thiserror::Error; 5 | 6 | pub type FunctionResult = Result; 7 | 8 | #[derive(Debug, Error)] 9 | pub enum FunctionError { 10 | Caught(String), 11 | Runtime(Error), 12 | } 13 | 14 | impl<'js> From> for FunctionError { 15 | fn from(value: CaughtError<'js>) -> Self { 16 | Self::Caught(value.to_string()) 17 | } 18 | } 19 | 20 | impl From for FunctionError { 21 | fn from(value: Error) -> Self { 22 | Self::Runtime(value) 23 | } 24 | } 25 | 26 | impl Display for FunctionError { 27 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 28 | match self { 29 | FunctionError::Caught(c) => f.write_str(c.as_str()), 30 | FunctionError::Runtime(rt) => rt.fmt(f), 31 | } 32 | } 33 | } 34 | 35 | pub trait ResultExt { 36 | #[allow(dead_code)] 37 | fn or_throw_msg(self, ctx: &Ctx, msg: &str) -> rquickjs::Result; 38 | fn or_throw(self, ctx: &Ctx) -> rquickjs::Result; 39 | } 40 | 41 | impl ResultExt for Result { 42 | fn or_throw_msg(self, ctx: &Ctx, msg: &str) -> rquickjs::Result { 43 | self.map_err(|_| { 44 | let mut message = String::with_capacity(100); 45 | message.push_str(msg); 46 | message.push_str("."); 47 | Exception::throw_message(ctx, &message) 48 | }) 49 | } 50 | 51 | fn or_throw(self, ctx: &Ctx) -> rquickjs::Result { 52 | self.map_err(|err| Exception::throw_message(ctx, &err.to_string())) 53 | } 54 | } 55 | 56 | impl ResultExt for Option { 57 | fn or_throw_msg(self, ctx: &Ctx, msg: &str) -> rquickjs::Result { 58 | self.ok_or_else(|| Exception::throw_message(ctx, msg)) 59 | } 60 | 61 | fn or_throw(self, ctx: &Ctx) -> rquickjs::Result { 62 | self.ok_or_else(|| Exception::throw_message(ctx, "Value is not present")) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /core/engine/src/nodes/function/v2/module/http/listener.rs: -------------------------------------------------------------------------------- 1 | use crate::nodes::function::http_handler::{DynamicHttpHandler, HttpHandlerRequest}; 2 | use crate::nodes::function::v2::error::{FunctionResult, ResultExt}; 3 | use crate::nodes::function::v2::listener::{RuntimeEvent, RuntimeListener}; 4 | use crate::nodes::function::v2::serde::rquickjs_conv; 5 | use rquickjs::prelude::{Async, Func}; 6 | use rquickjs::{CatchResultExt, Ctx}; 7 | use std::future::Future; 8 | use std::pin::Pin; 9 | 10 | pub(crate) struct HttpListener { 11 | pub http_handler: DynamicHttpHandler, 12 | } 13 | 14 | impl RuntimeListener for HttpListener { 15 | fn on_event<'js>( 16 | &self, 17 | ctx: Ctx<'js>, 18 | event: RuntimeEvent, 19 | ) -> Pin + 'js>> { 20 | let http_handler = self.http_handler.clone(); 21 | 22 | Box::pin(async move { 23 | if event != RuntimeEvent::Startup { 24 | return Ok(()); 25 | } 26 | 27 | let Some(http_handler) = http_handler.clone() else { 28 | return Ok(()); 29 | }; 30 | 31 | ctx.globals() 32 | .set( 33 | "__executeHttp", 34 | Func::from(Async(move |ctx: Ctx<'js>, request_obj: rquickjs::Value| { 35 | let http_handler = http_handler.clone(); 36 | let request_result = 37 | rquickjs_conv::from_value::(request_obj); 38 | 39 | async move { 40 | let request = request_result?; 41 | let response = http_handler.handle(request).await.or_throw(&ctx)?; 42 | let response_object = rquickjs_conv::to_value(ctx.clone(), response)?; 43 | rquickjs::Result::Ok(response_object) 44 | } 45 | })), 46 | ) 47 | .catch(&ctx)?; 48 | 49 | Ok(()) 50 | }) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /bindings/nodejs/src/decision.rs: -------------------------------------------------------------------------------- 1 | use crate::engine::ZenEvaluateOptions; 2 | use crate::mt::spawn_worker; 3 | use crate::safe_result::SafeResult; 4 | use napi::anyhow::anyhow; 5 | use napi_derive::napi; 6 | use serde_json::Value; 7 | use std::sync::Arc; 8 | use zen_engine::Decision; 9 | 10 | #[napi] 11 | pub struct ZenDecision(pub(crate) Arc); 12 | 13 | impl From for ZenDecision { 14 | fn from(value: Decision) -> Self { 15 | Self(value.into()) 16 | } 17 | } 18 | 19 | #[napi] 20 | impl ZenDecision { 21 | #[napi(constructor)] 22 | pub fn new() -> napi::Result { 23 | Err(anyhow!("Private constructor").into()) 24 | } 25 | 26 | #[napi(ts_return_type = "Promise")] 27 | pub async fn evaluate( 28 | &self, 29 | context: Value, 30 | opts: Option, 31 | ) -> napi::Result { 32 | let decision = self.0.clone(); 33 | let result = spawn_worker(move || { 34 | let options = opts.unwrap_or_default(); 35 | 36 | async move { 37 | decision 38 | .evaluate_serialized(context.into(), options.into()) 39 | .await 40 | } 41 | }) 42 | .await 43 | .map_err(|_| anyhow!("Hook timed out"))? 44 | .map_err(|e| anyhow!(e))?; 45 | 46 | Ok(result) 47 | } 48 | 49 | #[napi( 50 | ts_return_type = "Promise<{ success: true, data: ZenEngineResponse } | { success: false; error: any; }>" 51 | )] 52 | pub async fn safe_evaluate( 53 | &self, 54 | context: Value, 55 | opts: Option, 56 | ) -> SafeResult { 57 | self.evaluate(context, opts).await.into() 58 | } 59 | 60 | #[napi] 61 | pub fn validate(&self) -> napi::Result<()> { 62 | let decision = self.0.clone(); 63 | let result = decision 64 | .validate() 65 | .map_err(|e| anyhow!(serde_json::to_string(&e).unwrap_or_else(|_| e.to_string())))?; 66 | 67 | Ok(result) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /core/engine/src/nodes/function/v2/module/http/backend/callback.rs: -------------------------------------------------------------------------------- 1 | use super::{HttpBackend, HttpResponse}; 2 | use crate::nodes::function::v2::error::ResultExt; 3 | use crate::nodes::function::v2::module::http::{HttpMethod, HttpRequestConfig}; 4 | use crate::nodes::function::v2::serde::rquickjs_conv; 5 | use crate::nodes::http_handler::HttpHandlerRequest; 6 | use rquickjs::promise::MaybePromise; 7 | use rquickjs::{CatchResultExt, Ctx}; 8 | use std::future::Future; 9 | use std::pin::Pin; 10 | 11 | pub(crate) struct CallbackHttpBackend; 12 | 13 | impl HttpBackend for CallbackHttpBackend { 14 | fn execute_http<'js>( 15 | &self, 16 | ctx: Ctx<'js>, 17 | method: HttpMethod, 18 | url: String, 19 | config: HttpRequestConfig, 20 | ) -> Pin>> + 'js>> { 21 | Box::pin(async move { 22 | let execute_http_fn: rquickjs::Function = 23 | ctx.globals().get("__executeHttp").or_throw(&ctx)?; 24 | 25 | let http_request = HttpHandlerRequest { 26 | url, 27 | method: method.to_string(), 28 | body: config.data, 29 | headers: config.headers.into_iter().map(|(k, v)| (k, v.0)).collect(), 30 | params: config.params.into_iter().map(|(k, v)| (k, v.0)).collect(), 31 | auth: config 32 | .auth 33 | .map(serde_json::to_value) 34 | .transpose() 35 | .or_throw(&ctx)?, 36 | }; 37 | 38 | let http_request_js = rquickjs_conv::to_value(ctx.clone(), http_request)?; 39 | let response_promise: MaybePromise = execute_http_fn 40 | .call((http_request_js,)) 41 | .catch(&ctx) 42 | .or_throw(&ctx)?; 43 | 44 | let response: HttpResponse = response_promise 45 | .into_future() 46 | .await 47 | .catch(&ctx) 48 | .or_throw(&ctx)?; 49 | 50 | Ok(response) 51 | }) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /test-data/$nodes-child.json: -------------------------------------------------------------------------------- 1 | { 2 | "contentType": "application/vnd.gorules.decision", 3 | "nodes": [ 4 | { 5 | "type": "inputNode", 6 | "content": { 7 | "schema": "" 8 | }, 9 | "id": "adbcb4f4-015c-429f-a9b5-5e08e50470d8", 10 | "name": "request", 11 | "position": { 12 | "x": 80, 13 | "y": 230 14 | } 15 | }, 16 | { 17 | "type": "expressionNode", 18 | "content": { 19 | "expressions": [ 20 | { 21 | "id": "ffad6138-2b20-4439-b043-b0e835573178", 22 | "key": "expressionParentNodes", 23 | "value": "$nodes.request.$nodes" 24 | }, 25 | { 26 | "id": "ffad6138-2b20-4439-b043-b0e835573178", 27 | "key": "expressionRequest", 28 | "value": "$nodes.request" 29 | } 30 | ], 31 | "inputField": null, 32 | "outputPath": null, 33 | "executionMode": "single" 34 | }, 35 | "id": "ec43772d-7c5a-4d36-852b-cc18f470e2f5", 36 | "name": "expression1", 37 | "position": { 38 | "x": 430, 39 | "y": 230 40 | } 41 | }, 42 | { 43 | "type": "functionNode", 44 | "content": { 45 | "source": "import zen from 'zen';\n\n/** @type {Handler} **/\nexport const handler = async (input) => {\n return {\n functionParentNodes: input['$nodes'].request['$nodes']\n, functionRequest: input['$nodes'].request\n };\n};\n" 46 | }, 47 | "id": "91b84889-a9f7-4452-bea6-8610f6ba4084", 48 | "name": "function1", 49 | "position": { 50 | "x": 430, 51 | "y": 135 52 | } 53 | } 54 | ], 55 | "edges": [ 56 | { 57 | "id": "06c0b8f4-45a3-4422-b58a-27da004d0786", 58 | "sourceId": "adbcb4f4-015c-429f-a9b5-5e08e50470d8", 59 | "type": "edge", 60 | "targetId": "ec43772d-7c5a-4d36-852b-cc18f470e2f5" 61 | }, 62 | { 63 | "id": "5ad36d1a-a4a1-4f89-bec4-ed54175655d0", 64 | "sourceId": "adbcb4f4-015c-429f-a9b5-5e08e50470d8", 65 | "type": "edge", 66 | "targetId": "91b84889-a9f7-4452-bea6-8610f6ba4084" 67 | } 68 | ] 69 | } -------------------------------------------------------------------------------- /bindings/nodejs/src/expression.rs: -------------------------------------------------------------------------------- 1 | use napi::anyhow::anyhow; 2 | use napi_derive::napi; 3 | use serde_json::Value; 4 | 5 | #[napi] 6 | pub fn evaluate_expression_sync(expression: String, context: Option) -> napi::Result { 7 | let ctx = context.unwrap_or(Value::Null); 8 | 9 | Ok( 10 | zen_expression::evaluate_expression(expression.as_str(), ctx.into()) 11 | .map_err(|e| anyhow!(serde_json::to_string(&e).unwrap_or_else(|_| e.to_string())))? 12 | .to_value(), 13 | ) 14 | } 15 | 16 | #[allow(dead_code)] 17 | #[napi] 18 | pub fn evaluate_unary_expression_sync(expression: String, context: Value) -> napi::Result { 19 | Ok( 20 | zen_expression::evaluate_unary_expression(expression.as_str(), context.into()) 21 | .map_err(|e| anyhow!(serde_json::to_string(&e).unwrap_or_else(|_| e.to_string())))?, 22 | ) 23 | } 24 | 25 | #[allow(dead_code)] 26 | #[napi] 27 | pub fn render_template_sync(template: String, context: Value) -> napi::Result { 28 | Ok(zen_tmpl::render(template.as_str(), context.into()) 29 | .map_err(|e| anyhow!(serde_json::to_string(&e).unwrap_or_else(|_| e.to_string())))? 30 | .to_value()) 31 | } 32 | 33 | #[allow(dead_code)] 34 | #[napi] 35 | pub async fn evaluate_expression( 36 | expression: String, 37 | context: Option, 38 | ) -> napi::Result { 39 | napi::tokio::spawn(async move { evaluate_expression_sync(expression, context) }) 40 | .await 41 | .map_err(|_| anyhow!("Hook timed out"))? 42 | } 43 | 44 | #[allow(dead_code)] 45 | #[napi] 46 | pub async fn evaluate_unary_expression(expression: String, context: Value) -> napi::Result { 47 | napi::tokio::spawn(async move { evaluate_unary_expression_sync(expression, context) }) 48 | .await 49 | .map_err(|_| anyhow!("Hook timed out"))? 50 | } 51 | 52 | #[allow(dead_code)] 53 | #[napi] 54 | pub async fn render_template(template: String, context: Value) -> napi::Result { 55 | napi::tokio::spawn(async move { render_template_sync(template, context) }) 56 | .await 57 | .map_err(|_| anyhow!("Hook timed out"))? 58 | } 59 | -------------------------------------------------------------------------------- /bindings/uniffi/src/decision.rs: -------------------------------------------------------------------------------- 1 | use crate::engine::ZenEvaluateOptions; 2 | use crate::error::ZenError; 3 | use crate::types::{JsonBuffer, ZenEngineResponse}; 4 | use serde_json::Value; 5 | use std::sync::Arc; 6 | use tokio::runtime::Handle; 7 | use tokio::task; 8 | use zen_engine::Decision; 9 | 10 | #[derive(uniffi::Object)] 11 | pub struct ZenDecision { 12 | decision: Arc, 13 | } 14 | 15 | impl From for ZenDecision { 16 | fn from(value: Decision) -> Self { 17 | Self { 18 | decision: Arc::new(value), 19 | } 20 | } 21 | } 22 | 23 | #[uniffi::export(async_runtime = "tokio")] 24 | impl ZenDecision { 25 | pub async fn evaluate( 26 | &self, 27 | context: JsonBuffer, 28 | options: Option, 29 | ) -> Result { 30 | let options = options.unwrap_or_default(); 31 | let context: Value = context.try_into()?; 32 | 33 | let decision = self.decision.clone(); 34 | 35 | // Use spawn_blocking to run the non-Send code synchronously 36 | let response = task::spawn_blocking(move || { 37 | // The blocking code that uses non-Send types 38 | Handle::current().block_on(async move { 39 | decision 40 | .evaluate_with_opts(context.into(), options.into()) 41 | .await 42 | .map(|response| ZenEngineResponse::try_from(response)) 43 | .map_err(|err| { 44 | ZenError::EvaluationError( 45 | serde_json::to_string(&err.as_ref()) 46 | .unwrap_or_else(|_| err.to_string()), 47 | ) 48 | }) 49 | }) 50 | }) 51 | .await 52 | .map_err(|e| ZenError::EvaluationError(format!("Task failed: {:?}", e)))???; 53 | 54 | Ok(response) 55 | } 56 | 57 | pub fn validate(&self) -> Result<(), ZenError> { 58 | self.decision.validate().map_err(|e| { 59 | ZenError::ValidationError(serde_json::to_string(&e).unwrap_or_else(|_| e.to_string())) 60 | }) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /core/engine/src/nodes/custom/adapter.rs: -------------------------------------------------------------------------------- 1 | use crate::nodes::result::{NodeError, NodeResult}; 2 | use json_dotpath::DotPaths; 3 | use serde::Serialize; 4 | use serde_json::Value; 5 | use std::fmt::Debug; 6 | use std::future::Future; 7 | use std::pin::Pin; 8 | use std::sync::Arc; 9 | use zen_expression::variable::Variable; 10 | use zen_tmpl::TemplateRenderError; 11 | 12 | pub trait CustomNodeAdapter: Debug + Send { 13 | fn handle(&self, request: CustomNodeRequest) -> Pin + '_>>; 14 | } 15 | 16 | #[derive(Default, Debug)] 17 | pub struct NoopCustomNode; 18 | 19 | impl CustomNodeAdapter for NoopCustomNode { 20 | fn handle(&self, request: CustomNodeRequest) -> Pin>> { 21 | Box::pin(async move { 22 | Err(NodeError { 23 | trace: None, 24 | node_id: request.node.id.clone(), 25 | source: "Custom node handler not provided".to_string().into(), 26 | }) 27 | }) 28 | } 29 | } 30 | 31 | #[derive(Serialize)] 32 | #[serde(rename_all = "camelCase")] 33 | pub struct CustomNodeRequest { 34 | pub input: Variable, 35 | pub node: CustomDecisionNode, 36 | } 37 | 38 | impl CustomNodeRequest { 39 | pub fn get_field(&self, path: &str) -> Result, TemplateRenderError> { 40 | let Some(selected_value) = self.get_field_raw(path) else { 41 | return Ok(None); 42 | }; 43 | 44 | let Variable::String(template) = selected_value else { 45 | return Ok(Some(selected_value)); 46 | }; 47 | 48 | let template_value = zen_tmpl::render(template.as_ref(), self.input.clone())?; 49 | Ok(Some(template_value)) 50 | } 51 | 52 | fn get_field_raw(&self, path: &str) -> Option { 53 | self.node.config.dot_get(path).ok().flatten() 54 | } 55 | } 56 | 57 | #[derive(Serialize, Clone)] 58 | #[serde(rename_all = "camelCase")] 59 | pub struct CustomDecisionNode { 60 | pub id: Arc, 61 | pub name: Arc, 62 | pub kind: Arc, 63 | pub config: Arc, 64 | } 65 | 66 | pub type DynamicCustomNode = Arc; 67 | -------------------------------------------------------------------------------- /core/engine/src/nodes/function/v1/script.rs: -------------------------------------------------------------------------------- 1 | use crate::nodes::function::v2::serde::JsValue; 2 | use anyhow::Context as _; 3 | use rquickjs::{Context, Ctx, Error as QError, FromJs, Module, Runtime}; 4 | use serde::{Deserialize, Serialize}; 5 | use serde_json::Value; 6 | use std::fmt::Debug; 7 | use std::rc::Rc; 8 | use zen_expression::variable::Variable; 9 | 10 | #[derive(Debug, Deserialize)] 11 | #[serde(rename_all = "camelCase")] 12 | pub struct EvaluateResponse { 13 | pub output: Variable, 14 | pub log: Vec, 15 | } 16 | 17 | pub struct Script { 18 | runtime: Runtime, 19 | } 20 | 21 | impl Script { 22 | pub fn new(runtime: Runtime) -> Self { 23 | Self { runtime } 24 | } 25 | 26 | pub async fn call

(&mut self, source: &str, args: &P) -> anyhow::Result 27 | where 28 | P: Serialize, 29 | { 30 | let runtime = &self.runtime; 31 | let context = Context::full(&runtime).context("Failed to create context")?; 32 | 33 | let args_str = 34 | serde_json::to_string(args).context("Failed to serialize function arguments")?; 35 | 36 | let json_response = context.with(|ctx| -> anyhow::Result { 37 | Module::evaluate( 38 | ctx.clone(), 39 | "main", 40 | "import 'internals'; globalThis.now = Date.now();", 41 | ) 42 | .unwrap() 43 | .finish::<()>() 44 | .unwrap(); 45 | 46 | let _ = ctx 47 | .globals() 48 | .set("log", Vec::::new()) 49 | .map_err(|e| map_js_error(&ctx, e))?; 50 | 51 | ctx.eval::(format!("{source};main({args_str})")) 52 | .map_err(|e| map_js_error(&ctx, e)) 53 | })?; 54 | 55 | serde_json::from_str(json_response.as_str()).context("Failed to parse function result") 56 | } 57 | } 58 | 59 | fn map_js_error(ctx: &Ctx, e: QError) -> anyhow::Error { 60 | let error = JsValue::from_js(&ctx, ctx.catch()) 61 | .map(|v| v.0) 62 | .unwrap_or(Variable::String(Rc::from(e.to_string().as_str()))); 63 | 64 | anyhow::Error::msg(error.to_string()) 65 | } 66 | -------------------------------------------------------------------------------- /test-data/graphs/expression-fields.json: -------------------------------------------------------------------------------- 1 | { 2 | "tests": [ 3 | { 4 | "input": { 5 | "customer": { 6 | "firstName": "John", 7 | "lastName": "Doe", 8 | "age": 30 9 | }, 10 | "order": { 11 | "id": "ORD-001", 12 | "total": 100 13 | } 14 | }, 15 | "output": { 16 | "customer": { 17 | "firstName": "John", 18 | "lastName": "Doe", 19 | "age": 30, 20 | "fullName": "John Doe" 21 | }, 22 | "order": { 23 | "id": "ORD-001", 24 | "total": 100 25 | } 26 | } 27 | }, 28 | { 29 | "input": { 30 | "customer": { 31 | "firstName": "Jane", 32 | "lastName": "Smith", 33 | "age": 25 34 | }, 35 | "order": { 36 | "id": "ORD-002", 37 | "total": 150 38 | } 39 | }, 40 | "output": { 41 | "customer": { 42 | "firstName": "Jane", 43 | "lastName": "Smith", 44 | "age": 25, 45 | "fullName": "Jane Smith" 46 | }, 47 | "order": { 48 | "id": "ORD-002", 49 | "total": 150 50 | } 51 | } 52 | } 53 | ], 54 | "nodes": [ 55 | { 56 | "type": "inputNode", 57 | "id": "input-node", 58 | "name": "request", 59 | "position": { 60 | "x": 100, 61 | "y": 100 62 | } 63 | }, 64 | { 65 | "type": "expressionNode", 66 | "id": "expression-node-1", 67 | "name": "customerFullName", 68 | "position": { 69 | "x": 300, 70 | "y": 100 71 | }, 72 | "content": { 73 | "inputField": "customer", 74 | "outputPath": "customer", 75 | "expressions": [ 76 | { 77 | "id": "07795ded-cb9b-4165-9b5e-783b066dda61", 78 | "key": "fullName", 79 | "value": "`${firstName} ${lastName}`" 80 | } 81 | ], 82 | "passThrough": true 83 | } 84 | } 85 | ], 86 | "edges": [ 87 | { 88 | "id": "edge-1", 89 | "sourceId": "input-node", 90 | "targetId": "expression-node-1", 91 | "type": "edge" 92 | } 93 | ] 94 | } -------------------------------------------------------------------------------- /bindings/c/src/custom_node.rs: -------------------------------------------------------------------------------- 1 | use crate::languages::native::NativeCustomNode; 2 | use anyhow::anyhow; 3 | use std::ffi::{c_char, CString}; 4 | use std::future::Future; 5 | use std::pin::Pin; 6 | use zen_engine::nodes::custom::{CustomNodeAdapter, CustomNodeRequest, NoopCustomNode}; 7 | use zen_engine::nodes::{NodeResponse, NodeResult}; 8 | 9 | #[derive(Debug)] 10 | pub(crate) enum DynamicCustomNode { 11 | Noop(NoopCustomNode), 12 | Native(NativeCustomNode), 13 | #[cfg(feature = "go")] 14 | Go(crate::languages::go::GoCustomNode), 15 | } 16 | 17 | impl Default for DynamicCustomNode { 18 | fn default() -> Self { 19 | Self::Noop(Default::default()) 20 | } 21 | } 22 | 23 | impl CustomNodeAdapter for DynamicCustomNode { 24 | fn handle(&self, request: CustomNodeRequest) -> Pin + '_>> { 25 | Box::pin(async move { 26 | match self { 27 | DynamicCustomNode::Noop(cn) => cn.handle(request).await, 28 | DynamicCustomNode::Native(cn) => cn.handle(request).await, 29 | #[cfg(feature = "go")] 30 | DynamicCustomNode::Go(cn) => cn.handle(request).await, 31 | } 32 | }) 33 | } 34 | } 35 | 36 | #[repr(C)] 37 | pub struct ZenCustomNodeResult { 38 | content: *mut c_char, 39 | error: *mut c_char, 40 | } 41 | 42 | impl ZenCustomNodeResult { 43 | pub fn into_node_result(self) -> anyhow::Result { 44 | let maybe_error = match self.error.is_null() { 45 | false => Some(unsafe { CString::from_raw(self.error) }), 46 | true => None, 47 | }; 48 | 49 | if let Some(c_error) = maybe_error { 50 | let maybe_str = c_error.to_str().unwrap_or("unknown error"); 51 | return Err(anyhow!("{maybe_str}")); 52 | } 53 | 54 | if self.content.is_null() { 55 | return Err(anyhow!("response not provided")); 56 | } 57 | 58 | let content_cstr = unsafe { CString::from_raw(self.content) }; 59 | let node_response: NodeResponse = serde_json::from_slice(content_cstr.to_bytes()) 60 | .map_err(|_| anyhow!("failed to deserialize"))?; 61 | 62 | Ok(node_response) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /core/engine/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["GoRules Team "] 3 | description = "Business rules engine" 4 | name = "zen-engine" 5 | license = "MIT" 6 | version = "0.52.2" 7 | edition = "2021" 8 | repository = "https://github.com/gorules/zen.git" 9 | 10 | [lib] 11 | doctest = false 12 | 13 | [dependencies] 14 | ahash = { workspace = true } 15 | anyhow = { workspace = true } 16 | thiserror = { workspace = true } 17 | petgraph = { workspace = true } 18 | serde_json = { workspace = true, features = ["arbitrary_precision"] } 19 | serde = { workspace = true, features = ["derive", "rc"] } 20 | strum = { workspace = true, features = ["derive"] } 21 | once_cell = { workspace = true } 22 | json_dotpath = { workspace = true } 23 | rust_decimal = { workspace = true, features = ["maths-nopanic"] } 24 | fixedbitset = "0.5" 25 | tokio = { workspace = true, features = ["sync", "time"] } 26 | rquickjs = { version = "0.10", features = ["macro", "loader", "rust-alloc", "futures", "either", "properties"] } 27 | zen-types = { path = "../types", version = "0.52.2" } 28 | zen-expression = { path = "../expression", version = "0.52.2" } 29 | zen-tmpl = { path = "../template", version = "0.52.2" } 30 | nohash-hasher = { workspace = true } 31 | downcast-rs = { version = "2.0", features = ["std", "sync"] } 32 | 33 | [dev-dependencies] 34 | tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } 35 | criterion = { workspace = true, features = ["async_tokio"] } 36 | insta = { version = "1.43", features = ["yaml", "redactions"] } 37 | 38 | [target.'cfg(not(target_family = "wasm"))'.dependencies] 39 | async-trait = { version = "0.1" } 40 | http = { version = "1.3" } 41 | reqwest = { version = "0.12", features = ["json", "rustls-tls"], default-features = false } 42 | reqsign = { version = "0.17", features = ["aws", "azure", "google", "default-context"] } 43 | sha2 = { version = "0.10" } 44 | jsonschema = { version = "0.33" } 45 | 46 | [target.'cfg(target_family = "wasm")'.dependencies] 47 | jsonschema = { version = "0.33", default-features = false } 48 | rquickjs = { version = "0.10", features = ["macro", "loader", "rust-alloc", "futures", "either", "properties", "bindgen"] } 49 | 50 | [[bench]] 51 | harness = false 52 | name = "engine" 53 | 54 | [features] 55 | bindgen = ["rquickjs/bindgen"] -------------------------------------------------------------------------------- /core/expression/src/vm/date/duration_unit.rs: -------------------------------------------------------------------------------- 1 | use crate::variable::VariableType; 2 | use std::rc::Rc; 3 | 4 | #[derive(Debug, Clone, Copy)] 5 | pub(crate) enum DurationUnit { 6 | Second, 7 | Minute, 8 | Hour, 9 | Day, 10 | Week, 11 | Month, 12 | Quarter, 13 | Year, 14 | } 15 | 16 | impl DurationUnit { 17 | pub fn variable_type() -> VariableType { 18 | VariableType::Enum( 19 | Some(Rc::from("DurationUnit")), 20 | vec![ 21 | "seconds", "second", "secs", "sec", "s", "minutes", "minute", "min", "mins", "m", 22 | "hours", "hour", "hr", "hrs", "h", "days", "day", "d", "weeks", "week", "w", 23 | "months", "month", "mo", "M", "quarters", "quarter", "qtr", "q", "years", "year", 24 | "y", 25 | ] 26 | .into_iter() 27 | .map(Into::into) 28 | .collect(), 29 | ) 30 | } 31 | 32 | pub fn parse(unit: &str) -> Option { 33 | match unit { 34 | "seconds" | "second" | "secs" | "sec" | "s" => Some(Self::Second), 35 | "minutes" | "minute" | "min" | "mins" | "m" => Some(Self::Minute), 36 | "hours" | "hour" | "hr" | "hrs" | "h" => Some(Self::Hour), 37 | "days" | "day" | "d" => Some(Self::Day), 38 | "weeks" | "week" | "w" => Some(Self::Week), 39 | "months" | "month" | "mo" | "M" => Some(Self::Month), 40 | "quarters" | "quarter" | "qtr" | "q" => Some(Self::Quarter), 41 | "years" | "year" | "y" => Some(Self::Year), 42 | _ => None, 43 | } 44 | } 45 | 46 | pub fn as_secs(&self) -> Option { 47 | match self { 48 | DurationUnit::Second => Some(1), 49 | DurationUnit::Minute => Some(60), 50 | DurationUnit::Hour => Some(3600), 51 | DurationUnit::Day => Some(86_400), 52 | DurationUnit::Week => Some(86_400 * 7), 53 | // Calendar units 54 | DurationUnit::Quarter => None, 55 | DurationUnit::Month => None, 56 | DurationUnit::Year => None, 57 | } 58 | } 59 | 60 | pub fn as_millis(&self) -> Option { 61 | self.as_secs().map(|s| s as f64 * 1000_f64) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /core/engine/tests/snapshots/engine__multi-switch_0.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: core/engine/tests/engine.rs 3 | expression: serialized_result 4 | --- 5 | performance: "[perf]" 6 | result: 7 | flag: 8 | turnover: red 9 | trace: 10 | 2ee16c8c-fb12-4f20-9813-67bad6f4eb14: 11 | id: 2ee16c8c-fb12-4f20-9813-67bad6f4eb14 12 | input: 13 | company: 14 | type: LLC 15 | name: Model Turnover LLC 16 | order: 17 | "$serde_json::private::Number": "3" 18 | output: 19 | flag: 20 | turnover: red 21 | performance: "[perf]" 22 | traceData: 23 | index: 24 | "$serde_json::private::Number": "2" 25 | reference_map: 26 | company.turnover: ~ 27 | rule: 28 | _id: 952300fd-e4d9-4301-8a5a-4eda1d01d8ee 29 | "company.turnover[fa0fd31a-8865-43fb-8a60-b729c640140a]": "" 30 | 84b0e11b-8c9d-46f3-ac34-f674f3b98068: 31 | id: 84b0e11b-8c9d-46f3-ac34-f674f3b98068 32 | input: 33 | flag: 34 | turnover: red 35 | name: Response 36 | order: 37 | "$serde_json::private::Number": "4" 38 | output: ~ 39 | performance: "[perf]" 40 | traceData: ~ 41 | dc7b8739-e234-4363-afe9-df156f082f6f: 42 | id: dc7b8739-e234-4363-afe9-df156f082f6f 43 | input: 44 | company: 45 | type: LLC 46 | name: switchNode 1 47 | order: 48 | "$serde_json::private::Number": "2" 49 | output: 50 | company: 51 | type: LLC 52 | performance: "[perf]" 53 | traceData: 54 | statements: 55 | - id: 931eda5b-a780-428b-9a0a-e3eb6283bab4 56 | de6cc00d-ef1b-46f5-9beb-9285d468c39d: 57 | id: de6cc00d-ef1b-46f5-9beb-9285d468c39d 58 | input: 59 | company: 60 | type: LLC 61 | name: switchNode 1 62 | order: 63 | "$serde_json::private::Number": "1" 64 | output: 65 | company: 66 | type: LLC 67 | performance: "[perf]" 68 | traceData: 69 | statements: 70 | - id: 6499e0bb-2cda-4a5f-9246-d48e7d2177fb 71 | fecde070-38cf-4656-81d7-3a2cb6e38f8f: 72 | id: fecde070-38cf-4656-81d7-3a2cb6e38f8f 73 | input: ~ 74 | name: Request 75 | order: 76 | "$serde_json::private::Number": "0" 77 | output: 78 | company: 79 | type: LLC 80 | performance: "[perf]" 81 | traceData: ~ 82 | -------------------------------------------------------------------------------- /test-data/graphs/expression-loop.json: -------------------------------------------------------------------------------- 1 | { 2 | "tests": [ 3 | { 4 | "input": { 5 | "cart": { 6 | "items": [ 7 | { 8 | "name": "Apple", 9 | "price": 0.5, 10 | "quantity": 3 11 | }, 12 | { 13 | "name": "Banana", 14 | "price": 0.3, 15 | "quantity": 5 16 | }, 17 | { 18 | "name": "Orange", 19 | "price": 0.7, 20 | "quantity": 2 21 | } 22 | ], 23 | "customerId": "CUST-001" 24 | } 25 | }, 26 | "output": { 27 | "cart": { 28 | "items": [ 29 | { 30 | "name": "Apple", 31 | "price": 0.5, 32 | "quantity": 3, 33 | "totalPrice": 1.5 34 | }, 35 | { 36 | "name": "Banana", 37 | "price": 0.3, 38 | "quantity": 5, 39 | "totalPrice": 1.5 40 | }, 41 | { 42 | "name": "Orange", 43 | "price": 0.7, 44 | "quantity": 2, 45 | "totalPrice": 1.4 46 | } 47 | ], 48 | "customerId": "CUST-001" 49 | } 50 | } 51 | } 52 | ], 53 | "nodes": [ 54 | { 55 | "type": "inputNode", 56 | "id": "input-node", 57 | "name": "request", 58 | "position": { 59 | "x": 100, 60 | "y": 100 61 | } 62 | }, 63 | { 64 | "type": "expressionNode", 65 | "id": "expression-node-1", 66 | "name": "calculateItemTotal", 67 | "position": { 68 | "x": 300, 69 | "y": 100 70 | }, 71 | "content": { 72 | "inputField": "cart.items", 73 | "outputPath": "cart.items", 74 | "executionMode": "loop", 75 | "expressions": [ 76 | { 77 | "id": "total-price-exp", 78 | "key": "totalPrice", 79 | "value": "price * quantity" 80 | } 81 | ], 82 | "passThrough": true 83 | } 84 | } 85 | ], 86 | "edges": [ 87 | { 88 | "id": "edge-1", 89 | "sourceId": "input-node", 90 | "targetId": "expression-node-1", 91 | "type": "edge" 92 | } 93 | ] 94 | } -------------------------------------------------------------------------------- /core/engine/src/nodes/function/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod http_handler; 2 | pub(crate) mod v1; 3 | pub(crate) mod v2; 4 | 5 | use crate::nodes::definition::NodeHandler; 6 | use crate::nodes::function::v1::{FunctionV1NodeHandler, FunctionV1Trace}; 7 | use crate::nodes::function::v2::{FunctionV2NodeHandler, FunctionV2Trace}; 8 | use crate::nodes::result::NodeResult; 9 | use crate::nodes::NodeContext; 10 | use std::sync::Arc; 11 | use zen_types::decision::{FunctionContent, FunctionNodeContent}; 12 | use zen_types::variable::Variable; 13 | 14 | #[derive(Debug, Clone)] 15 | pub struct FunctionNodeHandler; 16 | 17 | pub type FunctionNodeData = FunctionNodeContent; 18 | 19 | pub type FunctionNodeTrace = Variable; 20 | 21 | impl NodeHandler for FunctionNodeHandler { 22 | type NodeData = FunctionNodeData; 23 | type TraceData = FunctionNodeTrace; 24 | 25 | async fn handle(&self, ctx: NodeContext) -> NodeResult { 26 | match &ctx.node { 27 | FunctionNodeContent::Version1(source) => { 28 | let v1_context = NodeContext::, FunctionV1Trace> { 29 | id: ctx.id.clone(), 30 | name: ctx.name.clone(), 31 | input: ctx.input.clone(), 32 | extensions: ctx.extensions.clone(), 33 | trace: ctx.config.trace.then(|| Default::default()), 34 | iteration: ctx.iteration, 35 | config: ctx.config, 36 | node: source.clone(), 37 | }; 38 | 39 | FunctionV1NodeHandler.handle(v1_context).await 40 | } 41 | FunctionNodeContent::Version2(content) => { 42 | let v2_context = NodeContext:: { 43 | id: ctx.id.clone(), 44 | name: ctx.name.clone(), 45 | input: ctx.input.clone(), 46 | extensions: ctx.extensions.clone(), 47 | trace: ctx.config.trace.then(|| Default::default()), 48 | iteration: ctx.iteration, 49 | config: ctx.config, 50 | node: content.clone(), 51 | }; 52 | 53 | FunctionV2NodeHandler.handle(v2_context).await 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /core/expression_repl/src/main.rs: -------------------------------------------------------------------------------- 1 | use colored::Colorize; 2 | use rustyline::config::Configurer; 3 | use rustyline::{DefaultEditor, Result}; 4 | use serde_json::json; 5 | 6 | use zen_expression::{Isolate, Variable}; 7 | 8 | trait PrettyPrint { 9 | fn pretty_print(&self) -> String; 10 | } 11 | 12 | impl PrettyPrint for Variable { 13 | fn pretty_print(&self) -> String { 14 | match &self { 15 | Variable::Number(num) => format!("{}", num.normalize().to_string().yellow()), 16 | Variable::String(str) => format!("{}", format!("'{}'", str).green()), 17 | Variable::Bool(b) => format!("{}", b.to_string().yellow()), 18 | Variable::Null => format!("{}", "null".bold()), 19 | Variable::Array(a) => { 20 | let arr = a.borrow(); 21 | let elements = arr 22 | .iter() 23 | .map(|i| i.pretty_print()) 24 | .collect::>() 25 | .join(", "); 26 | format!("[{}]", elements) 27 | } 28 | Variable::Object(m) => { 29 | let map = m.borrow(); 30 | let elements = map 31 | .iter() 32 | .map(|(key, value)| format!("{}: {}", key, value.pretty_print())) 33 | .collect::>() 34 | .join(", "); 35 | 36 | format!("{{ {} }}", elements) 37 | } 38 | Variable::Dynamic(d) => d.to_string(), 39 | } 40 | } 41 | } 42 | 43 | fn main() -> Result<()> { 44 | let mut rl = DefaultEditor::new()?; 45 | rl.set_auto_add_history(true); 46 | 47 | loop { 48 | let readline = rl.readline("> "); 49 | let Ok(line) = readline else { 50 | break; 51 | }; 52 | 53 | let mut isolate = Isolate::new(); 54 | isolate.set_environment( 55 | json!({ "customer": { "firstName": "John", "lastName": "Doe", "age": 20 }, "hello": true, "$": 10 }).into(), 56 | ); 57 | let result = isolate.run_standard(line.as_str()); 58 | 59 | match result { 60 | Ok(res) => println!("{}", res.pretty_print()), 61 | Err(err) => println!("Error: {}", err.to_string().red()), 62 | }; 63 | } 64 | 65 | Ok(()) 66 | } 67 | -------------------------------------------------------------------------------- /core/template/src/parser.rs: -------------------------------------------------------------------------------- 1 | use crate::error::{ParserError, TemplateRenderError}; 2 | use crate::lexer::Token; 3 | use std::iter::Peekable; 4 | use std::slice::Iter; 5 | 6 | #[derive(Debug, PartialOrd, PartialEq)] 7 | pub(crate) enum Node<'a> { 8 | Text(&'a str), 9 | Expression(&'a str), 10 | } 11 | 12 | #[derive(Debug, PartialOrd, PartialEq)] 13 | enum ParserState { 14 | Text, 15 | Expression, 16 | } 17 | 18 | pub(crate) struct Parser<'source, 'tokens> { 19 | cursor: Peekable>>, 20 | state: ParserState, 21 | nodes: Vec>, 22 | } 23 | 24 | impl<'source, 'tokens, T> From for Parser<'source, 'tokens> 25 | where 26 | T: Into<&'tokens [Token<'source>]>, 27 | { 28 | fn from(value: T) -> Self { 29 | let tokens = value.into(); 30 | let cursor = tokens.iter().peekable(); 31 | 32 | Self { 33 | cursor, 34 | nodes: Default::default(), 35 | state: ParserState::Text, 36 | } 37 | } 38 | } 39 | 40 | impl<'source, 'tokens> Parser<'source, 'tokens> { 41 | pub(crate) fn collect(mut self) -> Result>, TemplateRenderError> { 42 | while let Some(token) = self.cursor.next() { 43 | match token { 44 | Token::Text(text) => self.text(text), 45 | Token::OpenBracket => self.open_bracket()?, 46 | Token::CloseBracket => self.close_bracket()?, 47 | } 48 | } 49 | 50 | Ok(self.nodes) 51 | } 52 | 53 | fn text(&mut self, data: &'source str) { 54 | match self.state { 55 | ParserState::Text => self.nodes.push(Node::Text(data)), 56 | ParserState::Expression => self.nodes.push(Node::Expression(data)), 57 | } 58 | } 59 | 60 | fn open_bracket(&mut self) -> Result<(), ParserError> { 61 | if self.state == ParserState::Expression { 62 | return Err(ParserError::OpenBracket); 63 | } 64 | 65 | self.state = ParserState::Expression; 66 | Ok(()) 67 | } 68 | 69 | fn close_bracket(&mut self) -> Result<(), ParserError> { 70 | if self.state != ParserState::Expression { 71 | return Err(ParserError::CloseBracket); 72 | } 73 | 74 | self.state = ParserState::Text; 75 | Ok(()) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /bindings/uniffi/src/loader.rs: -------------------------------------------------------------------------------- 1 | use crate::error::ZenError; 2 | use crate::types::JsonBuffer; 3 | use std::fmt::{Debug, Formatter}; 4 | use std::future::Future; 5 | use std::pin::Pin; 6 | use std::sync::Arc; 7 | use uniffi::deps::anyhow::anyhow; 8 | use zen_engine::loader::{DecisionLoader, LoaderError, LoaderResponse}; 9 | use zen_engine::model::DecisionContent; 10 | 11 | #[uniffi::export(callback_interface)] 12 | #[async_trait::async_trait] 13 | pub trait ZenDecisionLoaderCallback: Send + Sync { 14 | async fn load(&self, key: String) -> Result, ZenError>; 15 | } 16 | 17 | pub struct NoopDecisionLoader; 18 | 19 | #[async_trait::async_trait] 20 | impl ZenDecisionLoaderCallback for NoopDecisionLoader { 21 | async fn load(&self, _: String) -> Result, ZenError> { 22 | Err(ZenError::Zero) 23 | } 24 | } 25 | 26 | pub struct ZenDecisionLoaderCallbackWrapper(pub Box); 27 | 28 | impl Debug for ZenDecisionLoaderCallbackWrapper { 29 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 30 | write!(f, "ZenDecisionLoaderCallbackWrapper") 31 | } 32 | } 33 | 34 | impl DecisionLoader for ZenDecisionLoaderCallbackWrapper { 35 | fn load<'a>( 36 | &'a self, 37 | key: &'a str, 38 | ) -> Pin + 'a + Send>> { 39 | Box::pin(async move { 40 | let maybe_json_buffer = match self.0.load(key.into()).await { 41 | Ok(r) => r, 42 | Err(error) => { 43 | return Err(LoaderError::Internal { 44 | key: key.to_string(), 45 | source: anyhow!(error), 46 | }); 47 | } 48 | }; 49 | 50 | let Some(json_buffer) = maybe_json_buffer else { 51 | return Err(LoaderError::NotFound(key.to_string())); 52 | }; 53 | 54 | let decision_content: DecisionContent = 55 | serde_json::from_slice(json_buffer.0.as_slice()).map_err(|e| { 56 | LoaderError::Internal { 57 | key: key.to_string(), 58 | source: anyhow!(e), 59 | } 60 | })?; 61 | 62 | Ok(Arc::new(decision_content)) 63 | }) 64 | } 65 | } 66 | --------------------------------------------------------------------------------