zen_engine/nodes/decision_table/
mod.rs

1use crate::nodes::definition::NodeHandler;
2use crate::nodes::result::NodeResult;
3use crate::nodes::{NodeContext, NodeResponse};
4use ahash::HashMap;
5use serde::Serialize;
6use std::ops::Deref;
7use std::rc::Rc;
8use std::sync::Arc;
9use zen_expression::variable::ToVariable;
10use zen_expression::Isolate;
11use zen_types::decision::{DecisionTableContent, DecisionTableHitPolicy, TransformAttributes};
12use zen_types::variable::Variable;
13#[derive(Debug, Clone)]
14pub struct DecisionTableNodeHandler;
15
16pub type DecisionTableNodeData = DecisionTableContent;
17
18type DecisionTableContext = NodeContext<DecisionTableNodeData, DecisionTableNodeTrace>;
19
20impl NodeHandler for DecisionTableNodeHandler {
21    type NodeData = DecisionTableNodeData;
22    type TraceData = DecisionTableNodeTrace;
23
24    fn transform_attributes(
25        &self,
26        ctx: &NodeContext<Self::NodeData, Self::TraceData>,
27    ) -> Option<TransformAttributes> {
28        Some(ctx.node.transform_attributes.clone())
29    }
30
31    async fn handle(&self, ctx: NodeContext<Self::NodeData, Self::TraceData>) -> NodeResult {
32        match ctx.node.hit_policy {
33            DecisionTableHitPolicy::First => self.handle_first_hit(ctx),
34            DecisionTableHitPolicy::Collect => self.handle_collect(ctx),
35        }
36    }
37}
38
39impl DecisionTableNodeHandler {
40    fn handle_first_hit(&self, ctx: DecisionTableContext) -> NodeResult {
41        let mut isolate = Isolate::new();
42        isolate.set_environment(ctx.input.depth_clone(1));
43
44        for (index, rule) in ctx.node.rules.iter().enumerate() {
45            if let Some(result) = self.evaluate_row(&ctx, rule, &mut isolate) {
46                return match result {
47                    RowResult::Output(output) => ctx.success(output),
48                    RowResult::WithTrace {
49                        output,
50                        reference_map,
51                        rule,
52                    } => {
53                        ctx.trace(|t| {
54                            *t = DecisionTableNodeTrace::FirstHit(DecisionTableRowTrace {
55                                reference_map,
56                                index,
57                                rule,
58                            })
59                        });
60
61                        ctx.success(output)
62                    }
63                };
64            }
65        }
66
67        Ok(NodeResponse {
68            output: Variable::Null,
69            trace_data: None,
70        })
71    }
72
73    fn handle_collect(&self, ctx: DecisionTableContext) -> NodeResult {
74        let mut isolate = Isolate::new();
75        let mut outputs = Vec::new();
76        let mut traces = Vec::new();
77        isolate.set_environment(ctx.input.depth_clone(1));
78
79        for (index, rule) in ctx.node.rules.iter().enumerate() {
80            if let Some(result) = self.evaluate_row(&ctx, rule, &mut isolate) {
81                match result {
82                    RowResult::Output(output) => {
83                        outputs.push(output);
84                    }
85                    RowResult::WithTrace {
86                        output,
87                        reference_map,
88                        rule,
89                    } => {
90                        outputs.push(output);
91                        traces.push(DecisionTableRowTrace {
92                            index,
93                            rule,
94                            reference_map,
95                        });
96                    }
97                }
98            }
99        }
100
101        ctx.trace(|t| {
102            *t = DecisionTableNodeTrace::Collect(traces);
103        });
104
105        ctx.success(Variable::from_array(outputs))
106    }
107
108    fn evaluate_row<'a>(
109        &self,
110        ctx: &'a DecisionTableContext,
111        rule: &'a HashMap<Arc<str>, Arc<str>>,
112        isolate: &mut Isolate<'a>,
113    ) -> Option<RowResult> {
114        let content = &ctx.node;
115        for input in content.inputs.iter() {
116            let rule_value = rule.get(&input.id)?;
117            if rule_value.is_empty() {
118                continue;
119            }
120
121            match &input.field {
122                None => {
123                    let result = isolate.run_standard(rule_value).ok()?;
124                    if !result.as_bool().unwrap_or(false) {
125                        return None;
126                    }
127                }
128                Some(field) => {
129                    isolate.set_reference(&field).ok()?;
130                    if !isolate.run_unary(&rule_value).ok()? {
131                        return None;
132                    }
133                }
134            }
135        }
136
137        let outputs = Variable::empty_object();
138        for output in content.outputs.iter() {
139            let rule_value = rule.get(&output.id)?;
140            if rule_value.is_empty() {
141                continue;
142            }
143
144            let res = isolate.run_standard(rule_value).ok()?;
145            outputs.dot_insert(output.field.deref(), res);
146        }
147
148        if !ctx.config.trace {
149            return Some(RowResult::Output(outputs));
150        }
151
152        let id_str = Rc::<str>::from("_id");
153        let description_str = Rc::<str>::from("_description");
154
155        let rule_id = match rule.get(id_str.as_ref()) {
156            Some(rid) => Rc::<str>::from(rid.deref()),
157            None => Rc::from(""),
158        };
159
160        let mut expressions: HashMap<Rc<str>, Rc<str>> = Default::default();
161        let mut reference_map: HashMap<Rc<str>, Variable> = Default::default();
162
163        expressions.insert(id_str.clone(), rule_id.clone());
164        if let Some(description) = rule.get(description_str.as_ref()) {
165            expressions.insert(description_str.clone(), Rc::from(description.deref()));
166        }
167
168        for input in content.inputs.iter() {
169            let rule_value = rule.get(input.id.deref())?;
170            let Some(input_field) = &input.field else {
171                continue;
172            };
173
174            if let Some(reference) = isolate.get_reference(input_field.deref()) {
175                reference_map.insert(Rc::from(input_field.deref()), reference);
176            } else if let Some(reference) = isolate.run_standard(input_field.deref()).ok() {
177                reference_map.insert(Rc::from(input_field.deref()), reference);
178            }
179
180            let input_identifier = format!("{input_field}[{}]", &input.id);
181            expressions.insert(
182                Rc::from(input_identifier.as_str()),
183                Rc::from(rule_value.deref()),
184            );
185        }
186
187        Some(RowResult::WithTrace {
188            output: outputs.to_variable(),
189            reference_map,
190            rule: expressions,
191        })
192    }
193}
194
195enum RowResult {
196    Output(Variable),
197    WithTrace {
198        output: Variable,
199        reference_map: HashMap<Rc<str>, Variable>,
200        rule: HashMap<Rc<str>, Rc<str>>,
201    },
202}
203
204#[derive(Debug, Clone, Serialize, ToVariable)]
205pub struct DecisionTableRowTrace {
206    index: usize,
207    reference_map: HashMap<Rc<str>, Variable>,
208    rule: HashMap<Rc<str>, Rc<str>>,
209}
210
211#[derive(Debug, Clone, Serialize, ToVariable)]
212#[serde(untagged)]
213pub enum DecisionTableNodeTrace {
214    FirstHit(DecisionTableRowTrace),
215    Collect(Vec<DecisionTableRowTrace>),
216}
217
218impl Default for DecisionTableNodeTrace {
219    fn default() -> Self {
220        DecisionTableNodeTrace::Collect(Default::default())
221    }
222}