Skip to main content

mockforge_foundation/state_machine/
condition_evaluator.rs

1//! Condition expression evaluator for state machine transitions
2//!
3//! Provides safe evaluation of JavaScript/TypeScript-like expressions for
4//! conditional state transitions. Uses rquickjs for sandboxed execution.
5
6use serde_json::Value;
7use std::collections::HashMap;
8use thiserror::Error;
9
10/// Error types for condition evaluation
11#[derive(Debug, Error)]
12pub enum ConditionError {
13    /// Expression parsing or syntax error
14    #[error("Expression syntax error: {0}")]
15    SyntaxError(String),
16
17    /// Runtime evaluation error
18    #[error("Evaluation error: {0}")]
19    EvaluationError(String),
20
21    /// Type mismatch error
22    #[error("Type error: {0}")]
23    TypeError(String),
24
25    /// Variable not found
26    #[error("Variable not found: {0}")]
27    VariableNotFound(String),
28}
29
30/// Result type for condition evaluation
31pub type ConditionResult<T> = Result<T, ConditionError>;
32
33/// Condition evaluator for state machine transitions
34///
35/// Evaluates JavaScript/TypeScript-like expressions in a sandboxed environment.
36/// Supports variable access, comparison operators, logical operators, and
37/// array/object access.
38pub struct ConditionEvaluator {
39    /// Context variables available for evaluation
40    context: HashMap<String, Value>,
41}
42
43impl ConditionEvaluator {
44    /// Create a new condition evaluator with empty context
45    pub fn new() -> Self {
46        Self {
47            context: HashMap::new(),
48        }
49    }
50
51    /// Create a new condition evaluator with initial context
52    pub fn with_context(context: HashMap<String, Value>) -> Self {
53        Self { context }
54    }
55
56    /// Set a context variable
57    pub fn set_variable(&mut self, name: impl Into<String>, value: Value) {
58        self.context.insert(name.into(), value);
59    }
60
61    /// Get a context variable
62    pub fn get_variable(&self, name: &str) -> Option<&Value> {
63        self.context.get(name)
64    }
65
66    /// Evaluate a condition expression
67    ///
68    /// The expression can access variables from the context using dot notation
69    /// (e.g., `state.status`, `entity.count`). Supports:
70    /// - Comparison: `==`, `!=`, `>`, `<`, `>=`, `<=`
71    /// - Logical: `&&`, `||`, `!`
72    /// - Arithmetic: `+`, `-`, `*`, `/`, `%`
73    /// - Array/object access: `arr[0]`, `obj.field`
74    ///
75    /// Returns `true` if the condition is satisfied, `false` otherwise.
76    pub fn evaluate(&self, expression: &str) -> ConditionResult<bool> {
77        // Use rquickjs for safe JavaScript evaluation
78        // This is a simplified implementation - in production, you'd want
79        // more sophisticated parsing and validation
80
81        // For now, we'll use a simple expression parser
82        // In a full implementation, we'd use rquickjs::Context to evaluate JS
83        self.evaluate_simple(expression)
84    }
85
86    /// Simple expression evaluator (fallback when rquickjs is not available)
87    ///
88    /// This is a basic implementation that handles common cases.
89    /// For full JavaScript support, use rquickjs.
90    fn evaluate_simple(&self, expression: &str) -> ConditionResult<bool> {
91        let expr = expression.trim();
92
93        // Handle boolean literals
94        if expr == "true" {
95            return Ok(true);
96        }
97        if expr == "false" {
98            return Ok(false);
99        }
100
101        // Handle comparison operators
102        if let Some(result) = self.evaluate_comparison(expr)? {
103            return Ok(result);
104        }
105
106        // Handle logical operators
107        if let Some(result) = self.evaluate_logical(expr)? {
108            return Ok(result);
109        }
110
111        // Handle variable access
112        if let Some(value) = self.get_variable_value(expr)? {
113            return self.value_to_bool(&value);
114        }
115
116        Err(ConditionError::SyntaxError(format!("Unable to evaluate expression: {}", expr)))
117    }
118
119    /// Evaluate comparison expressions (==, !=, >, <, >=, <=)
120    fn evaluate_comparison(&self, expr: &str) -> ConditionResult<Option<bool>> {
121        // Note: We can't use closures with different signatures in an array,
122        // so we'll handle each operator separately
123
124        // Handle == operator
125        if let Some((left, right)) = expr.split_once("==") {
126            let left_val = self.evaluate_value(left.trim())?;
127            let right_val = self.evaluate_value(right.trim())?;
128            return Ok(Some(left_val == right_val));
129        }
130
131        // Handle != operator
132        if let Some((left, right)) = expr.split_once("!=") {
133            let left_val = self.evaluate_value(left.trim())?;
134            let right_val = self.evaluate_value(right.trim())?;
135            return Ok(Some(left_val != right_val));
136        }
137
138        // Handle numeric comparison operators
139        for op in [">=", "<=", ">", "<"] {
140            if let Some((left, right)) = expr.split_once(op) {
141                let left_val = self.evaluate_value(left.trim())?;
142                let right_val = self.evaluate_value(right.trim())?;
143
144                // Try numeric comparison
145                if let (Some(a), Some(b)) = (
146                    left_val.as_f64().or_else(|| left_val.as_i64().map(|i| i as f64)),
147                    right_val.as_f64().or_else(|| right_val.as_i64().map(|i| i as f64)),
148                ) {
149                    let result = match op {
150                        ">=" => a >= b,
151                        "<=" => a <= b,
152                        ">" => a > b,
153                        "<" => a < b,
154                        _ => false,
155                    };
156                    return Ok(Some(result));
157                }
158            }
159        }
160
161        Ok(None)
162    }
163
164    /// Evaluate logical expressions (&&, ||, !)
165    fn evaluate_logical(&self, expr: &str) -> ConditionResult<Option<bool>> {
166        // Handle NOT operator
167        if let Some(stripped) = expr.strip_prefix('!') {
168            let inner = stripped.trim();
169            let inner_result = self.evaluate(inner)?;
170            return Ok(Some(!inner_result));
171        }
172
173        // Handle AND operator
174        if let Some((left, right)) = expr.split_once("&&") {
175            let left_result = self.evaluate(left.trim())?;
176            if !left_result {
177                return Ok(Some(false));
178            }
179            return Ok(Some(self.evaluate(right.trim())?));
180        }
181
182        // Handle OR operator
183        if let Some((left, right)) = expr.split_once("||") {
184            let left_result = self.evaluate(left.trim())?;
185            if left_result {
186                return Ok(Some(true));
187            }
188            return Ok(Some(self.evaluate(right.trim())?));
189        }
190
191        Ok(None)
192    }
193
194    /// Evaluate a value expression (variable, literal, etc.)
195    fn evaluate_value(&self, expr: &str) -> ConditionResult<Value> {
196        // Try to get variable value
197        if let Some(value) = self.get_variable_value(expr)? {
198            return Ok(value.clone());
199        }
200
201        // Try to parse as JSON value
202        if let Ok(value) = serde_json::from_str::<Value>(expr) {
203            return Ok(value);
204        }
205
206        // Try to parse as number
207        if let Ok(num) = expr.parse::<f64>() {
208            return Ok(Value::Number(
209                serde_json::Number::from_f64(num).unwrap_or_else(|| serde_json::Number::from(0)),
210            ));
211        }
212
213        // Try to parse as boolean
214        if expr == "true" {
215            return Ok(Value::Bool(true));
216        }
217        if expr == "false" {
218            return Ok(Value::Bool(false));
219        }
220
221        // Return as string
222        Ok(Value::String(expr.to_string()))
223    }
224
225    /// Get variable value using dot notation (e.g., "state.status")
226    fn get_variable_value(&self, path: &str) -> ConditionResult<Option<Value>> {
227        let parts: Vec<&str> = path.split('.').collect();
228
229        if parts.is_empty() {
230            return Ok(None);
231        }
232
233        // Get root variable
234        let root = self.context.get(parts[0]);
235        if root.is_none() {
236            return Ok(None);
237        }
238
239        let mut value = root.unwrap().clone();
240
241        // Navigate through nested properties
242        for part in parts.iter().skip(1) {
243            match value {
244                Value::Object(ref obj) => {
245                    value = obj
246                        .get(*part)
247                        .ok_or_else(|| {
248                            ConditionError::VariableNotFound(format!("{}.{}", parts[0], part))
249                        })?
250                        .clone();
251                }
252                _ => {
253                    return Err(ConditionError::TypeError(format!(
254                        "Cannot access property '{}' on non-object",
255                        part
256                    )));
257                }
258            }
259        }
260
261        Ok(Some(value))
262    }
263
264    /// Convert a JSON value to boolean
265    fn value_to_bool(&self, value: &Value) -> ConditionResult<bool> {
266        match value {
267            Value::Bool(b) => Ok(*b),
268            Value::Number(n) => Ok(n.as_f64().unwrap_or(0.0) != 0.0),
269            Value::String(s) => Ok(!s.is_empty()),
270            Value::Array(arr) => Ok(!arr.is_empty()),
271            Value::Object(obj) => Ok(!obj.is_empty()),
272            Value::Null => Ok(false),
273        }
274    }
275}
276
277impl Default for ConditionEvaluator {
278    fn default() -> Self {
279        Self::new()
280    }
281}
282
283#[cfg(test)]
284mod tests {
285    use super::*;
286
287    #[test]
288    fn test_simple_boolean() {
289        let evaluator = ConditionEvaluator::new();
290        assert!(evaluator.evaluate("true").unwrap());
291        assert!(!evaluator.evaluate("false").unwrap());
292    }
293
294    #[test]
295    fn test_comparison_operators() {
296        let evaluator = ConditionEvaluator::new();
297        assert!(evaluator.evaluate("5 > 3").unwrap());
298        assert!(evaluator.evaluate("3 < 5").unwrap());
299        assert!(evaluator.evaluate("5 == 5").unwrap());
300        assert!(evaluator.evaluate("5 != 3").unwrap());
301    }
302
303    #[test]
304    fn test_variable_access() {
305        let mut context = HashMap::new();
306        context.insert("status".to_string(), Value::String("active".to_string()));
307        context.insert("count".to_string(), Value::Number(5.into()));
308
309        let evaluator = ConditionEvaluator::with_context(context);
310        assert!(evaluator.evaluate("count > 3").unwrap());
311    }
312
313    #[test]
314    fn test_logical_operators() {
315        let evaluator = ConditionEvaluator::new();
316        assert!(evaluator.evaluate("true && true").unwrap());
317        assert!(!evaluator.evaluate("true && false").unwrap());
318        assert!(evaluator.evaluate("true || false").unwrap());
319        assert!(!evaluator.evaluate("!true").unwrap());
320    }
321}