rust_rule_engine/engine/
rule.rs

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