,
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