zen_engine/
engine.rs

1use crate::decision::Decision;
2use crate::decision_graph::graph::DecisionGraphResponse;
3use crate::loader::{ClosureLoader, DynamicLoader, LoaderResponse, LoaderResult, NoopLoader};
4use crate::model::DecisionContent;
5use crate::nodes::custom::{DynamicCustomNode, NoopCustomNode};
6use crate::EvaluationError;
7use serde_json::Value;
8use std::fmt::Debug;
9use std::future::Future;
10use std::sync::Arc;
11use strum::{EnumString, IntoStaticStr};
12use zen_expression::variable::Variable;
13
14/// Structure used for generating and evaluating JDM decisions
15#[derive(Debug, Clone)]
16pub struct DecisionEngine {
17    loader: DynamicLoader,
18    adapter: DynamicCustomNode,
19}
20
21#[derive(Debug)]
22pub struct EvaluationOptions {
23    pub trace: bool,
24    pub max_depth: u8,
25}
26
27impl Default for EvaluationOptions {
28    fn default() -> Self {
29        Self {
30            trace: false,
31            max_depth: 10,
32        }
33    }
34}
35
36#[derive(Debug)]
37pub struct EvaluationSerializedOptions {
38    pub trace: EvaluationTraceKind,
39    pub max_depth: u8,
40}
41
42impl Default for EvaluationSerializedOptions {
43    fn default() -> Self {
44        Self {
45            trace: EvaluationTraceKind::None,
46            max_depth: 10,
47        }
48    }
49}
50
51#[derive(Debug, Default, PartialEq, Eq, EnumString, IntoStaticStr)]
52#[strum(serialize_all = "camelCase")]
53pub enum EvaluationTraceKind {
54    #[default]
55    None,
56    Default,
57    String,
58    Reference,
59    ReferenceString,
60}
61
62impl EvaluationTraceKind {
63    pub fn serialize_trace(&self, trace: &Variable) -> Value {
64        match self {
65            EvaluationTraceKind::None => Value::Null,
66            EvaluationTraceKind::Default => serde_json::to_value(&trace).unwrap_or_default(),
67            EvaluationTraceKind::String => {
68                Value::String(serde_json::to_string(&trace).unwrap_or_default())
69            }
70            EvaluationTraceKind::Reference => {
71                serde_json::to_value(&trace.serialize_ref()).unwrap_or_default()
72            }
73            EvaluationTraceKind::ReferenceString => {
74                Value::String(serde_json::to_string(&trace.serialize_ref()).unwrap_or_default())
75            }
76        }
77    }
78}
79
80impl Default for DecisionEngine {
81    fn default() -> Self {
82        Self {
83            loader: Arc::new(NoopLoader::default()),
84            adapter: Arc::new(NoopCustomNode::default()),
85        }
86    }
87}
88
89impl DecisionEngine {
90    pub fn new(loader: DynamicLoader, adapter: DynamicCustomNode) -> Self {
91        Self {
92            loader,
93            adapter,
94            ..Default::default()
95        }
96    }
97
98    pub fn with_adapter(mut self, adapter: DynamicCustomNode) -> Self {
99        self.adapter = adapter;
100        self
101    }
102
103    pub fn with_loader(mut self, loader: DynamicLoader) -> Self {
104        self.loader = loader;
105        self
106    }
107
108    pub fn with_closure_loader<F, O>(mut self, loader: F) -> Self
109    where
110        F: Fn(String) -> O + Sync + Send + 'static,
111        O: Future<Output = LoaderResponse> + Send,
112    {
113        self.loader = Arc::new(ClosureLoader::new(loader));
114        self
115    }
116
117    /// Evaluates a decision through loader using a key
118    pub async fn evaluate<K>(
119        &self,
120        key: K,
121        context: Variable,
122    ) -> Result<DecisionGraphResponse, Box<EvaluationError>>
123    where
124        K: AsRef<str>,
125    {
126        self.evaluate_with_opts(key, context, Default::default())
127            .await
128    }
129
130    /// Evaluates a decision through loader using a key with advanced options
131    pub async fn evaluate_with_opts<K>(
132        &self,
133        key: K,
134        context: Variable,
135        options: EvaluationOptions,
136    ) -> Result<DecisionGraphResponse, Box<EvaluationError>>
137    where
138        K: AsRef<str>,
139    {
140        let content = self.loader.load(key.as_ref()).await?;
141        let decision = self.create_decision(content);
142        decision.evaluate_with_opts(context, options).await
143    }
144
145    pub async fn evaluate_serialized<K>(
146        &self,
147        key: K,
148        context: Variable,
149        options: EvaluationSerializedOptions,
150    ) -> Result<Value, Value>
151    where
152        K: AsRef<str>,
153    {
154        let content = self
155            .loader
156            .load(key.as_ref())
157            .await
158            .map_err(|err| Value::String(err.to_string()))?;
159
160        let decision = self.create_decision(content);
161        decision.evaluate_serialized(context, options).await
162    }
163
164    /// Creates a decision from DecisionContent, exists for easier binding creation
165    pub fn create_decision(&self, content: Arc<DecisionContent>) -> Decision {
166        Decision::from(content)
167            .with_loader(self.loader.clone())
168            .with_adapter(self.adapter.clone())
169    }
170
171    /// Retrieves a decision based on the loader
172    pub async fn get_decision(&self, key: &str) -> LoaderResult<Decision> {
173        let content = self.loader.load(key).await?;
174        Ok(self.create_decision(content))
175    }
176}