rust_rule_engine/engine/
rule.rs

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