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