rust_rule_engine/engine/
rule.rs

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