rust_rule_engine/engine/
rule.rs

1use crate::types::{ActionType, LogicalOperator, Operator, Value};
2use chrono::{DateTime, Utc};
3use std::collections::HashMap;
4
5/// Expression in a condition - can be a field reference or function call
6#[derive(Debug, Clone)]
7pub enum ConditionExpression {
8    /// Direct field reference (e.g., User.age)
9    Field(String),
10    /// Function call with arguments (e.g., aiSentiment(User.text))
11    FunctionCall {
12        /// Function name
13        name: String,
14        /// Function arguments (field names or literal values)
15        args: Vec<String>,
16    },
17    /// Test CE - arbitrary expression that evaluates to boolean (CLIPS feature)
18    /// Example: test(calculate_discount(Order.amount) > 10.0)
19    Test {
20        /// Function name for the test
21        name: String,
22        /// Function arguments
23        args: Vec<String>,
24    },
25}
26
27/// Represents a single condition in a rule
28#[derive(Debug, Clone)]
29pub struct Condition {
30    /// The expression to evaluate (field or function call)
31    pub expression: ConditionExpression,
32    /// The comparison operator to use
33    pub operator: Operator,
34    /// The value to compare against
35    pub value: Value,
36
37    // Keep field for backward compatibility
38    #[deprecated(note = "Use expression instead")]
39    #[doc(hidden)]
40    pub field: String,
41}
42
43impl Condition {
44    /// Create a new condition with a field reference
45    pub fn new(field: String, operator: Operator, value: Value) -> Self {
46        Self {
47            expression: ConditionExpression::Field(field.clone()),
48            operator,
49            value,
50            field, // Keep for backward compatibility
51        }
52    }
53
54    /// Create a new condition with a function call
55    pub fn with_function(
56        function_name: String,
57        args: Vec<String>,
58        operator: Operator,
59        value: Value,
60    ) -> Self {
61        Self {
62            expression: ConditionExpression::FunctionCall {
63                name: function_name.clone(),
64                args,
65            },
66            operator,
67            value,
68            field: function_name, // Use function name for backward compat
69        }
70    }
71
72    /// Create a new Test CE condition
73    /// The function must return a boolean value
74    pub fn with_test(function_name: String, args: Vec<String>) -> Self {
75        Self {
76            expression: ConditionExpression::Test {
77                name: function_name.clone(),
78                args,
79            },
80            operator: Operator::Equal, // Not used for Test CE
81            value: Value::Boolean(true), // Not used for Test CE
82            field: format!("test({})", function_name), // For backward compat
83        }
84    }
85
86    /// Evaluate this condition against the given facts
87    pub fn evaluate(&self, facts: &HashMap<String, Value>) -> bool {
88        match &self.expression {
89            ConditionExpression::Field(field_name) => {
90                if let Some(field_value) = get_nested_value(facts, field_name) {
91                    self.operator.evaluate(field_value, &self.value)
92                } else {
93                    false
94                }
95            }
96            ConditionExpression::FunctionCall { .. } | ConditionExpression::Test { .. } => {
97                // Function calls and Test CE need engine context - will be handled by evaluate_with_engine
98                false
99            }
100        }
101    }
102
103    /// Evaluate condition with access to engine's function registry
104    /// This is needed for function call evaluation
105    pub fn evaluate_with_engine(
106        &self,
107        facts: &HashMap<String, Value>,
108        function_registry: &HashMap<
109            String,
110            std::sync::Arc<dyn Fn(Vec<Value>, &HashMap<String, Value>) -> crate::errors::Result<Value> + Send + Sync>,
111        >,
112    ) -> bool {
113        match &self.expression {
114            ConditionExpression::Field(field_name) => {
115                if let Some(field_value) = get_nested_value(facts, field_name) {
116                    self.operator.evaluate(field_value, &self.value)
117                } else {
118                    false
119                }
120            }
121            ConditionExpression::FunctionCall { name, args } => {
122                // Call the function with arguments
123                if let Some(function) = function_registry.get(name) {
124                    // Resolve arguments from facts
125                    let arg_values: Vec<Value> = args
126                        .iter()
127                        .map(|arg| {
128                            get_nested_value(facts, arg)
129                                .cloned()
130                                .unwrap_or(Value::String(arg.clone()))
131                        })
132                        .collect();
133
134                    // Call function
135                    if let Ok(result) = function(arg_values, facts) {
136                        // Compare function result with expected value
137                        return self.operator.evaluate(&result, &self.value);
138                    }
139                }
140                false
141            }
142            ConditionExpression::Test { name, args } => {
143                // Test CE: Call the function and expect boolean result
144                if let Some(function) = function_registry.get(name) {
145                    // Resolve arguments from facts
146                    let arg_values: Vec<Value> = args
147                        .iter()
148                        .map(|arg| {
149                            get_nested_value(facts, arg)
150                                .cloned()
151                                .unwrap_or(Value::String(arg.clone()))
152                        })
153                        .collect();
154
155                    // Call function
156                    if let Ok(result) = function(arg_values, facts) {
157                        // Test CE expects boolean result directly
158                        match result {
159                            Value::Boolean(b) => return b,
160                            Value::Integer(i) => return i != 0,
161                            Value::Number(f) => return f != 0.0,
162                            Value::String(s) => return !s.is_empty(),
163                            _ => return false,
164                        }
165                    }
166                }
167                false
168            }
169        }
170    }
171}
172
173/// Group of conditions with logical operators
174#[derive(Debug, Clone)]
175pub enum ConditionGroup {
176    /// A single condition
177    Single(Condition),
178    /// A compound condition with two sub-conditions and a logical operator
179    Compound {
180        /// The left side condition
181        left: Box<ConditionGroup>,
182        /// The logical operator (AND, OR)
183        operator: LogicalOperator,
184        /// The right side condition
185        right: Box<ConditionGroup>,
186    },
187    /// A negated condition group
188    Not(Box<ConditionGroup>),
189    /// Pattern matching: check if at least one fact matches the condition
190    Exists(Box<ConditionGroup>),
191    /// Pattern matching: check if all facts of the target type match the condition
192    Forall(Box<ConditionGroup>),
193    /// Accumulate pattern: aggregate values from matching facts
194    /// Example: accumulate(Order($amount: amount, status == "completed"), sum($amount))
195    Accumulate {
196        /// Variable to bind the result to (e.g., "$total")
197        result_var: String,
198        /// Source pattern to match facts (e.g., "Order")
199        source_pattern: String,
200        /// Field to extract from matching facts (e.g., "$amount: amount")
201        extract_field: String,
202        /// Conditions on the source pattern
203        source_conditions: Vec<String>,
204        /// Accumulate function to apply (sum, avg, count, min, max)
205        function: String,
206        /// Variable passed to function (e.g., "$amount" in "sum($amount)")
207        function_arg: String,
208    },
209}
210
211impl ConditionGroup {
212    /// Create a single condition group
213    pub fn single(condition: Condition) -> Self {
214        ConditionGroup::Single(condition)
215    }
216
217    /// Create a compound condition using logical AND operator
218    pub fn and(left: ConditionGroup, right: ConditionGroup) -> Self {
219        ConditionGroup::Compound {
220            left: Box::new(left),
221            operator: LogicalOperator::And,
222            right: Box::new(right),
223        }
224    }
225
226    /// Create a compound condition using logical OR operator
227    pub fn or(left: ConditionGroup, right: ConditionGroup) -> Self {
228        ConditionGroup::Compound {
229            left: Box::new(left),
230            operator: LogicalOperator::Or,
231            right: Box::new(right),
232        }
233    }
234
235    /// Create a negated condition using logical NOT operator
236    #[allow(clippy::should_implement_trait)]
237    pub fn not(condition: ConditionGroup) -> Self {
238        ConditionGroup::Not(Box::new(condition))
239    }
240
241    /// Create an exists condition - checks if at least one fact matches
242    pub fn exists(condition: ConditionGroup) -> Self {
243        ConditionGroup::Exists(Box::new(condition))
244    }
245
246    /// Create a forall condition - checks if all facts of target type match
247    pub fn forall(condition: ConditionGroup) -> Self {
248        ConditionGroup::Forall(Box::new(condition))
249    }
250
251    /// Create an accumulate condition - aggregates values from matching facts
252    pub fn accumulate(
253        result_var: String,
254        source_pattern: String,
255        extract_field: String,
256        source_conditions: Vec<String>,
257        function: String,
258        function_arg: String,
259    ) -> Self {
260        ConditionGroup::Accumulate {
261            result_var,
262            source_pattern,
263            extract_field,
264            source_conditions,
265            function,
266            function_arg,
267        }
268    }
269
270    /// Evaluate this condition group against facts
271    pub fn evaluate(&self, facts: &HashMap<String, Value>) -> bool {
272        match self {
273            ConditionGroup::Single(condition) => condition.evaluate(facts),
274            ConditionGroup::Compound {
275                left,
276                operator,
277                right,
278            } => {
279                let left_result = left.evaluate(facts);
280                let right_result = right.evaluate(facts);
281                match operator {
282                    LogicalOperator::And => left_result && right_result,
283                    LogicalOperator::Or => left_result || right_result,
284                    LogicalOperator::Not => !left_result, // For Not, we ignore right side
285                }
286            }
287            ConditionGroup::Not(condition) => !condition.evaluate(facts),
288            ConditionGroup::Exists(_) | ConditionGroup::Forall(_) | ConditionGroup::Accumulate { .. } => {
289                // Pattern matching and accumulate conditions need Facts struct, not HashMap
290                // For now, return false - these will be handled by the engine
291                false
292            }
293        }
294    }
295
296    /// Evaluate this condition group against Facts (supports pattern matching)
297    pub fn evaluate_with_facts(&self, facts: &crate::engine::facts::Facts) -> bool {
298        use crate::engine::pattern_matcher::PatternMatcher;
299
300        match self {
301            ConditionGroup::Single(condition) => {
302                let fact_map = facts.get_all_facts();
303                condition.evaluate(&fact_map)
304            }
305            ConditionGroup::Compound {
306                left,
307                operator,
308                right,
309            } => {
310                let left_result = left.evaluate_with_facts(facts);
311                let right_result = right.evaluate_with_facts(facts);
312                match operator {
313                    LogicalOperator::And => left_result && right_result,
314                    LogicalOperator::Or => left_result || right_result,
315                    LogicalOperator::Not => !left_result,
316                }
317            }
318            ConditionGroup::Not(condition) => !condition.evaluate_with_facts(facts),
319            ConditionGroup::Exists(condition) => PatternMatcher::evaluate_exists(condition, facts),
320            ConditionGroup::Forall(condition) => PatternMatcher::evaluate_forall(condition, facts),
321            ConditionGroup::Accumulate { .. } => {
322                // Accumulate conditions need special handling - they will be evaluated
323                // during the engine execution phase, not here
324                // For now, return true to allow the rule to continue evaluation
325                true
326            }
327        }
328    }
329}
330
331/// A rule with conditions and actions
332#[derive(Debug, Clone)]
333pub struct Rule {
334    /// The unique name of the rule
335    pub name: String,
336    /// Optional description of what the rule does
337    pub description: Option<String>,
338    /// Priority of the rule (higher values execute first)
339    pub salience: i32,
340    /// Whether the rule is enabled for execution
341    pub enabled: bool,
342    /// Prevents the rule from activating itself in the same cycle
343    pub no_loop: bool,
344    /// Prevents the rule from firing again until agenda group changes
345    pub lock_on_active: bool,
346    /// Agenda group this rule belongs to (for workflow control)
347    pub agenda_group: Option<String>,
348    /// Activation group - only one rule in group can fire
349    pub activation_group: Option<String>,
350    /// Rule becomes effective from this date
351    pub date_effective: Option<DateTime<Utc>>,
352    /// Rule expires after this date
353    pub date_expires: Option<DateTime<Utc>>,
354    /// The conditions that must be met for the rule to fire
355    pub conditions: ConditionGroup,
356    /// The actions to execute when the rule fires
357    pub actions: Vec<ActionType>,
358}
359
360impl Rule {
361    /// Create a new rule with the given name, conditions, and actions
362    pub fn new(name: String, conditions: ConditionGroup, actions: Vec<ActionType>) -> Self {
363        Self {
364            name,
365            description: None,
366            salience: 0,
367            enabled: true,
368            no_loop: false,
369            lock_on_active: false,
370            agenda_group: None,
371            activation_group: None,
372            date_effective: None,
373            date_expires: None,
374            conditions,
375            actions,
376        }
377    }
378
379    /// Add a description to the rule
380    pub fn with_description(mut self, description: String) -> Self {
381        self.description = Some(description);
382        self
383    }
384
385    /// Set the salience (priority) of the rule
386    pub fn with_salience(mut self, salience: i32) -> Self {
387        self.salience = salience;
388        self
389    }
390
391    /// Set the priority of the rule (alias for salience)
392    pub fn with_priority(mut self, priority: i32) -> Self {
393        self.salience = priority;
394        self
395    }
396
397    /// Enable or disable no-loop behavior for this rule
398    pub fn with_no_loop(mut self, no_loop: bool) -> Self {
399        self.no_loop = no_loop;
400        self
401    }
402
403    /// Enable or disable lock-on-active behavior for this rule
404    pub fn with_lock_on_active(mut self, lock_on_active: bool) -> Self {
405        self.lock_on_active = lock_on_active;
406        self
407    }
408
409    /// Set the agenda group for this rule
410    pub fn with_agenda_group(mut self, agenda_group: String) -> Self {
411        self.agenda_group = Some(agenda_group);
412        self
413    }
414
415    /// Set the activation group for this rule
416    pub fn with_activation_group(mut self, activation_group: String) -> Self {
417        self.activation_group = Some(activation_group);
418        self
419    }
420
421    /// Set the effective date for this rule
422    pub fn with_date_effective(mut self, date_effective: DateTime<Utc>) -> Self {
423        self.date_effective = Some(date_effective);
424        self
425    }
426
427    /// Set the expiration date for this rule
428    pub fn with_date_expires(mut self, date_expires: DateTime<Utc>) -> Self {
429        self.date_expires = Some(date_expires);
430        self
431    }
432
433    /// Parse and set the effective date from ISO string
434    pub fn with_date_effective_str(mut self, date_str: &str) -> Result<Self, chrono::ParseError> {
435        let date = DateTime::parse_from_rfc3339(date_str)?.with_timezone(&Utc);
436        self.date_effective = Some(date);
437        Ok(self)
438    }
439
440    /// Parse and set the expiration date from ISO string
441    pub fn with_date_expires_str(mut self, date_str: &str) -> Result<Self, chrono::ParseError> {
442        let date = DateTime::parse_from_rfc3339(date_str)?.with_timezone(&Utc);
443        self.date_expires = Some(date);
444        Ok(self)
445    }
446
447    /// Check if this rule is active at the given timestamp
448    pub fn is_active_at(&self, timestamp: DateTime<Utc>) -> bool {
449        // Check if rule is effective
450        if let Some(effective) = self.date_effective {
451            if timestamp < effective {
452                return false;
453            }
454        }
455
456        // Check if rule has expired
457        if let Some(expires) = self.date_expires {
458            if timestamp >= expires {
459                return false;
460            }
461        }
462
463        true
464    }
465
466    /// Check if this rule is currently active (using current time)
467    pub fn is_active(&self) -> bool {
468        self.is_active_at(Utc::now())
469    }
470
471    /// Check if this rule matches the given facts
472    pub fn matches(&self, facts: &HashMap<String, Value>) -> bool {
473        self.enabled && self.conditions.evaluate(facts)
474    }
475}
476
477/// Result of rule execution
478#[derive(Debug, Clone)]
479pub struct RuleExecutionResult {
480    /// The name of the rule that was executed
481    pub rule_name: String,
482    /// Whether the rule's conditions matched and it fired
483    pub matched: bool,
484    /// List of actions that were executed
485    pub actions_executed: Vec<String>,
486    /// Time taken to execute the rule in milliseconds
487    pub execution_time_ms: f64,
488}
489
490impl RuleExecutionResult {
491    /// Create a new rule execution result
492    pub fn new(rule_name: String) -> Self {
493        Self {
494            rule_name,
495            matched: false,
496            actions_executed: Vec::new(),
497            execution_time_ms: 0.0,
498        }
499    }
500
501    /// Mark the rule as matched
502    pub fn matched(mut self) -> Self {
503        self.matched = true;
504        self
505    }
506
507    /// Set the actions that were executed
508    pub fn with_actions(mut self, actions: Vec<String>) -> Self {
509        self.actions_executed = actions;
510        self
511    }
512
513    /// Set the execution time in milliseconds
514    pub fn with_execution_time(mut self, time_ms: f64) -> Self {
515        self.execution_time_ms = time_ms;
516        self
517    }
518}
519
520/// Helper function to get nested values from a HashMap
521fn get_nested_value<'a>(data: &'a HashMap<String, Value>, path: &str) -> Option<&'a Value> {
522    let parts: Vec<&str> = path.split('.').collect();
523    let mut current = data.get(parts[0])?;
524
525    for part in parts.iter().skip(1) {
526        match current {
527            Value::Object(obj) => {
528                current = obj.get(*part)?;
529            }
530            _ => return None,
531        }
532    }
533
534    Some(current)
535}