moduforge_rules_engine/handler/table/
zen.rs

1use ahash::HashMap;
2use anyhow::anyhow;
3use std::sync::Arc;
4
5use crate::handler::node::{NodeRequest, NodeResponse, NodeResult};
6use crate::handler::table::{RowOutput, RowOutputKind};
7use crate::model::{DecisionNodeKind, DecisionTableContent, DecisionTableHitPolicy};
8use serde::Serialize;
9use tokio::sync::Mutex;
10use moduforge_rules_expression::variable::Variable;
11use moduforge_rules_expression::Isolate;
12
13#[derive(Debug, Serialize)]
14struct RowResult {
15    rule: Option<HashMap<String, String>>,
16    reference_map: Option<HashMap<String, Variable>>,
17    index: usize,
18    #[serde(skip)]
19    output: RowOutput,
20}
21
22#[derive(Debug)]
23pub struct DecisionTableHandler {
24    trace: bool,
25}
26
27impl DecisionTableHandler {
28    pub fn new(trace: bool) -> Self {
29        Self { trace }
30    }
31
32    pub async fn handle(
33        &mut self,
34        request: NodeRequest,
35    ) -> NodeResult {
36        let content = match &request.node.kind {
37            DecisionNodeKind::DecisionTableNode { content } => Ok(content),
38            _ => Err(anyhow!("Unexpected node type")),
39        }?;
40
41        let inner_handler = DecisionTableHandlerInner::new(self.trace);
42        inner_handler.handle(request.input.depth_clone(1), content).await
43    }
44}
45
46struct DecisionTableHandlerInner<'a> {
47    isolate: Isolate<'a>,
48    trace: bool,
49}
50
51impl<'a> DecisionTableHandlerInner<'a> {
52    pub fn new(trace: bool) -> Self {
53        Self { isolate: Isolate::new(), trace }
54    }
55
56    pub async fn handle(
57        self,
58        input: Variable,
59        content: &'a DecisionTableContent,
60    ) -> NodeResult {
61        let self_mutex = Arc::new(Mutex::new(self));
62
63        content
64            .transform_attributes
65            .run_with(input, |input| {
66                let self_mutex = self_mutex.clone();
67                async move {
68                    let mut self_ref = self_mutex.lock().await;
69
70                    self_ref.isolate.clear_references();
71                    self_ref.isolate.set_environment(input);
72                    match &content.hit_policy {
73                        DecisionTableHitPolicy::First => {
74                            self_ref.handle_first_hit(&content).await
75                        },
76                        DecisionTableHitPolicy::Collect => {
77                            self_ref.handle_collect(&content).await
78                        },
79                    }
80                }
81            })
82            .await
83    }
84
85    async fn handle_first_hit(
86        &mut self,
87        content: &'a DecisionTableContent,
88    ) -> NodeResult {
89        for i in 0..content.rules.len() {
90            if let Some(result) = self.evaluate_row(&content, i) {
91                return Ok(NodeResponse {
92                    output: result.output.to_json().await,
93                    trace_data: self
94                        .trace
95                        .then(|| serde_json::to_value(&result).ok())
96                        .flatten(),
97                });
98            }
99        }
100
101        Ok(NodeResponse { output: Variable::Null, trace_data: None })
102    }
103
104    async fn handle_collect(
105        &mut self,
106        content: &'a DecisionTableContent,
107    ) -> NodeResult {
108        let mut results = Vec::new();
109        for i in 0..content.rules.len() {
110            if let Some(result) = self.evaluate_row(&content, i) {
111                results.push(result);
112            }
113        }
114
115        let mut outputs = Vec::with_capacity(results.len());
116        for res in &results {
117            outputs.push(res.output.to_json().await);
118        }
119
120        Ok(NodeResponse {
121            output: Variable::from_array(outputs),
122            trace_data: self
123                .trace
124                .then(|| serde_json::to_value(&results).ok())
125                .flatten(),
126        })
127    }
128
129    fn evaluate_row(
130        &mut self,
131        content: &'a DecisionTableContent,
132        index: usize,
133    ) -> Option<RowResult> {
134        let rule = content.rules.get(index)?;
135        for input in &content.inputs {
136            let rule_value = rule.get(input.id.as_str())?;
137            if rule_value.trim().is_empty() {
138                continue;
139            }
140
141            match &input.field {
142                None => {
143                    let result =
144                        self.isolate.run_standard(rule_value.as_str()).ok()?;
145                    if !result.as_bool().unwrap_or(false) {
146                        return None;
147                    }
148                },
149                Some(field) => {
150                    self.isolate.set_reference(field.as_str()).ok()?;
151                    if !self.isolate.run_unary(rule_value.as_str()).ok()? {
152                        return None;
153                    }
154                },
155            }
156        }
157
158        let mut outputs: RowOutput = Default::default();
159        for output in &content.outputs {
160            let rule_value = rule.get(output.id.as_str())?;
161            if rule_value.trim().is_empty() {
162                continue;
163            }
164
165            let res = self.isolate.run_standard(rule_value).ok()?;
166            outputs.push(&output.field, RowOutputKind::Variable(res));
167        }
168
169        if !self.trace {
170            return Some(RowResult {
171                output: outputs,
172                rule: None,
173                reference_map: None,
174                index,
175            });
176        }
177
178        let rule_id = match rule.get("_id") {
179            Some(rid) => rid.clone(),
180            None => "".to_string(),
181        };
182
183        let mut expressions: HashMap<String, String> = Default::default();
184        let mut reference_map: HashMap<String, Variable> = Default::default();
185
186        expressions.insert("_id".to_string(), rule_id.clone());
187        if let Some(description) = rule.get("_description") {
188            expressions.insert("_description".to_string(), description.clone());
189        }
190
191        for input in &content.inputs {
192            let rule_value = rule.get(input.id.as_str())?;
193            let Some(input_field) = &input.field else {
194                continue;
195            };
196
197            if let Some(reference) =
198                self.isolate.get_reference(input_field.as_str())
199            {
200                reference_map.insert(input_field.clone(), reference);
201            } else if let Some(reference) =
202                self.isolate.run_standard(input_field.as_str()).ok()
203            {
204                reference_map.insert(input_field.clone(), reference);
205            }
206
207            let input_identifier = format!("{input_field}[{}]", &input.id);
208            expressions.insert(input_identifier, rule_value.clone());
209        }
210
211        Some(RowResult {
212            output: outputs,
213            rule: Some(expressions),
214            reference_map: Some(reference_map),
215            index,
216        })
217    }
218}