1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
//! # ZEN Engine
//!
//! ZEN Engine is business friendly Open-Source Business Rules Engine (BRE) which executes decision
//! models according to the GoRules JSON Decision Model (JDM) standard. It's written in Rust and
//! provides native bindings for NodeJS and Python.
//!
//! # Usage
//!
//! To execute a simple decision using a Noop (default) loader you can use the code below.
//!
//! ```rust
//! 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:
//!
//! ```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.
//! 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<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()
//!     }
//! }
//!
//! impl DecisionLoader for MemoryLoader {
//! fn load<'a>(&'a self, key: &'a str) -> impl Future<Output = LoaderResponse> + 'a {
//!     async move {
//!         self.get(&key)
//!             .ok_or_else(|| LoaderError::NotFound(key.to_string()).into())
//!     }
//! }
//! ```

#![deny(clippy::unwrap_used)]
#![allow(clippy::module_inception)]

mod decision;
mod engine;
mod error;
pub mod handler;
mod util;

mod config;
pub mod loader;
#[path = "model/mod.rs"]
pub mod model;

pub use config::ZEN_CONFIG;
pub use decision::Decision;
pub use engine::{DecisionEngine, EvaluationOptions};
pub use error::EvaluationError;
pub use handler::graph::DecisionGraphResponse;
pub use handler::graph::DecisionGraphTrace;
pub use handler::graph::DecisionGraphValidationError;
pub use handler::node::NodeError;