ZEN Engine is embeddable Open-Source Business Rules Engine (BRE). It's written in Rust and provides native bindings for NodeJS, Go and Python.

Installation

[dependencies]
zen-engine = "0"

Usage

To execute a simple decision using a Noop (default) loader you can use the code below.

use serde_json::json;
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;
}

Alternatively, you may create decision indirectly without constructing the engine utilising Decision::from function.

Loaders

For more advanced use cases where you want to load multiple decisions and utilise graphs you may use one of the following pre-made loaders:

  • FilesystemLoader - with a given path as a root it tries to load a decision based on relative path
  • MemoryLoader - works as a HashMap (key-value store)
  • ClosureLoader - allows for definition of simple async callback function which takes key as a parameter and returns an Arc<DecisionContent> 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:

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.

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<HashMap<String, Arc<DecisionContent>>>,
}

impl MemoryLoader {
    pub fn add<K, D>(&self, key: K, content: D)
        where
            K: Into<String>,
            D: Into<DecisionContent>,
    {
        let mut mref = self.memory_refs.write().unwrap();
        mref.insert(key.into(), Arc::new(content.into()));
    }
    pub fn get<K>(&self, key: K) -> Option<Arc<DecisionContent>>
        where
            K: AsRef<str>,
    {
        let mref = self.memory_refs.read().unwrap();
        mref.get(key.as_ref()).map(|r| r.clone())
    }
    pub fn remove<K>(&self, key: K) -> bool
        where
            K: AsRef<str>,
    {
        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()))
    }
}

Evaluate expressions

You may also evaluate ZEN Expressions using the zen-expression crate:

use zen_expression::{evaluate_expression, json};

fn main() {
    let context = json!({ "tax": { "percentage": 10 } });
    let tax_amount = evaluate_expression("50 * tax.percentage / 100", &context).unwrap();

    assert_eq!(tax_amount, json!(5));
}

High performance

When evaluating a lot of expressions at once, you can use Isolate directly. Under the hood, Isolate will re-use allocated memory from previous evaluations, drastically improving performance.

use zen_expression::{Isolate, json};

fn main() {
    let context = json!({ "tax": { "percentage": 10 } });
    let mut isolate = Isolate::with_environment(&context);

    // Fast 🚀
    for _ in 0..1_000 {
        let tax_amount = isolate.run_standard("50 * tax.percentage / 100").unwrap();
        assert_eq!(tax_amount, json!(5));
    }
}