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}
54
55impl ConditionGroup {
56    /// Create a single condition group
57    pub fn single(condition: Condition) -> Self {
58        ConditionGroup::Single(condition)
59    }
60
61    /// Create a compound condition using logical AND operator
62    pub fn and(left: ConditionGroup, right: ConditionGroup) -> Self {
63        ConditionGroup::Compound {
64            left: Box::new(left),
65            operator: LogicalOperator::And,
66            right: Box::new(right),
67        }
68    }
69
70    /// Create a compound condition using logical OR operator
71    pub fn or(left: ConditionGroup, right: ConditionGroup) -> Self {
72        ConditionGroup::Compound {
73            left: Box::new(left),
74            operator: LogicalOperator::Or,
75            right: Box::new(right),
76        }
77    }
78
79    /// Create a negated condition using logical NOT operator
80    #[allow(clippy::should_implement_trait)]
81    pub fn not(condition: ConditionGroup) -> Self {
82        ConditionGroup::Not(Box::new(condition))
83    }
84
85    /// Evaluate this condition group against facts
86    pub fn evaluate(&self, facts: &HashMap<String, Value>) -> bool {
87        match self {
88            ConditionGroup::Single(condition) => condition.evaluate(facts),
89            ConditionGroup::Compound {
90                left,
91                operator,
92                right,
93            } => {
94                let left_result = left.evaluate(facts);
95                let right_result = right.evaluate(facts);
96                match operator {
97                    LogicalOperator::And => left_result && right_result,
98                    LogicalOperator::Or => left_result || right_result,
99                    LogicalOperator::Not => !left_result, // For Not, we ignore right side
100                }
101            }
102            ConditionGroup::Not(condition) => !condition.evaluate(facts),
103        }
104    }
105}
106
107/// A rule with conditions and actions
108#[derive(Debug, Clone)]
109pub struct Rule {
110    /// The unique name of the rule
111    pub name: String,
112    /// Optional description of what the rule does
113    pub description: Option<String>,
114    /// Priority of the rule (higher values execute first)
115    pub salience: i32,
116    /// Whether the rule is enabled for execution
117    pub enabled: bool,
118    /// Prevents the rule from activating itself in the same cycle
119    pub no_loop: bool,
120    /// Prevents the rule from firing again until agenda group changes
121    pub lock_on_active: bool,
122    /// Agenda group this rule belongs to (for workflow control)
123    pub agenda_group: Option<String>,
124    /// Activation group - only one rule in group can fire
125    pub activation_group: Option<String>,
126    /// Rule becomes effective from this date
127    pub date_effective: Option<DateTime<Utc>>,
128    /// Rule expires after this date
129    pub date_expires: Option<DateTime<Utc>>,
130    /// The conditions that must be met for the rule to fire
131    pub conditions: ConditionGroup,
132    /// The actions to execute when the rule fires
133    pub actions: Vec<ActionType>,
134}
135
136impl Rule {
137    /// Create a new rule with the given name, conditions, and actions
138    pub fn new(name: String, conditions: ConditionGroup, actions: Vec<ActionType>) -> Self {
139        Self {
140            name,
141            description: None,
142            salience: 0,
143            enabled: true,
144            no_loop: false,
145            lock_on_active: false,
146            agenda_group: None,
147            activation_group: None,
148            date_effective: None,
149            date_expires: None,
150            conditions,
151            actions,
152        }
153    }
154
155    /// Add a description to the rule
156    pub fn with_description(mut self, description: String) -> Self {
157        self.description = Some(description);
158        self
159    }
160
161    /// Set the salience (priority) of the rule
162    pub fn with_salience(mut self, salience: i32) -> Self {
163        self.salience = salience;
164        self
165    }
166
167    /// Set the priority of the rule (alias for salience)
168    pub fn with_priority(mut self, priority: i32) -> Self {
169        self.salience = priority;
170        self
171    }
172
173    /// Enable or disable no-loop behavior for this rule
174    pub fn with_no_loop(mut self, no_loop: bool) -> Self {
175        self.no_loop = no_loop;
176        self
177    }
178
179    /// Enable or disable lock-on-active behavior for this rule
180    pub fn with_lock_on_active(mut self, lock_on_active: bool) -> Self {
181        self.lock_on_active = lock_on_active;
182        self
183    }
184
185    /// Set the agenda group for this rule
186    pub fn with_agenda_group(mut self, agenda_group: String) -> Self {
187        self.agenda_group = Some(agenda_group);
188        self
189    }
190
191    /// Set the activation group for this rule
192    pub fn with_activation_group(mut self, activation_group: String) -> Self {
193        self.activation_group = Some(activation_group);
194        self
195    }
196
197    /// Set the effective date for this rule
198    pub fn with_date_effective(mut self, date_effective: DateTime<Utc>) -> Self {
199        self.date_effective = Some(date_effective);
200        self
201    }
202
203    /// Set the expiration date for this rule
204    pub fn with_date_expires(mut self, date_expires: DateTime<Utc>) -> Self {
205        self.date_expires = Some(date_expires);
206        self
207    }
208
209    /// Parse and set the effective date from ISO string
210    pub fn with_date_effective_str(mut self, date_str: &str) -> Result<Self, chrono::ParseError> {
211        let date = DateTime::parse_from_rfc3339(date_str)?.with_timezone(&Utc);
212        self.date_effective = Some(date);
213        Ok(self)
214    }
215
216    /// Parse and set the expiration date from ISO string
217    pub fn with_date_expires_str(mut self, date_str: &str) -> Result<Self, chrono::ParseError> {
218        let date = DateTime::parse_from_rfc3339(date_str)?.with_timezone(&Utc);
219        self.date_expires = Some(date);
220        Ok(self)
221    }
222
223    /// Check if this rule is active at the given timestamp
224    pub fn is_active_at(&self, timestamp: DateTime<Utc>) -> bool {
225        // Check if rule is effective
226        if let Some(effective) = self.date_effective {
227            if timestamp < effective {
228                return false;
229            }
230        }
231
232        // Check if rule has expired
233        if let Some(expires) = self.date_expires {
234            if timestamp >= expires {
235                return false;
236            }
237        }
238
239        true
240    }
241
242    /// Check if this rule is currently active (using current time)
243    pub fn is_active(&self) -> bool {
244        self.is_active_at(Utc::now())
245    }
246
247    /// Check if this rule matches the given facts
248    pub fn matches(&self, facts: &HashMap<String, Value>) -> bool {
249        self.enabled && self.conditions.evaluate(facts)
250    }
251}
252
253/// Result of rule execution
254#[derive(Debug, Clone)]
255pub struct RuleExecutionResult {
256    /// The name of the rule that was executed
257    pub rule_name: String,
258    /// Whether the rule's conditions matched and it fired
259    pub matched: bool,
260    /// List of actions that were executed
261    pub actions_executed: Vec<String>,
262    /// Time taken to execute the rule in milliseconds
263    pub execution_time_ms: f64,
264}
265
266impl RuleExecutionResult {
267    /// Create a new rule execution result
268    pub fn new(rule_name: String) -> Self {
269        Self {
270            rule_name,
271            matched: false,
272            actions_executed: Vec::new(),
273            execution_time_ms: 0.0,
274        }
275    }
276
277    /// Mark the rule as matched
278    pub fn matched(mut self) -> Self {
279        self.matched = true;
280        self
281    }
282
283    /// Set the actions that were executed
284    pub fn with_actions(mut self, actions: Vec<String>) -> Self {
285        self.actions_executed = actions;
286        self
287    }
288
289    /// Set the execution time in milliseconds
290    pub fn with_execution_time(mut self, time_ms: f64) -> Self {
291        self.execution_time_ms = time_ms;
292        self
293    }
294}
295
296/// Helper function to get nested values from a HashMap
297fn get_nested_value<'a>(data: &'a HashMap<String, Value>, path: &str) -> Option<&'a Value> {
298    let parts: Vec<&str> = path.split('.').collect();
299    let mut current = data.get(parts[0])?;
300
301    for part in parts.iter().skip(1) {
302        match current {
303            Value::Object(obj) => {
304                current = obj.get(*part)?;
305            }
306            _ => return None,
307        }
308    }
309
310    Some(current)
311}