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 need RETE engine context
280                // For now, just basic evaluation
281                // TODO: Full multifield support in RETE engine
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                        _ => {
302                            // Other operations (collect, first, last) need RETE context
303                            // Return true for now to not block rule evaluation
304                            true
305                        }
306                    }
307                } else {
308                    false
309                }
310            }
311        }
312    }
313}
314
315/// Group of conditions with logical operators
316#[derive(Debug, Clone)]
317pub enum ConditionGroup {
318    /// A single condition
319    Single(Condition),
320    /// A compound condition with two sub-conditions and a logical operator
321    Compound {
322        /// The left side condition
323        left: Box<ConditionGroup>,
324        /// The logical operator (AND, OR)
325        operator: LogicalOperator,
326        /// The right side condition
327        right: Box<ConditionGroup>,
328    },
329    /// A negated condition group
330    Not(Box<ConditionGroup>),
331    /// Pattern matching: check if at least one fact matches the condition
332    Exists(Box<ConditionGroup>),
333    /// Pattern matching: check if all facts of the target type match the condition
334    Forall(Box<ConditionGroup>),
335    /// Accumulate pattern: aggregate values from matching facts
336    /// Example: accumulate(Order($amount: amount, status == "completed"), sum($amount))
337    Accumulate {
338        /// Variable to bind the result to (e.g., "$total")
339        result_var: String,
340        /// Source pattern to match facts (e.g., "Order")
341        source_pattern: String,
342        /// Field to extract from matching facts (e.g., "$amount: amount")
343        extract_field: String,
344        /// Conditions on the source pattern
345        source_conditions: Vec<String>,
346        /// Accumulate function to apply (sum, avg, count, min, max)
347        function: String,
348        /// Variable passed to function (e.g., "$amount" in "sum($amount)")
349        function_arg: String,
350    },
351}
352
353impl ConditionGroup {
354    /// Create a single condition group
355    pub fn single(condition: Condition) -> Self {
356        ConditionGroup::Single(condition)
357    }
358
359    /// Create a compound condition using logical AND operator
360    pub fn and(left: ConditionGroup, right: ConditionGroup) -> Self {
361        ConditionGroup::Compound {
362            left: Box::new(left),
363            operator: LogicalOperator::And,
364            right: Box::new(right),
365        }
366    }
367
368    /// Create a compound condition using logical OR operator
369    pub fn or(left: ConditionGroup, right: ConditionGroup) -> Self {
370        ConditionGroup::Compound {
371            left: Box::new(left),
372            operator: LogicalOperator::Or,
373            right: Box::new(right),
374        }
375    }
376
377    /// Create a negated condition using logical NOT operator
378    #[allow(clippy::should_implement_trait)]
379    pub fn not(condition: ConditionGroup) -> Self {
380        ConditionGroup::Not(Box::new(condition))
381    }
382
383    /// Create an exists condition - checks if at least one fact matches
384    pub fn exists(condition: ConditionGroup) -> Self {
385        ConditionGroup::Exists(Box::new(condition))
386    }
387
388    /// Create a forall condition - checks if all facts of target type match
389    pub fn forall(condition: ConditionGroup) -> Self {
390        ConditionGroup::Forall(Box::new(condition))
391    }
392
393    /// Create an accumulate condition - aggregates values from matching facts
394    pub fn accumulate(
395        result_var: String,
396        source_pattern: String,
397        extract_field: String,
398        source_conditions: Vec<String>,
399        function: String,
400        function_arg: String,
401    ) -> Self {
402        ConditionGroup::Accumulate {
403            result_var,
404            source_pattern,
405            extract_field,
406            source_conditions,
407            function,
408            function_arg,
409        }
410    }
411
412    /// Evaluate this condition group against facts
413    pub fn evaluate(&self, facts: &HashMap<String, Value>) -> bool {
414        match self {
415            ConditionGroup::Single(condition) => condition.evaluate(facts),
416            ConditionGroup::Compound {
417                left,
418                operator,
419                right,
420            } => {
421                let left_result = left.evaluate(facts);
422                let right_result = right.evaluate(facts);
423                match operator {
424                    LogicalOperator::And => left_result && right_result,
425                    LogicalOperator::Or => left_result || right_result,
426                    LogicalOperator::Not => !left_result, // For Not, we ignore right side
427                }
428            }
429            ConditionGroup::Not(condition) => !condition.evaluate(facts),
430            ConditionGroup::Exists(_) | ConditionGroup::Forall(_) | ConditionGroup::Accumulate { .. } => {
431                // Pattern matching and accumulate conditions need Facts struct, not HashMap
432                // For now, return false - these will be handled by the engine
433                false
434            }
435        }
436    }
437
438    /// Evaluate this condition group against Facts (supports pattern matching)
439    pub fn evaluate_with_facts(&self, facts: &crate::engine::facts::Facts) -> bool {
440        use crate::engine::pattern_matcher::PatternMatcher;
441
442        match self {
443            ConditionGroup::Single(condition) => {
444                let fact_map = facts.get_all_facts();
445                condition.evaluate(&fact_map)
446            }
447            ConditionGroup::Compound {
448                left,
449                operator,
450                right,
451            } => {
452                let left_result = left.evaluate_with_facts(facts);
453                let right_result = right.evaluate_with_facts(facts);
454                match operator {
455                    LogicalOperator::And => left_result && right_result,
456                    LogicalOperator::Or => left_result || right_result,
457                    LogicalOperator::Not => !left_result,
458                }
459            }
460            ConditionGroup::Not(condition) => !condition.evaluate_with_facts(facts),
461            ConditionGroup::Exists(condition) => PatternMatcher::evaluate_exists(condition, facts),
462            ConditionGroup::Forall(condition) => PatternMatcher::evaluate_forall(condition, facts),
463            ConditionGroup::Accumulate { .. } => {
464                // Accumulate conditions need special handling - they will be evaluated
465                // during the engine execution phase, not here
466                // For now, return true to allow the rule to continue evaluation
467                true
468            }
469        }
470    }
471}
472
473/// A rule with conditions and actions
474#[derive(Debug, Clone)]
475pub struct Rule {
476    /// The unique name of the rule
477    pub name: String,
478    /// Optional description of what the rule does
479    pub description: Option<String>,
480    /// Priority of the rule (higher values execute first)
481    pub salience: i32,
482    /// Whether the rule is enabled for execution
483    pub enabled: bool,
484    /// Prevents the rule from activating itself in the same cycle
485    pub no_loop: bool,
486    /// Prevents the rule from firing again until agenda group changes
487    pub lock_on_active: bool,
488    /// Agenda group this rule belongs to (for workflow control)
489    pub agenda_group: Option<String>,
490    /// Activation group - only one rule in group can fire
491    pub activation_group: Option<String>,
492    /// Rule becomes effective from this date
493    pub date_effective: Option<DateTime<Utc>>,
494    /// Rule expires after this date
495    pub date_expires: Option<DateTime<Utc>>,
496    /// The conditions that must be met for the rule to fire
497    pub conditions: ConditionGroup,
498    /// The actions to execute when the rule fires
499    pub actions: Vec<ActionType>,
500}
501
502impl Rule {
503    /// Create a new rule with the given name, conditions, and actions
504    pub fn new(name: String, conditions: ConditionGroup, actions: Vec<ActionType>) -> Self {
505        Self {
506            name,
507            description: None,
508            salience: 0,
509            enabled: true,
510            no_loop: false,
511            lock_on_active: false,
512            agenda_group: None,
513            activation_group: None,
514            date_effective: None,
515            date_expires: None,
516            conditions,
517            actions,
518        }
519    }
520
521    /// Add a description to the rule
522    pub fn with_description(mut self, description: String) -> Self {
523        self.description = Some(description);
524        self
525    }
526
527    /// Set the salience (priority) of the rule
528    pub fn with_salience(mut self, salience: i32) -> Self {
529        self.salience = salience;
530        self
531    }
532
533    /// Set the priority of the rule (alias for salience)
534    pub fn with_priority(mut self, priority: i32) -> Self {
535        self.salience = priority;
536        self
537    }
538
539    /// Enable or disable no-loop behavior for this rule
540    pub fn with_no_loop(mut self, no_loop: bool) -> Self {
541        self.no_loop = no_loop;
542        self
543    }
544
545    /// Enable or disable lock-on-active behavior for this rule
546    pub fn with_lock_on_active(mut self, lock_on_active: bool) -> Self {
547        self.lock_on_active = lock_on_active;
548        self
549    }
550
551    /// Set the agenda group for this rule
552    pub fn with_agenda_group(mut self, agenda_group: String) -> Self {
553        self.agenda_group = Some(agenda_group);
554        self
555    }
556
557    /// Set the activation group for this rule
558    pub fn with_activation_group(mut self, activation_group: String) -> Self {
559        self.activation_group = Some(activation_group);
560        self
561    }
562
563    /// Set the effective date for this rule
564    pub fn with_date_effective(mut self, date_effective: DateTime<Utc>) -> Self {
565        self.date_effective = Some(date_effective);
566        self
567    }
568
569    /// Set the expiration date for this rule
570    pub fn with_date_expires(mut self, date_expires: DateTime<Utc>) -> Self {
571        self.date_expires = Some(date_expires);
572        self
573    }
574
575    /// Parse and set the effective date from ISO string
576    pub fn with_date_effective_str(mut self, date_str: &str) -> Result<Self, chrono::ParseError> {
577        let date = DateTime::parse_from_rfc3339(date_str)?.with_timezone(&Utc);
578        self.date_effective = Some(date);
579        Ok(self)
580    }
581
582    /// Parse and set the expiration date from ISO string
583    pub fn with_date_expires_str(mut self, date_str: &str) -> Result<Self, chrono::ParseError> {
584        let date = DateTime::parse_from_rfc3339(date_str)?.with_timezone(&Utc);
585        self.date_expires = Some(date);
586        Ok(self)
587    }
588
589    /// Check if this rule is active at the given timestamp
590    pub fn is_active_at(&self, timestamp: DateTime<Utc>) -> bool {
591        // Check if rule is effective
592        if let Some(effective) = self.date_effective {
593            if timestamp < effective {
594                return false;
595            }
596        }
597
598        // Check if rule has expired
599        if let Some(expires) = self.date_expires {
600            if timestamp >= expires {
601                return false;
602            }
603        }
604
605        true
606    }
607
608    /// Check if this rule is currently active (using current time)
609    pub fn is_active(&self) -> bool {
610        self.is_active_at(Utc::now())
611    }
612
613    /// Check if this rule matches the given facts
614    pub fn matches(&self, facts: &HashMap<String, Value>) -> bool {
615        self.enabled && self.conditions.evaluate(facts)
616    }
617}
618
619/// Result of rule execution
620#[derive(Debug, Clone)]
621pub struct RuleExecutionResult {
622    /// The name of the rule that was executed
623    pub rule_name: String,
624    /// Whether the rule's conditions matched and it fired
625    pub matched: bool,
626    /// List of actions that were executed
627    pub actions_executed: Vec<String>,
628    /// Time taken to execute the rule in milliseconds
629    pub execution_time_ms: f64,
630}
631
632impl RuleExecutionResult {
633    /// Create a new rule execution result
634    pub fn new(rule_name: String) -> Self {
635        Self {
636            rule_name,
637            matched: false,
638            actions_executed: Vec::new(),
639            execution_time_ms: 0.0,
640        }
641    }
642
643    /// Mark the rule as matched
644    pub fn matched(mut self) -> Self {
645        self.matched = true;
646        self
647    }
648
649    /// Set the actions that were executed
650    pub fn with_actions(mut self, actions: Vec<String>) -> Self {
651        self.actions_executed = actions;
652        self
653    }
654
655    /// Set the execution time in milliseconds
656    pub fn with_execution_time(mut self, time_ms: f64) -> Self {
657        self.execution_time_ms = time_ms;
658        self
659    }
660}
661
662/// Helper function to get nested values from a HashMap
663fn get_nested_value<'a>(data: &'a HashMap<String, Value>, path: &str) -> Option<&'a Value> {
664    let parts: Vec<&str> = path.split('.').collect();
665    let mut current = data.get(parts[0])?;
666
667    for part in parts.iter().skip(1) {
668        match current {
669            Value::Object(obj) => {
670                current = obj.get(*part)?;
671            }
672            _ => return None,
673        }
674    }
675
676    Some(current)
677}