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