1use crate::types::{ActionType, LogicalOperator, Operator, Value};
2use chrono::{DateTime, Utc};
3use std::collections::HashMap;
4
5#[derive(Debug, Clone)]
7pub enum ConditionExpression {
8 Field(String),
10 FunctionCall {
12 name: String,
14 args: Vec<String>,
16 },
17}
18
19#[derive(Debug, Clone)]
21pub struct Condition {
22 pub expression: ConditionExpression,
24 pub operator: Operator,
26 pub value: Value,
28
29 #[deprecated(note = "Use expression instead")]
31 #[doc(hidden)]
32 pub field: String,
33}
34
35impl Condition {
36 pub fn new(field: String, operator: Operator, value: Value) -> Self {
38 Self {
39 expression: ConditionExpression::Field(field.clone()),
40 operator,
41 value,
42 field, }
44 }
45
46 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, }
62 }
63
64 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 false
77 }
78 }
79 }
80
81 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 if let Some(function) = function_registry.get(name) {
102 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 if let Ok(result) = function(arg_values, facts) {
114 return self.operator.evaluate(&result, &self.value);
116 }
117 }
118 false
119 }
120 }
121 }
122}
123
124#[derive(Debug, Clone)]
126pub enum ConditionGroup {
127 Single(Condition),
129 Compound {
131 left: Box<ConditionGroup>,
133 operator: LogicalOperator,
135 right: Box<ConditionGroup>,
137 },
138 Not(Box<ConditionGroup>),
140 Exists(Box<ConditionGroup>),
142 Forall(Box<ConditionGroup>),
144}
145
146impl ConditionGroup {
147 pub fn single(condition: Condition) -> Self {
149 ConditionGroup::Single(condition)
150 }
151
152 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 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 #[allow(clippy::should_implement_trait)]
172 pub fn not(condition: ConditionGroup) -> Self {
173 ConditionGroup::Not(Box::new(condition))
174 }
175
176 pub fn exists(condition: ConditionGroup) -> Self {
178 ConditionGroup::Exists(Box::new(condition))
179 }
180
181 pub fn forall(condition: ConditionGroup) -> Self {
183 ConditionGroup::Forall(Box::new(condition))
184 }
185
186 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, }
202 }
203 ConditionGroup::Not(condition) => !condition.evaluate(facts),
204 ConditionGroup::Exists(_) | ConditionGroup::Forall(_) => {
205 false
208 }
209 }
210 }
211
212 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#[derive(Debug, Clone)]
243pub struct Rule {
244 pub name: String,
246 pub description: Option<String>,
248 pub salience: i32,
250 pub enabled: bool,
252 pub no_loop: bool,
254 pub lock_on_active: bool,
256 pub agenda_group: Option<String>,
258 pub activation_group: Option<String>,
260 pub date_effective: Option<DateTime<Utc>>,
262 pub date_expires: Option<DateTime<Utc>>,
264 pub conditions: ConditionGroup,
266 pub actions: Vec<ActionType>,
268}
269
270impl Rule {
271 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 pub fn with_description(mut self, description: String) -> Self {
291 self.description = Some(description);
292 self
293 }
294
295 pub fn with_salience(mut self, salience: i32) -> Self {
297 self.salience = salience;
298 self
299 }
300
301 pub fn with_priority(mut self, priority: i32) -> Self {
303 self.salience = priority;
304 self
305 }
306
307 pub fn with_no_loop(mut self, no_loop: bool) -> Self {
309 self.no_loop = no_loop;
310 self
311 }
312
313 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 pub fn with_agenda_group(mut self, agenda_group: String) -> Self {
321 self.agenda_group = Some(agenda_group);
322 self
323 }
324
325 pub fn with_activation_group(mut self, activation_group: String) -> Self {
327 self.activation_group = Some(activation_group);
328 self
329 }
330
331 pub fn with_date_effective(mut self, date_effective: DateTime<Utc>) -> Self {
333 self.date_effective = Some(date_effective);
334 self
335 }
336
337 pub fn with_date_expires(mut self, date_expires: DateTime<Utc>) -> Self {
339 self.date_expires = Some(date_expires);
340 self
341 }
342
343 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 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 pub fn is_active_at(&self, timestamp: DateTime<Utc>) -> bool {
359 if let Some(effective) = self.date_effective {
361 if timestamp < effective {
362 return false;
363 }
364 }
365
366 if let Some(expires) = self.date_expires {
368 if timestamp >= expires {
369 return false;
370 }
371 }
372
373 true
374 }
375
376 pub fn is_active(&self) -> bool {
378 self.is_active_at(Utc::now())
379 }
380
381 pub fn matches(&self, facts: &HashMap<String, Value>) -> bool {
383 self.enabled && self.conditions.evaluate(facts)
384 }
385}
386
387#[derive(Debug, Clone)]
389pub struct RuleExecutionResult {
390 pub rule_name: String,
392 pub matched: bool,
394 pub actions_executed: Vec<String>,
396 pub execution_time_ms: f64,
398}
399
400impl RuleExecutionResult {
401 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 pub fn matched(mut self) -> Self {
413 self.matched = true;
414 self
415 }
416
417 pub fn with_actions(mut self, actions: Vec<String>) -> Self {
419 self.actions_executed = actions;
420 self
421 }
422
423 pub fn with_execution_time(mut self, time_ms: f64) -> Self {
425 self.execution_time_ms = time_ms;
426 self
427 }
428}
429
430fn 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}