1use crate::types::{ActionType, LogicalOperator, Operator, Value};
2use chrono::{DateTime, Utc};
3use std::collections::HashMap;
4
5#[derive(Debug, Clone)]
7pub struct Condition {
8 pub field: String,
10 pub operator: Operator,
12 pub value: Value,
14}
15
16impl Condition {
17 pub fn new(field: String, operator: Operator, value: Value) -> Self {
19 Self {
20 field,
21 operator,
22 value,
23 }
24 }
25
26 pub fn evaluate(&self, facts: &HashMap<String, Value>) -> bool {
28 if let Some(field_value) = get_nested_value(facts, &self.field) {
29 self.operator.evaluate(field_value, &self.value)
31 } else {
32 false
33 }
34 }
35}
36
37#[derive(Debug, Clone)]
39pub enum ConditionGroup {
40 Single(Condition),
42 Compound {
44 left: Box<ConditionGroup>,
46 operator: LogicalOperator,
48 right: Box<ConditionGroup>,
50 },
51 Not(Box<ConditionGroup>),
53 Exists(Box<ConditionGroup>),
55 Forall(Box<ConditionGroup>),
57}
58
59impl ConditionGroup {
60 pub fn single(condition: Condition) -> Self {
62 ConditionGroup::Single(condition)
63 }
64
65 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 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 #[allow(clippy::should_implement_trait)]
85 pub fn not(condition: ConditionGroup) -> Self {
86 ConditionGroup::Not(Box::new(condition))
87 }
88
89 pub fn exists(condition: ConditionGroup) -> Self {
91 ConditionGroup::Exists(Box::new(condition))
92 }
93
94 pub fn forall(condition: ConditionGroup) -> Self {
96 ConditionGroup::Forall(Box::new(condition))
97 }
98
99 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, }
115 }
116 ConditionGroup::Not(condition) => !condition.evaluate(facts),
117 ConditionGroup::Exists(_) | ConditionGroup::Forall(_) => {
118 false
121 }
122 }
123 }
124
125 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#[derive(Debug, Clone)]
156pub struct Rule {
157 pub name: String,
159 pub description: Option<String>,
161 pub salience: i32,
163 pub enabled: bool,
165 pub no_loop: bool,
167 pub lock_on_active: bool,
169 pub agenda_group: Option<String>,
171 pub activation_group: Option<String>,
173 pub date_effective: Option<DateTime<Utc>>,
175 pub date_expires: Option<DateTime<Utc>>,
177 pub conditions: ConditionGroup,
179 pub actions: Vec<ActionType>,
181}
182
183impl Rule {
184 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 pub fn with_description(mut self, description: String) -> Self {
204 self.description = Some(description);
205 self
206 }
207
208 pub fn with_salience(mut self, salience: i32) -> Self {
210 self.salience = salience;
211 self
212 }
213
214 pub fn with_priority(mut self, priority: i32) -> Self {
216 self.salience = priority;
217 self
218 }
219
220 pub fn with_no_loop(mut self, no_loop: bool) -> Self {
222 self.no_loop = no_loop;
223 self
224 }
225
226 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 pub fn with_agenda_group(mut self, agenda_group: String) -> Self {
234 self.agenda_group = Some(agenda_group);
235 self
236 }
237
238 pub fn with_activation_group(mut self, activation_group: String) -> Self {
240 self.activation_group = Some(activation_group);
241 self
242 }
243
244 pub fn with_date_effective(mut self, date_effective: DateTime<Utc>) -> Self {
246 self.date_effective = Some(date_effective);
247 self
248 }
249
250 pub fn with_date_expires(mut self, date_expires: DateTime<Utc>) -> Self {
252 self.date_expires = Some(date_expires);
253 self
254 }
255
256 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 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 pub fn is_active_at(&self, timestamp: DateTime<Utc>) -> bool {
272 if let Some(effective) = self.date_effective {
274 if timestamp < effective {
275 return false;
276 }
277 }
278
279 if let Some(expires) = self.date_expires {
281 if timestamp >= expires {
282 return false;
283 }
284 }
285
286 true
287 }
288
289 pub fn is_active(&self) -> bool {
291 self.is_active_at(Utc::now())
292 }
293
294 pub fn matches(&self, facts: &HashMap<String, Value>) -> bool {
296 self.enabled && self.conditions.evaluate(facts)
297 }
298}
299
300#[derive(Debug, Clone)]
302pub struct RuleExecutionResult {
303 pub rule_name: String,
305 pub matched: bool,
307 pub actions_executed: Vec<String>,
309 pub execution_time_ms: f64,
311}
312
313impl RuleExecutionResult {
314 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 pub fn matched(mut self) -> Self {
326 self.matched = true;
327 self
328 }
329
330 pub fn with_actions(mut self, actions: Vec<String>) -> Self {
332 self.actions_executed = actions;
333 self
334 }
335
336 pub fn with_execution_time(mut self, time_ms: f64) -> Self {
338 self.execution_time_ms = time_ms;
339 self
340 }
341}
342
343fn 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}