From fe6d1bc47e27c586af597d5125f09830e0cd9d1c Mon Sep 17 00:00:00 2001 From: stefan-gorules <127550877+stefan-gorules@users.noreply.github.com> Date: Fri, 7 Apr 2023 02:54:43 +0200 Subject: [PATCH] refactor: flatten zen engine exports (#17) * refactor: flatten zen-engine exports; * fix: update bindings and readme * fix fmt * fix benchmark --- README.md | 6 +- bindings/nodejs/src/decision.rs | 3 +- bindings/nodejs/src/engine.rs | 4 +- bindings/nodejs/src/loader.rs | 2 +- bindings/python/src/decision.rs | 3 +- bindings/python/src/engine.rs | 5 +- bindings/python/src/loader.rs | 2 +- core/engine/README.md | 171 ++++++++++++++++-------- core/engine/benches/engine.rs | 4 +- core/engine/src/decision.rs | 5 +- core/engine/src/engine.rs | 11 +- core/engine/src/handler/decision.rs | 2 +- core/engine/src/handler/function/mod.rs | 2 +- core/engine/src/handler/node.rs | 2 +- core/engine/src/handler/table/zen.rs | 2 +- core/engine/src/handler/tree.rs | 4 +- core/engine/src/lib.rs | 20 +-- core/engine/src/loader/filesystem.rs | 2 +- core/engine/src/loader/memory.rs | 2 +- core/engine/src/loader/mod.rs | 15 ++- core/engine/src/model/decision.rs | 87 ------------ core/engine/src/model/mod.rs | 88 +++++++++++- core/parser/README.md | 165 ++++++++--------------- core/vm/README.md | 18 +-- 24 files changed, 316 insertions(+), 309 deletions(-) delete mode 100644 core/engine/src/model/decision.rs diff --git a/README.md b/README.md index 1a07cdf5..23ff547c 100644 --- a/README.md +++ b/README.md @@ -19,14 +19,14 @@ zen-engine = "0" ```rust use serde_json::json; -use zen_engine::engine::DecisionEngine; -use zen_engine::model::decision::DecisionContent; +use zen_engine::DecisionEngine; +use zen_engine::model::DecisionContent; async fn evaluate() { let decision_content: DecisionContent = serde_json::from_str(include_str!("jdm_graph.json")).unwrap(); let engine = DecisionEngine::default(); let decision = engine.create_decision(decision_content.into()); - + let result = decision.evaluate(&json!({ "input": 12 })).await; } ``` diff --git a/bindings/nodejs/src/decision.rs b/bindings/nodejs/src/decision.rs index 5a156d31..2a7a318f 100644 --- a/bindings/nodejs/src/decision.rs +++ b/bindings/nodejs/src/decision.rs @@ -5,8 +5,7 @@ use napi::tokio; use napi_derive::napi; use serde_json::Value; use std::sync::Arc; -use zen_engine::decision::Decision; -use zen_engine::engine::EvaluationOptions; +use zen_engine::{Decision, EvaluationOptions}; #[napi(js_name = "ZenDecision")] pub struct JsZenDecision(pub(crate) Arc>); diff --git a/bindings/nodejs/src/engine.rs b/bindings/nodejs/src/engine.rs index 4525bbed..91fc03d9 100644 --- a/bindings/nodejs/src/engine.rs +++ b/bindings/nodejs/src/engine.rs @@ -6,8 +6,8 @@ use napi::{tokio, JsFunction}; use napi_derive::napi; use serde_json::Value; use std::sync::Arc; -use zen_engine::engine::{DecisionEngine, EvaluationOptions}; -use zen_engine::model::decision::DecisionContent; +use zen_engine::model::DecisionContent; +use zen_engine::{DecisionEngine, EvaluationOptions}; #[napi(js_name = "ZenEngine")] pub struct JsZenEngine { diff --git a/bindings/nodejs/src/loader.rs b/bindings/nodejs/src/loader.rs index 7cff4002..284649bd 100644 --- a/bindings/nodejs/src/loader.rs +++ b/bindings/nodejs/src/loader.rs @@ -6,7 +6,7 @@ use napi::JsFunction; use std::sync::Arc; use zen_engine::loader::{DecisionLoader, LoaderError, LoaderResult}; -use zen_engine::model::decision::DecisionContent; +use zen_engine::model::DecisionContent; pub(crate) struct JsDecisionLoader { function: Option>>, diff --git a/bindings/python/src/decision.rs b/bindings/python/src/decision.rs index dd5b0188..a27bcea2 100644 --- a/bindings/python/src/decision.rs +++ b/bindings/python/src/decision.rs @@ -6,8 +6,7 @@ use pyo3::types::PyDict; use pyo3::{pyclass, pymethods, PyObject, PyResult, Python, ToPyObject}; use pythonize::depythonize; use std::sync::Arc; -use zen_engine::decision::Decision; -use zen_engine::engine::EvaluationOptions; +use zen_engine::{Decision, EvaluationOptions}; #[pyclass] #[pyo3(name = "ZenDecision")] diff --git a/bindings/python/src/engine.rs b/bindings/python/src/engine.rs index dd1d502a..168fa61c 100644 --- a/bindings/python/src/engine.rs +++ b/bindings/python/src/engine.rs @@ -7,9 +7,8 @@ use pyo3::{pyclass, pymethods, PyObject, PyResult, Python, ToPyObject}; use pythonize::depythonize; use serde::{Deserialize, Serialize}; use std::sync::Arc; -use zen_engine::engine::{DecisionEngine, EvaluationOptions}; - -use zen_engine::model::decision::DecisionContent; +use zen_engine::model::DecisionContent; +use zen_engine::{DecisionEngine, EvaluationOptions}; #[pyclass] #[pyo3(name = "ZenEngine")] diff --git a/bindings/python/src/loader.rs b/bindings/python/src/loader.rs index adab78b4..814e18e7 100644 --- a/bindings/python/src/loader.rs +++ b/bindings/python/src/loader.rs @@ -3,7 +3,7 @@ use async_trait::async_trait; use pyo3::{PyObject, Python}; use std::sync::Arc; use zen_engine::loader::{DecisionLoader, LoaderError, LoaderResult}; -use zen_engine::model::decision::DecisionContent; +use zen_engine::model::DecisionContent; #[derive(Default)] pub(crate) struct PyDecisionLoader(Option); diff --git a/core/engine/README.md b/core/engine/README.md index bf6b6091..b6d5606e 100644 --- a/core/engine/README.md +++ b/core/engine/README.md @@ -1,71 +1,130 @@ -# ZEN VM +# ZEN Engine -Parser for business-first expression language used in -Zen business rules engine by GoRules. The language is designed -to follow these principles: - -- Side-effect free -- Dynamic types -- Simple syntax for broad audiences - -It's primary objective is to bridge the gap between business analysts and engineers, -while providing outstanding performance and readability. +ZEN Engine is business friendly Open-Source Business Rules Engine +(BRE) to execute decision models according to the GoRules JSON +Decision Model (JDM) standard. It is written in Rust and provides +native bindings for NodeJS and Python. ZEN Engine allows to load +and execute JSON Decision Model (JDM) from JSON files. ## Resources [Documentation](https://gorules.io/docs/) -[Zen Language Playground](https://gorules.io/docs/rules-engine/expression-language#playground) +[Online Rules Engine Editor](https://editor.gorules.io/) -[Online Rules Editor](https://editor.gorules.io/) +## Installation -## Unary tests -Unary test is a comma-separated list of simple expressions which -evaluate to a boolean value. Each comma separation is treated as -or operator. Inside unary expressions, a special symbol is available -$ which refers to a current column. - -Some examples: -```js -// Given: $ = 1 -1, 2, 3 // true -1 // true ->= 1 // true -< 1 // false -[0..10] // true, (internally this is $ >= 0 and $ <= 10) -> 0 and < 10 // true - -// Given: $ = 'USD' -'GBP', 'USD' // true -'EUR' // false -startsWith($, "US") // true - defaults to expression mode, comma is unavailable -endsWith($, "US") // false - defaults to expression mode -lower($) == "usd" // true - defaults to expression mode +Add the following to your Cargo.toml file: +```toml +[dependencies] +zen-engine = "0" ``` -## Standard tests +## Usage +To execute a simple decision using a Noop (default) loader you can use the code below. -Expressions feature full capability syntax of ZEN language. -They give you access to all functions, and are most useful when -defining columns or outputs. Full syntax is also available in unary -expressions when $ is used (as it forces the expression mode). +```rust +use serde_json::json; +use zen_engine::DecisionEngine; +use zen_engine::model::DecisionContent; -```js -100 + 100 // 200 -10 * 5 // 50 -10 ^ 2 // 100 -1 in [1, 2, 3] // true -5 in (5..10] // false -sum([1, 2, 3]) // 6 -max([1, 2, 3]) // 3 +async fn evaluate() { + let decision_content: DecisionContent = serde_json::from_str(include_str!("jdm_graph.json")).unwrap(); + let engine = DecisionEngine::default(); + let decision = engine.create_decision(decision_content.into()); + + let result = decision.evaluate(&json!({ "input": 12 })).await; +} +``` -"hello" + " " + "world" // "hello world" -len("world") // 5 -weekdayString(date("2022-11-08")) // "Tue" -contains("hello world", "hello") // true -upper('john') // "JOHN" +Alternatively, you may create decision indirectly without constructing the engine utilising +`Decision::from` function. -some(['admin', 'user'], # == "admin") // true -not all([100, 200, 400, 800], # in (100..800)) // false -filter([100, 200, 400, 800], # >= 200) // [200, 400, 800] +## Loaders +For more advanced use cases where you want to load multiple decisions and utilise graphs you +may use one of the following pre-made loaders: +- FilesystemLoader - with a given path as a root it tries to load a decision based on relative path +- MemoryLoader - works as a HashMap (key-value store) +- ClosureLoader - allows for definition of simple async callback function which takes key as a parameter + and returns an `Arc` instance +- NoopLoader - (default) fails to load decision, allows for usage of create_decision + (mostly existing for streamlining API across languages) + +### Filesystem loader +Assuming that you have a folder with decision models (.json files) which is located under /app/decisions, +you may use FilesystemLoader in the following way: + +```rust +use serde_json::json; +use zen_engine::DecisionEngine; +use zen_engine::loader::{FilesystemLoader, FilesystemLoaderOptions}; + +async fn evaluate() { + let engine = DecisionEngine::new(FilesystemLoader::new(FilesystemLoaderOptions { + keep_in_memory: true, // optionally, keep in memory for increase performance + root: "/app/decisions" + })); + + let context = json!({ "customer": { "joinedAt": "2022-01-01" } }); + // If you plan on using it multiple times, you may cache JDM for minor performance gains + // In case of bindings (in other languages, this increase is much greater) + { + let promotion_decision = engine.get_decision("commercial/promotion.json").await.unwrap(); + let result = promotion_decision.evaluate(&context).await.unwrap(); + } + + // Or on demand + { + let result = engine.evaluate("commercial/promotion.json", &context).await.unwrap(); + } +} +``` + +### Custom loader +You may create a custom loader for zen engine by implementing `DecisionLoader` trait using async_trait crate. +Here's an example of how MemoryLoader has been implemented. + +```rust +use std::collections::HashMap; +use std::sync::{Arc, RwLock}; +use zen_engine::loader::{DecisionLoader, LoaderError, LoaderResponse}; +use zen_engine::model::DecisionContent; + +#[derive(Debug, Default)] +pub struct MemoryLoader { + memory_refs: RwLock>>, +} + +impl MemoryLoader { + pub fn add(&self, key: K, content: D) + where + K: Into, + D: Into, + { + let mut mref = self.memory_refs.write().unwrap(); + mref.insert(key.into(), Arc::new(content.into())); + } + pub fn get(&self, key: K) -> Option> + where + K: AsRef, + { + let mref = self.memory_refs.read().unwrap(); + mref.get(key.as_ref()).map(|r| r.clone()) + } + pub fn remove(&self, key: K) -> bool + where + K: AsRef, + { + let mut mref = self.memory_refs.write().unwrap(); + mref.remove(key.as_ref()).is_some() + } +} + +#[async_trait] +impl DecisionLoader for MemoryLoader { + async fn load(&self, key: &str) -> LoaderResponse { + self.get(&key) + .ok_or_else(|| LoaderError::NotFound(key.to_string())) + } +} ``` \ No newline at end of file diff --git a/core/engine/benches/engine.rs b/core/engine/benches/engine.rs index 639da65a..9f2144da 100644 --- a/core/engine/benches/engine.rs +++ b/core/engine/benches/engine.rs @@ -3,8 +3,8 @@ use criterion::{criterion_group, criterion_main, Bencher, Criterion}; use futures::executor::block_on; use serde_json::{json, Value}; use std::path::Path; -use zen_engine::engine::DecisionEngine; -use zen_engine::loader::filesystem::{FilesystemLoader, FilesystemLoaderOptions}; +use zen_engine::loader::{FilesystemLoader, FilesystemLoaderOptions}; +use zen_engine::DecisionEngine; fn create_graph() -> DecisionEngine { let cargo_root = Path::new(env!("CARGO_MANIFEST_DIR")); diff --git a/core/engine/src/decision.rs b/core/engine/src/decision.rs index a8ab8c7a..913cf908 100644 --- a/core/engine/src/decision.rs +++ b/core/engine/src/decision.rs @@ -1,8 +1,7 @@ use crate::engine::EvaluationOptions; use crate::handler::tree::{GraphResponse, GraphTree, GraphTreeConfig}; -use crate::loader::noop::NoopLoader; -use crate::loader::DecisionLoader; -use crate::model::decision::DecisionContent; +use crate::loader::{DecisionLoader, NoopLoader}; +use crate::model::DecisionContent; use anyhow::Context; use serde_json::Value; use std::sync::Arc; diff --git a/core/engine/src/engine.rs b/core/engine/src/engine.rs index b87fc531..ae1f7a6f 100644 --- a/core/engine/src/engine.rs +++ b/core/engine/src/engine.rs @@ -1,9 +1,7 @@ use crate::decision::Decision; use crate::handler::tree::GraphResponse; -use crate::loader::closure::ClosureLoader; -use crate::loader::noop::NoopLoader; -use crate::loader::{DecisionLoader, LoaderResponse, LoaderResult}; -use crate::model::decision::DecisionContent; +use crate::loader::{ClosureLoader, DecisionLoader, LoaderResponse, LoaderResult, NoopLoader}; +use crate::model::DecisionContent; use serde_json::Value; use std::future::Future; @@ -111,9 +109,8 @@ impl DecisionEngine { #[cfg(test)] mod tests { use super::*; - use crate::loader::filesystem::{FilesystemLoader, FilesystemLoaderOptions}; - use crate::loader::memory::MemoryLoader; - use crate::model::decision::DecisionContent; + use crate::loader::{FilesystemLoader, FilesystemLoaderOptions, MemoryLoader}; + use crate::model::DecisionContent; use serde_json::json; use std::path::Path; diff --git a/core/engine/src/handler/decision.rs b/core/engine/src/handler/decision.rs index 21523503..1052b52f 100644 --- a/core/engine/src/handler/decision.rs +++ b/core/engine/src/handler/decision.rs @@ -1,7 +1,7 @@ use crate::handler::node::{NodeRequest, NodeResponse, NodeResult}; use crate::handler::tree::{GraphTree, GraphTreeConfig}; use crate::loader::DecisionLoader; -use crate::model::decision::DecisionNodeKind; +use crate::model::DecisionNodeKind; use anyhow::{anyhow, Context}; use async_recursion::async_recursion; use std::ops::Deref; diff --git a/core/engine/src/handler/function/mod.rs b/core/engine/src/handler/function/mod.rs index ad18e68d..b200d1bc 100644 --- a/core/engine/src/handler/function/mod.rs +++ b/core/engine/src/handler/function/mod.rs @@ -5,7 +5,7 @@ use serde_json::{json, Value}; use crate::handler::function::script::{EvaluateResponse, Script}; use crate::handler::node::{NodeRequest, NodeResponse, NodeResult}; -use crate::model::decision::DecisionNodeKind; +use crate::model::DecisionNodeKind; mod script; mod vm; diff --git a/core/engine/src/handler/node.rs b/core/engine/src/handler/node.rs index 9d72edc8..75205768 100644 --- a/core/engine/src/handler/node.rs +++ b/core/engine/src/handler/node.rs @@ -1,4 +1,4 @@ -use crate::model::decision::DecisionNode; +use crate::model::DecisionNode; use serde::{Deserialize, Serialize}; use serde_json::Value; use std::fmt::{Display, Formatter}; diff --git a/core/engine/src/handler/table/zen.rs b/core/engine/src/handler/table/zen.rs index caae1f54..403d649e 100644 --- a/core/engine/src/handler/table/zen.rs +++ b/core/engine/src/handler/table/zen.rs @@ -6,7 +6,7 @@ use serde_json::Value; use crate::handler::node::{NodeRequest, NodeResponse, NodeResult}; use crate::handler::table::{RowOutput, RowOutputKind}; -use crate::model::decision::{DecisionNodeKind, DecisionTableContent, DecisionTableHitPolicy}; +use crate::model::{DecisionNodeKind, DecisionTableContent, DecisionTableHitPolicy}; use zen_vm::isolate::Isolate; #[derive(Debug, Serialize)] diff --git a/core/engine/src/handler/tree.rs b/core/engine/src/handler/tree.rs index 31acc43f..e4f8e47d 100644 --- a/core/engine/src/handler/tree.rs +++ b/core/engine/src/handler/tree.rs @@ -3,7 +3,7 @@ use crate::handler::function::FunctionHandler; use crate::handler::node::{NodeError, NodeRequest}; use crate::handler::table::zen::DecisionTableHandler; use crate::loader::DecisionLoader; -use crate::model::decision::{DecisionContent, DecisionNode, DecisionNodeKind}; +use crate::model::{DecisionContent, DecisionNode, DecisionNodeKind}; use anyhow::anyhow; use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; @@ -338,7 +338,7 @@ impl<'a, T: DecisionLoader> GraphTree<'a, T> { #[cfg(test)] mod tests { use crate::handler::tree::{GraphTree, GraphTreeConfig}; - use crate::loader::memory::MemoryLoader; + use crate::loader::MemoryLoader; use serde_json::json; use std::sync::Arc; diff --git a/core/engine/src/lib.rs b/core/engine/src/lib.rs index 806e4566..568ce00a 100644 --- a/core/engine/src/lib.rs +++ b/core/engine/src/lib.rs @@ -10,8 +10,8 @@ //! //! ```rust //! use serde_json::json; -//! use zen_engine::engine::DecisionEngine; -//! use zen_engine::model::decision::DecisionContent; +//! use zen_engine::DecisionEngine; +//! use zen_engine::model::DecisionContent; //! //! async fn evaluate() { //! let decision_content: DecisionContent = serde_json::from_str(include_str!("jdm_graph.json")).unwrap(); @@ -42,11 +42,11 @@ //! you may use FilesystemLoader in the following way: //! //! ```rust -//! use zen_engine::engine::DecisionEngine; -//! use zen_engine::loader::filesystem::{FilesystemLoader, FilesystemLoaderOptions}; +//! use serde_json::json; +//! use zen_engine::DecisionEngine; +//! use zen_engine::loader::{FilesystemLoader, FilesystemLoaderOptions}; //! //! async fn evaluate() { -//! use serde_json::json; //! let engine = DecisionEngine::new(FilesystemLoader::new(FilesystemLoaderOptions { //! keep_in_memory: true, // optionally, keep in memory for increase performance //! root: "/app/decisions" @@ -76,7 +76,7 @@ //! use std::collections::HashMap; //! use std::sync::{Arc, RwLock}; //! use zen_engine::loader::{DecisionLoader, LoaderError, LoaderResponse}; -//! use zen_engine::model::decision::DecisionContent; +//! use zen_engine::model::DecisionContent; //! //! #[derive(Debug, Default)] //! pub struct MemoryLoader { @@ -122,9 +122,13 @@ #![deny(clippy::unwrap_used)] #![allow(clippy::module_inception)] +mod decision; +mod engine; mod handler; -pub mod decision; -pub mod engine; pub mod loader; +#[path = "model/mod.rs"] pub mod model; + +pub use decision::Decision; +pub use engine::{DecisionEngine, EvaluationOptions}; diff --git a/core/engine/src/loader/filesystem.rs b/core/engine/src/loader/filesystem.rs index 0696215b..c0f2aea0 100644 --- a/core/engine/src/loader/filesystem.rs +++ b/core/engine/src/loader/filesystem.rs @@ -1,7 +1,7 @@ use crate::loader::{DecisionLoader, LoaderError, LoaderResponse}; use async_trait::async_trait; -use crate::model::decision::DecisionContent; +use crate::model::DecisionContent; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::fs::File; diff --git a/core/engine/src/loader/memory.rs b/core/engine/src/loader/memory.rs index 57a46fe1..bd7e28f1 100644 --- a/core/engine/src/loader/memory.rs +++ b/core/engine/src/loader/memory.rs @@ -1,5 +1,5 @@ use crate::loader::{DecisionLoader, LoaderError, LoaderResponse}; -use crate::model::decision::DecisionContent; +use crate::model::DecisionContent; use async_trait::async_trait; use std::collections::HashMap; use std::sync::{Arc, RwLock}; diff --git a/core/engine/src/loader/mod.rs b/core/engine/src/loader/mod.rs index 4aa1cf24..409bb85a 100644 --- a/core/engine/src/loader/mod.rs +++ b/core/engine/src/loader/mod.rs @@ -1,11 +1,16 @@ -pub mod closure; -pub mod filesystem; -pub mod memory; -pub mod noop; +mod closure; +mod filesystem; +mod memory; +mod noop; + +pub use closure::ClosureLoader; +pub use filesystem::{FilesystemLoader, FilesystemLoaderOptions}; +pub use memory::MemoryLoader; +pub use noop::NoopLoader; use async_trait::async_trait; -use crate::model::decision::DecisionContent; +use crate::model::DecisionContent; use std::fmt::Debug; use std::sync::Arc; use thiserror::Error; diff --git a/core/engine/src/model/decision.rs b/core/engine/src/model/decision.rs deleted file mode 100644 index 5c4866ee..00000000 --- a/core/engine/src/model/decision.rs +++ /dev/null @@ -1,87 +0,0 @@ -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; - -#[cfg(feature = "bincode")] -use bincode::{Decode, Encode}; - -#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] -#[cfg_attr(feature = "bincode", derive(Encode, Decode))] -#[serde(rename_all = "camelCase")] -pub struct DecisionContent { - pub nodes: Vec, - pub edges: Vec, -} - -#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] -#[cfg_attr(feature = "bincode", derive(Encode, Decode))] -#[serde(rename_all = "camelCase")] -pub struct DecisionEdge { - pub source_id: String, - pub target_id: String, -} - -#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] -#[cfg_attr(feature = "bincode", derive(Encode, Decode))] -#[serde(rename_all = "camelCase")] -pub struct DecisionNode { - pub id: String, - pub name: String, - #[serde(rename = "type")] - #[serde(flatten)] - pub kind: DecisionNodeKind, -} - -#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] -#[cfg_attr(feature = "bincode", derive(Encode, Decode))] -#[serde(tag = "type")] -#[serde(rename_all = "camelCase")] -pub enum DecisionNodeKind { - InputNode, - OutputNode, - FunctionNode { content: String }, - DecisionNode { content: DecisionNodeContent }, - DecisionTableNode { content: DecisionTableContent }, -} - -#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] -#[cfg_attr(feature = "bincode", derive(Encode, Decode))] -#[serde(rename_all = "camelCase")] -pub struct DecisionNodeContent { - pub key: String, -} - -#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] -#[cfg_attr(feature = "bincode", derive(Encode, Decode))] -#[serde(rename_all = "camelCase")] -pub struct DecisionTableContent { - pub rules: Vec>, - pub inputs: Vec, - pub outputs: Vec, - pub hit_policy: DecisionTableHitPolicy, -} - -#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] -#[cfg_attr(feature = "bincode", derive(Encode, Decode))] -#[serde(rename_all = "camelCase")] -pub enum DecisionTableHitPolicy { - First, - Collect, -} - -#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] -#[cfg_attr(feature = "bincode", derive(Encode, Decode))] -#[serde(rename_all = "camelCase")] -pub struct DecisionTableInputField { - pub id: String, - pub name: String, - pub field: String, -} - -#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] -#[cfg_attr(feature = "bincode", derive(Encode, Decode))] -#[serde(rename_all = "camelCase")] -pub struct DecisionTableOutputField { - pub id: String, - pub name: String, - pub field: String, -} diff --git a/core/engine/src/model/mod.rs b/core/engine/src/model/mod.rs index 3eae83d4..5c4866ee 100644 --- a/core/engine/src/model/mod.rs +++ b/core/engine/src/model/mod.rs @@ -1 +1,87 @@ -pub mod decision; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +#[cfg(feature = "bincode")] +use bincode::{Decode, Encode}; + +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +#[cfg_attr(feature = "bincode", derive(Encode, Decode))] +#[serde(rename_all = "camelCase")] +pub struct DecisionContent { + pub nodes: Vec, + pub edges: Vec, +} + +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +#[cfg_attr(feature = "bincode", derive(Encode, Decode))] +#[serde(rename_all = "camelCase")] +pub struct DecisionEdge { + pub source_id: String, + pub target_id: String, +} + +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +#[cfg_attr(feature = "bincode", derive(Encode, Decode))] +#[serde(rename_all = "camelCase")] +pub struct DecisionNode { + pub id: String, + pub name: String, + #[serde(rename = "type")] + #[serde(flatten)] + pub kind: DecisionNodeKind, +} + +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +#[cfg_attr(feature = "bincode", derive(Encode, Decode))] +#[serde(tag = "type")] +#[serde(rename_all = "camelCase")] +pub enum DecisionNodeKind { + InputNode, + OutputNode, + FunctionNode { content: String }, + DecisionNode { content: DecisionNodeContent }, + DecisionTableNode { content: DecisionTableContent }, +} + +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +#[cfg_attr(feature = "bincode", derive(Encode, Decode))] +#[serde(rename_all = "camelCase")] +pub struct DecisionNodeContent { + pub key: String, +} + +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +#[cfg_attr(feature = "bincode", derive(Encode, Decode))] +#[serde(rename_all = "camelCase")] +pub struct DecisionTableContent { + pub rules: Vec>, + pub inputs: Vec, + pub outputs: Vec, + pub hit_policy: DecisionTableHitPolicy, +} + +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +#[cfg_attr(feature = "bincode", derive(Encode, Decode))] +#[serde(rename_all = "camelCase")] +pub enum DecisionTableHitPolicy { + First, + Collect, +} + +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +#[cfg_attr(feature = "bincode", derive(Encode, Decode))] +#[serde(rename_all = "camelCase")] +pub struct DecisionTableInputField { + pub id: String, + pub name: String, + pub field: String, +} + +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +#[cfg_attr(feature = "bincode", derive(Encode, Decode))] +#[serde(rename_all = "camelCase")] +pub struct DecisionTableOutputField { + pub id: String, + pub name: String, + pub field: String, +} diff --git a/core/parser/README.md b/core/parser/README.md index 65a65852..0d51c27e 100644 --- a/core/parser/README.md +++ b/core/parser/README.md @@ -1,124 +1,71 @@ -# ZEN Engine +# ZEN Parser -ZEN Engine is business friendly Open-Source Business Rules Engine -(BRE) to execute decision models according to the GoRules JSON -Decision Model (JDM) standard. It is written in Rust and provides -native bindings for NodeJS and Python. ZEN Engine allows to load -and execute JSON Decision Model (JDM) from JSON files. +Virtual machine for business-first expression language used in +Zen business rules engine by GoRules. The language is designed +to follow these principles: + +- Side-effect free +- Dynamic types +- Simple syntax for broad audiences + +It's primary objective is to bridge the gap between business analysts and engineers, +while providing outstanding performance and readability. ## Resources [Documentation](https://gorules.io/docs/) -[Online Rules Engine Editor](https://editor.gorules.io/) +[Zen Language Playground](https://gorules.io/docs/rules-engine/expression-language#playground) -## Installation +[Online Rules Editor](https://editor.gorules.io/) -Add the following to your Cargo.toml file: -```toml -[dependencies] -zen-engine = "0" +## Unary tests +Unary test is a comma-separated list of simple expressions which +evaluate to a boolean value. Each comma separation is treated as +or operator. Inside unary expressions, a special symbol is available +$ which refers to a current column. + +Some examples: +```js +// Given: $ = 1 +1, 2, 3 // true +1 // true +>= 1 // true +< 1 // false +[0..10] // true, (internally this is $ >= 0 and $ <= 10) +> 0 and < 10 // true + +// Given: $ = 'USD' +'GBP', 'USD' // true +'EUR' // false +startsWith($, "US") // true - defaults to expression mode, comma is unavailable +endsWith($, "US") // false - defaults to expression mode +lower($) == "usd" // true - defaults to expression mode ``` -## Usage -To execute a simple decision using a Noop (default) loader you can use the code below. +## Standard tests -```rust -use serde_json::json; -use zen_engine::engine::DecisionEngine; -use zen_engine::model::decision::DecisionContent; -async fn evaluate() { - let decision_content: DecisionContent = serde_json::from_str(include_str!("jdm_graph.json")).unwrap(); - let engine = DecisionEngine::default(); - let decision = engine.create_decision(decision_content.into()); - let result = decision.evaluate(&json!({ "input": 12 })).await; -} -``` +Expressions feature full capability syntax of ZEN language. +They give you access to all functions, and are most useful when +defining columns or outputs. Full syntax is also available in unary +expressions when $ is used (as it forces the expression mode). -Alternatively, you may create decision indirectly without constructing the engine utilising -`Decision::from` function. +```js +100 + 100 // 200 +10 * 5 // 50 +10 ^ 2 // 100 +1 in [1, 2, 3] // true +5 in (5..10] // false +sum([1, 2, 3]) // 6 +max([1, 2, 3]) // 3 -## Loaders -For more advanced use cases where you want to load multiple decisions and utilise graphs you -may use one of the following pre-made loaders: -- FilesystemLoader - with a given path as a root it tries to load a decision based on relative path -- MemoryLoader - works as a HashMap (key-value store) -- ClosureLoader - allows for definition of simple async callback function which takes key as a parameter -and returns an `Arc` instance -- NoopLoader - (default) fails to load decision, allows for usage of create_decision -(mostly existing for streamlining API across languages) +"hello" + " " + "world" // "hello world" +len("world") // 5 +weekdayString(date("2022-11-08")) // "Tue" +contains("hello world", "hello") // true +upper('john') // "JOHN" -### Filesystem loader -Assuming that you have a folder with decision models (.json files) which is located under /app/decisions, -you may use FilesystemLoader in the following way: - -```rust -use zen_engine::engine::DecisionEngine; -use zen_engine::loader::filesystem::{FilesystemLoader, FilesystemLoaderOptions}; -async fn evaluate() { - use serde_json::json; - let engine = DecisionEngine::new(FilesystemLoader::new(FilesystemLoaderOptions { - keep_in_memory: true, // optionally, keep in memory for increase performance - root: "/app/decisions" - })); - - let context = json!({ "customer": { "joinedAt": "2022-01-01" } }); - // If you plan on using it multiple times, you may cache JDM for minor performance gains - // In case of bindings (in other languages, this increase is much greater) - { - let promotion_decision = engine.get_decision("commercial/promotion.json").await.unwrap(); - let result = promotion_decision.evaluate(&context).await.unwrap(); - } - - // Or on demand - { - let result = engine.evaluate("commercial/promotion.json", &context).await.unwrap(); - } -} -``` - -### Custom loader -You may create a custom loader for zen engine by implementing `DecisionLoader` trait using async_trait crate. -Here's an example of how MemoryLoader has been implemented. - -```rust -use std::collections::HashMap; -use std::sync::{Arc, RwLock}; -use zen_engine::loader::{DecisionLoader, LoaderError, LoaderResponse}; -use zen_engine::model::decision::DecisionContent; -#[derive(Debug, Default)] -pub struct MemoryLoader { - memory_refs: RwLock>>, -} -impl MemoryLoader { - pub fn add(&self, key: K, content: D) - where - K: Into, - D: Into, - { - let mut mref = self.memory_refs.write().unwrap(); - mref.insert(key.into(), Arc::new(content.into())); - } - pub fn get(&self, key: K) -> Option> - where - K: AsRef, - { - let mref = self.memory_refs.read().unwrap(); - mref.get(key.as_ref()).map(|r| r.clone()) - } - pub fn remove(&self, key: K) -> bool - where - K: AsRef, - { - let mut mref = self.memory_refs.write().unwrap(); - mref.remove(key.as_ref()).is_some() - } -} -#[async_trait] -impl DecisionLoader for MemoryLoader { - async fn load(&self, key: &str) -> LoaderResponse { - self.get(&key) - .ok_or_else(|| LoaderError::NotFound(key.to_string())) - } -} +some(['admin', 'user'], # == "admin") // true +not all([100, 200, 400, 800], # in (100..800)) // false +filter([100, 200, 400, 800], # >= 200) // [200, 400, 800] ``` \ No newline at end of file diff --git a/core/vm/README.md b/core/vm/README.md index fb8241a6..bf6b6091 100644 --- a/core/vm/README.md +++ b/core/vm/README.md @@ -1,6 +1,6 @@ -# ZEN Parser +# ZEN VM -Virtual machine for business-first expression language used in +Parser for business-first expression language used in Zen business rules engine by GoRules. The language is designed to follow these principles: @@ -8,7 +8,7 @@ to follow these principles: - Dynamic types - Simple syntax for broad audiences -It's primary objective is to bridge the gap between business analysts and engineers, +It's primary objective is to bridge the gap between business analysts and engineers, while providing outstanding performance and readability. ## Resources @@ -20,9 +20,9 @@ while providing outstanding performance and readability. [Online Rules Editor](https://editor.gorules.io/) ## Unary tests -Unary test is a comma-separated list of simple expressions which -evaluate to a boolean value. Each comma separation is treated as -or operator. Inside unary expressions, a special symbol is available +Unary test is a comma-separated list of simple expressions which +evaluate to a boolean value. Each comma separation is treated as +or operator. Inside unary expressions, a special symbol is available $ which refers to a current column. Some examples: @@ -45,9 +45,9 @@ lower($) == "usd" // true - defaults to expression mode ## Standard tests -Expressions feature full capability syntax of ZEN language. -They give you access to all functions, and are most useful when -defining columns or outputs. Full syntax is also available in unary +Expressions feature full capability syntax of ZEN language. +They give you access to all functions, and are most useful when +defining columns or outputs. Full syntax is also available in unary expressions when $ is used (as it forces the expression mode). ```js