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    /// Multi-field operation (CLIPS-inspired)
26    /// Examples:
27    /// - Order.items $?all_items (Collect)
28    /// - Product.tags contains "value" (Contains)
29    /// - Order.items count > 0 (Count)
30    /// - Queue.tasks first (First)
31    /// - Queue.tasks last (Last)
32    /// - ShoppingCart.items empty (IsEmpty)
33    MultiField {
34        /// Field name (e.g., "Order.items")
35        field: String,
36        /// Multi-field operation type
37        operation: String,  // "collect", "contains", "count", "first", "last", "empty", "not_empty"
38        /// Optional variable for binding (e.g., "$?all_items")
39        variable: Option<String>,
40    },
41}
42
43/// Represents a single condition in a rule
44#[derive(Debug, Clone)]
45pub struct Condition {
46    /// The expression to evaluate (field or function call)
47    pub expression: ConditionExpression,
48    /// The comparison operator to use
49    pub operator: Operator,
50    /// The value to compare against
51    pub value: Value,
52
53    // Keep field for backward compatibility
54    #[deprecated(note = "Use expression instead")]
55    #[doc(hidden)]
56    pub field: String,
57}
58
59impl Condition {
60    /// Create a new condition with a field reference
61    pub fn new(field: String, operator: Operator, value: Value) -> Self {
62        Self {
63            expression: ConditionExpression::Field(field.clone()),
64            operator,
65            value,
66            field, // Keep for backward compatibility
67        }
68    }
69
70    /// Create a new condition with a function call
71    pub fn with_function(
72        function_name: String,
73        args: Vec<String>,
74        operator: Operator,
75        value: Value,
76    ) -> Self {
77        Self {
78            expression: ConditionExpression::FunctionCall {
79                name: function_name.clone(),
80                args,
81            },
82            operator,
83            value,
84            field: function_name, // Use function name for backward compat
85        }
86    }
87
88    /// Create a new Test CE condition
89    /// The function must return a boolean value
90    pub fn with_test(function_name: String, args: Vec<String>) -> Self {
91        Self {
92            expression: ConditionExpression::Test {
93                name: function_name.clone(),
94                args,
95            },
96            operator: Operator::Equal, // Not used for Test CE
97            value: Value::Boolean(true), // Not used for Test CE
98            field: format!("test({})", function_name), // For backward compat
99        }
100    }
101
102    /// Create multi-field collect condition
103    /// Example: Order.items $?all_items
104    pub fn with_multifield_collect(field: String, variable: String) -> Self {
105        Self {
106            expression: ConditionExpression::MultiField {
107                field: field.clone(),
108                operation: "collect".to_string(),
109                variable: Some(variable),
110            },
111            operator: Operator::Equal, // Not used for MultiField
112            value: Value::Boolean(true), // Not used
113            field, // For backward compat
114        }
115    }
116
117    /// Create multi-field count condition
118    /// Example: Order.items count > 0
119    pub fn with_multifield_count(field: String, operator: Operator, value: Value) -> Self {
120        Self {
121            expression: ConditionExpression::MultiField {
122                field: field.clone(),
123                operation: "count".to_string(),
124                variable: None,
125            },
126            operator,
127            value,
128            field, // For backward compat
129        }
130    }
131
132    /// Create multi-field first condition
133    /// Example: Queue.tasks first $first_task
134    pub fn with_multifield_first(field: String, variable: Option<String>) -> Self {
135        Self {
136            expression: ConditionExpression::MultiField {
137                field: field.clone(),
138                operation: "first".to_string(),
139                variable,
140            },
141            operator: Operator::Equal, // Not used
142            value: Value::Boolean(true), // Not used
143            field, // For backward compat
144        }
145    }
146
147    /// Create multi-field last condition
148    /// Example: Queue.tasks last $last_task
149    pub fn with_multifield_last(field: String, variable: Option<String>) -> Self {
150        Self {
151            expression: ConditionExpression::MultiField {
152                field: field.clone(),
153                operation: "last".to_string(),
154                variable,
155            },
156            operator: Operator::Equal, // Not used
157            value: Value::Boolean(true), // Not used
158            field, // For backward compat
159        }
160    }
161
162    /// Create multi-field empty condition
163    /// Example: ShoppingCart.items empty
164    pub fn with_multifield_empty(field: String) -> Self {
165        Self {
166            expression: ConditionExpression::MultiField {
167                field: field.clone(),
168                operation: "empty".to_string(),
169                variable: None,
170            },
171            operator: Operator::Equal, // Not used
172            value: Value::Boolean(true), // Not used
173            field, // For backward compat
174        }
175    }
176
177    /// Create multi-field not_empty condition
178    /// Example: ShoppingCart.items not_empty
179    pub fn with_multifield_not_empty(field: String) -> Self {
180        Self {
181            expression: ConditionExpression::MultiField {
182                field: field.clone(),
183                operation: "not_empty".to_string(),
184                variable: None,
185            },
186            operator: Operator::Equal, // Not used
187            value: Value::Boolean(true), // Not used
188            field, // For backward compat
189        }
190    }
191
192    /// Evaluate this condition against the given facts
193    pub fn evaluate(&self, facts: &HashMap<String, Value>) -> bool {
194        match &self.expression {
195            ConditionExpression::Field(field_name) => {
196                // Get field value, or treat as Null if not found
197                let field_value = get_nested_value(facts, field_name)
198                    .cloned()
199                    .unwrap_or(Value::Null);
200                
201                self.operator.evaluate(&field_value, &self.value)
202            }
203            ConditionExpression::FunctionCall { .. }
204            | ConditionExpression::Test { .. }
205            | ConditionExpression::MultiField { .. } => {
206                // Function calls, Test CE, and MultiField need engine context
207                // Will be handled by evaluate_with_engine
208                false
209            }
210        }
211    }
212
213    /// Evaluate condition with access to engine's function registry
214    /// This is needed for function call evaluation
215    pub fn evaluate_with_engine(
216        &self,
217        facts: &HashMap<String, Value>,
218        function_registry: &HashMap<
219            String,
220            std::sync::Arc<dyn Fn(Vec<Value>, &HashMap<String, Value>) -> crate::errors::Result<Value> + Send + Sync>,
221        >,
222    ) -> bool {
223        match &self.expression {
224            ConditionExpression::Field(field_name) => {
225                // Get field value, or treat as Null if not found
226                let field_value = get_nested_value(facts, field_name)
227                    .cloned()
228                    .unwrap_or(Value::Null);
229                
230                self.operator.evaluate(&field_value, &self.value)
231            }
232            ConditionExpression::FunctionCall { name, args } => {
233                // Call the function with arguments
234                if let Some(function) = function_registry.get(name) {
235                    // Resolve arguments from facts
236                    let arg_values: Vec<Value> = args
237                        .iter()
238                        .map(|arg| {
239                            get_nested_value(facts, arg)
240                                .cloned()
241                                .unwrap_or(Value::String(arg.clone()))
242                        })
243                        .collect();
244
245                    // Call function
246                    if let Ok(result) = function(arg_values, facts) {
247                        // Compare function result with expected value
248                        return self.operator.evaluate(&result, &self.value);
249                    }
250                }
251                false
252            }
253            ConditionExpression::Test { name, args } => {
254                // Test CE: Call the function and expect boolean result
255                if let Some(function) = function_registry.get(name) {
256                    // Resolve arguments from facts
257                    let arg_values: Vec<Value> = args
258                        .iter()
259                        .map(|arg| {
260                            get_nested_value(facts, arg)
261                                .cloned()
262                                .unwrap_or(Value::String(arg.clone()))
263                        })
264                        .collect();
265
266                    // Call function
267                    if let Ok(result) = function(arg_values, facts) {
268                        // Test CE expects boolean result directly
269                        match result {
270                            Value::Boolean(b) => return b,
271                            Value::Integer(i) => return i != 0,
272                            Value::Number(f) => return f != 0.0,
273                            Value::String(s) => return !s.is_empty(),
274                            _ => return false,
275                        }
276                    }
277                }
278                false
279            }
280            ConditionExpression::MultiField { field, operation, variable: _ } => {
281                // MultiField operations for array/collection handling
282                if let Some(field_value) = get_nested_value(facts, field) {
283                    match operation.as_str() {
284                        "empty" => {
285                            // Check if array is empty
286                            matches!(field_value, Value::Array(arr) if arr.is_empty())
287                        }
288                        "not_empty" => {
289                            // Check if array is not empty
290                            matches!(field_value, Value::Array(arr) if !arr.is_empty())
291                        }
292                        "count" => {
293                            // Get count and compare with value
294                            if let Value::Array(arr) = field_value {
295                                let count = Value::Integer(arr.len() as i64);
296                                self.operator.evaluate(&count, &self.value)
297                            } else {
298                                false
299                            }
300                        }
301                        "first" => {
302                            // Get first element and compare with value
303                            if let Value::Array(arr) = field_value {
304                                if let Some(first) = arr.first() {
305                                    self.operator.evaluate(first, &self.value)
306                                } else {
307                                    false
308                                }
309                            } else {
310                                false
311                            }
312                        }
313                        "last" => {
314                            // Get last element and compare with value
315                            if let Value::Array(arr) = field_value {
316                                if let Some(last) = arr.last() {
317                                    self.operator.evaluate(last, &self.value)
318                                } else {
319                                    false
320                                }
321                            } else {
322                                false
323                            }
324                        }
325                        "contains" => {
326                            // Check if array contains the specified value
327                            if let Value::Array(arr) = field_value {
328                                arr.iter().any(|item| self.operator.evaluate(item, &self.value))
329                            } else {
330                                false
331                            }
332                        }
333                        "collect" => {
334                            // Collect operation: just check if array exists and has values
335                            // Variable binding happens in RETE engine context
336                            matches!(field_value, Value::Array(arr) if !arr.is_empty())
337                        }
338                        _ => {
339                            // Unknown operation
340                            false
341                        }
342                    }
343                } else {
344                    false
345                }
346            }
347        }
348    }
349}
350
351/// Group of conditions with logical operators
352#[derive(Debug, Clone)]
353pub enum ConditionGroup {
354    /// A single condition
355    Single(Condition),
356    /// A compound condition with two sub-conditions and a logical operator
357    Compound {
358        /// The left side condition
359        left: Box<ConditionGroup>,
360        /// The logical operator (AND, OR)
361        operator: LogicalOperator,
362        /// The right side condition
363        right: Box<ConditionGroup>,
364    },
365    /// A negated condition group
366    Not(Box<ConditionGroup>),
367    /// Pattern matching: check if at least one fact matches the condition
368    Exists(Box<ConditionGroup>),
369    /// Pattern matching: check if all facts of the target type match the condition
370    Forall(Box<ConditionGroup>),
371    /// Accumulate pattern: aggregate values from matching facts
372    /// Example: accumulate(Order($amount: amount, status == "completed"), sum($amount))
373    Accumulate {
374        /// Variable to bind the result to (e.g., "$total")
375        result_var: String,
376        /// Source pattern to match facts (e.g., "Order")
377        source_pattern: String,
378        /// Field to extract from matching facts (e.g., "$amount: amount")
379        extract_field: String,
380        /// Conditions on the source pattern
381        source_conditions: Vec<String>,
382        /// Accumulate function to apply (sum, avg, count, min, max)
383        function: String,
384        /// Variable passed to function (e.g., "$amount" in "sum($amount)")
385        function_arg: String,
386    },
387}
388
389impl ConditionGroup {
390    /// Create a single condition group
391    pub fn single(condition: Condition) -> Self {
392        ConditionGroup::Single(condition)
393    }
394
395    /// Create a compound condition using logical AND operator
396    pub fn and(left: ConditionGroup, right: ConditionGroup) -> Self {
397        ConditionGroup::Compound {
398            left: Box::new(left),
399            operator: LogicalOperator::And,
400            right: Box::new(right),
401        }
402    }
403
404    /// Create a compound condition using logical OR operator
405    pub fn or(left: ConditionGroup, right: ConditionGroup) -> Self {
406        ConditionGroup::Compound {
407            left: Box::new(left),
408            operator: LogicalOperator::Or,
409            right: Box::new(right),
410        }
411    }
412
413    /// Create a negated condition using logical NOT operator
414    #[allow(clippy::should_implement_trait)]
415    pub fn not(condition: ConditionGroup) -> Self {
416        ConditionGroup::Not(Box::new(condition))
417    }
418
419    /// Create an exists condition - checks if at least one fact matches
420    pub fn exists(condition: ConditionGroup) -> Self {
421        ConditionGroup::Exists(Box::new(condition))
422    }
423
424    /// Create a forall condition - checks if all facts of target type match
425    pub fn forall(condition: ConditionGroup) -> Self {
426        ConditionGroup::Forall(Box::new(condition))
427    }
428
429    /// Create an accumulate condition - aggregates values from matching facts
430    pub fn accumulate(
431        result_var: String,
432        source_pattern: String,
433        extract_field: String,
434        source_conditions: Vec<String>,
435        function: String,
436        function_arg: String,
437    ) -> Self {
438        ConditionGroup::Accumulate {
439            result_var,
440            source_pattern,
441            extract_field,
442            source_conditions,
443            function,
444            function_arg,
445        }
446    }
447
448    /// Evaluate this condition group against facts
449    pub fn evaluate(&self, facts: &HashMap<String, Value>) -> bool {
450        match self {
451            ConditionGroup::Single(condition) => condition.evaluate(facts),
452            ConditionGroup::Compound {
453                left,
454                operator,
455                right,
456            } => {
457                let left_result = left.evaluate(facts);
458                let right_result = right.evaluate(facts);
459                match operator {
460                    LogicalOperator::And => left_result && right_result,
461                    LogicalOperator::Or => left_result || right_result,
462                    LogicalOperator::Not => !left_result, // For Not, we ignore right side
463                }
464            }
465            ConditionGroup::Not(condition) => !condition.evaluate(facts),
466            ConditionGroup::Exists(_) | ConditionGroup::Forall(_) | ConditionGroup::Accumulate { .. } => {
467                // Pattern matching and accumulate conditions need Facts struct, not HashMap
468                // For now, return false - these will be handled by the engine
469                false
470            }
471        }
472    }
473
474    /// Evaluate this condition group against Facts (supports pattern matching)
475    pub fn evaluate_with_facts(&self, facts: &crate::engine::facts::Facts) -> bool {
476        use crate::engine::pattern_matcher::PatternMatcher;
477
478        match self {
479            ConditionGroup::Single(condition) => {
480                let fact_map = facts.get_all_facts();
481                condition.evaluate(&fact_map)
482            }
483            ConditionGroup::Compound {
484                left,
485                operator,
486                right,
487            } => {
488                let left_result = left.evaluate_with_facts(facts);
489                let right_result = right.evaluate_with_facts(facts);
490                match operator {
491                    LogicalOperator::And => left_result && right_result,
492                    LogicalOperator::Or => left_result || right_result,
493                    LogicalOperator::Not => !left_result,
494                }
495            }
496            ConditionGroup::Not(condition) => !condition.evaluate_with_facts(facts),
497            ConditionGroup::Exists(condition) => PatternMatcher::evaluate_exists(condition, facts),
498            ConditionGroup::Forall(condition) => PatternMatcher::evaluate_forall(condition, facts),
499            ConditionGroup::Accumulate { .. } => {
500                // Accumulate conditions need special handling - they will be evaluated
501                // during the engine execution phase, not here
502                // For now, return true to allow the rule to continue evaluation
503                true
504            }
505        }
506    }
507}
508
509/// A rule with conditions and actions
510#[derive(Debug, Clone)]
511pub struct Rule {
512    /// The unique name of the rule
513    pub name: String,
514    /// Optional description of what the rule does
515    pub description: Option<String>,
516    /// Priority of the rule (higher values execute first)
517    pub salience: i32,
518    /// Whether the rule is enabled for execution
519    pub enabled: bool,
520    /// Prevents the rule from activating itself in the same cycle
521    pub no_loop: bool,
522    /// Prevents the rule from firing again until agenda group changes
523    pub lock_on_active: bool,
524    /// Agenda group this rule belongs to (for workflow control)
525    pub agenda_group: Option<String>,
526    /// Activation group - only one rule in group can fire
527    pub activation_group: Option<String>,
528    /// Rule becomes effective from this date
529    pub date_effective: Option<DateTime<Utc>>,
530    /// Rule expires after this date
531    pub date_expires: Option<DateTime<Utc>>,
532    /// The conditions that must be met for the rule to fire
533    pub conditions: ConditionGroup,
534    /// The actions to execute when the rule fires
535    pub actions: Vec<ActionType>,
536}
537
538impl Rule {
539    /// Create a new rule with the given name, conditions, and actions
540    pub fn new(name: String, conditions: ConditionGroup, actions: Vec<ActionType>) -> Self {
541        Self {
542            name,
543            description: None,
544            salience: 0,
545            enabled: true,
546            no_loop: false,
547            lock_on_active: false,
548            agenda_group: None,
549            activation_group: None,
550            date_effective: None,
551            date_expires: None,
552            conditions,
553            actions,
554        }
555    }
556
557    /// Add a description to the rule
558    pub fn with_description(mut self, description: String) -> Self {
559        self.description = Some(description);
560        self
561    }
562
563    /// Set the salience (priority) of the rule
564    pub fn with_salience(mut self, salience: i32) -> Self {
565        self.salience = salience;
566        self
567    }
568
569    /// Set the priority of the rule (alias for salience)
570    pub fn with_priority(mut self, priority: i32) -> Self {
571        self.salience = priority;
572        self
573    }
574
575    /// Enable or disable no-loop behavior for this rule
576    pub fn with_no_loop(mut self, no_loop: bool) -> Self {
577        self.no_loop = no_loop;
578        self
579    }
580
581    /// Enable or disable lock-on-active behavior for this rule
582    pub fn with_lock_on_active(mut self, lock_on_active: bool) -> Self {
583        self.lock_on_active = lock_on_active;
584        self
585    }
586
587    /// Set the agenda group for this rule
588    pub fn with_agenda_group(mut self, agenda_group: String) -> Self {
589        self.agenda_group = Some(agenda_group);
590        self
591    }
592
593    /// Set the activation group for this rule
594    pub fn with_activation_group(mut self, activation_group: String) -> Self {
595        self.activation_group = Some(activation_group);
596        self
597    }
598
599    /// Set the effective date for this rule
600    pub fn with_date_effective(mut self, date_effective: DateTime<Utc>) -> Self {
601        self.date_effective = Some(date_effective);
602        self
603    }
604
605    /// Set the expiration date for this rule
606    pub fn with_date_expires(mut self, date_expires: DateTime<Utc>) -> Self {
607        self.date_expires = Some(date_expires);
608        self
609    }
610
611    /// Parse and set the effective date from ISO string
612    pub fn with_date_effective_str(mut self, date_str: &str) -> Result<Self, chrono::ParseError> {
613        let date = DateTime::parse_from_rfc3339(date_str)?.with_timezone(&Utc);
614        self.date_effective = Some(date);
615        Ok(self)
616    }
617
618    /// Parse and set the expiration date from ISO string
619    pub fn with_date_expires_str(mut self, date_str: &str) -> Result<Self, chrono::ParseError> {
620        let date = DateTime::parse_from_rfc3339(date_str)?.with_timezone(&Utc);
621        self.date_expires = Some(date);
622        Ok(self)
623    }
624
625    /// Check if this rule is active at the given timestamp
626    pub fn is_active_at(&self, timestamp: DateTime<Utc>) -> bool {
627        // Check if rule is effective
628        if let Some(effective) = self.date_effective {
629            if timestamp < effective {
630                return false;
631            }
632        }
633
634        // Check if rule has expired
635        if let Some(expires) = self.date_expires {
636            if timestamp >= expires {
637                return false;
638            }
639        }
640
641        true
642    }
643
644    /// Check if this rule is currently active (using current time)
645    pub fn is_active(&self) -> bool {
646        self.is_active_at(Utc::now())
647    }
648
649    /// Check if this rule matches the given facts
650    pub fn matches(&self, facts: &HashMap<String, Value>) -> bool {
651        self.enabled && self.conditions.evaluate(facts)
652    }
653}
654
655/// Result of rule execution
656#[derive(Debug, Clone)]
657pub struct RuleExecutionResult {
658    /// The name of the rule that was executed
659    pub rule_name: String,
660    /// Whether the rule's conditions matched and it fired
661    pub matched: bool,
662    /// List of actions that were executed
663    pub actions_executed: Vec<String>,
664    /// Time taken to execute the rule in milliseconds
665    pub execution_time_ms: f64,
666}
667
668impl RuleExecutionResult {
669    /// Create a new rule execution result
670    pub fn new(rule_name: String) -> Self {
671        Self {
672            rule_name,
673            matched: false,
674            actions_executed: Vec::new(),
675            execution_time_ms: 0.0,
676        }
677    }
678
679    /// Mark the rule as matched
680    pub fn matched(mut self) -> Self {
681        self.matched = true;
682        self
683    }
684
685    /// Set the actions that were executed
686    pub fn with_actions(mut self, actions: Vec<String>) -> Self {
687        self.actions_executed = actions;
688        self
689    }
690
691    /// Set the execution time in milliseconds
692    pub fn with_execution_time(mut self, time_ms: f64) -> Self {
693        self.execution_time_ms = time_ms;
694        self
695    }
696}
697
698/// Helper function to get nested values from a HashMap
699fn get_nested_value<'a>(data: &'a HashMap<String, Value>, path: &str) -> Option<&'a Value> {
700    let parts: Vec<&str> = path.split('.').collect();
701    let mut current = data.get(parts[0])?;
702
703    for part in parts.iter().skip(1) {
704        match current {
705            Value::Object(obj) => {
706                current = obj.get(*part)?;
707            }
708            _ => return None,
709        }
710    }
711
712    Some(current)
713}