rust_rule_engine/engine/rule.rs
1#![allow(deprecated)]
2#![allow(clippy::type_complexity)]
3
4use crate::types::{ActionType, LogicalOperator, Operator, Value};
5use chrono::{DateTime, Utc};
6use std::collections::HashMap;
7
8/// Expression in a condition - can be a field reference or function call
9#[derive(Debug, Clone)]
10pub enum ConditionExpression {
11 /// Direct field reference (e.g., User.age)
12 Field(String),
13 /// Function call with arguments (e.g., aiSentiment(User.text))
14 FunctionCall {
15 /// Function name
16 name: String,
17 /// Function arguments (field names or literal values)
18 args: Vec<String>,
19 },
20 /// Test CE - arbitrary expression that evaluates to boolean (CLIPS feature)
21 /// Example: test(calculate_discount(Order.amount) > 10.0)
22 Test {
23 /// Function name for the test
24 name: String,
25 /// Function arguments
26 args: Vec<String>,
27 },
28 /// Multi-field operation (CLIPS-inspired)
29 /// Examples:
30 /// - Order.items $?all_items (Collect)
31 /// - Product.tags contains "value" (Contains)
32 /// - Order.items count > 0 (Count)
33 /// - Queue.tasks first (First)
34 /// - Queue.tasks last (Last)
35 /// - ShoppingCart.items empty (IsEmpty)
36 MultiField {
37 /// Field name (e.g., "Order.items")
38 field: String,
39 /// Multi-field operation type
40 operation: String, // "collect", "contains", "count", "first", "last", "empty", "not_empty"
41 /// Optional variable for binding (e.g., "$?all_items")
42 variable: Option<String>,
43 },
44}
45
46/// Represents a single condition in a rule
47#[derive(Debug, Clone)]
48pub struct Condition {
49 /// The expression to evaluate (field or function call)
50 pub expression: ConditionExpression,
51 /// The comparison operator to use
52 pub operator: Operator,
53 /// The value to compare against
54 pub value: Value,
55
56 // Keep field for backward compatibility
57 #[deprecated(note = "Use expression instead")]
58 #[doc(hidden)]
59 pub field: String,
60}
61
62impl Condition {
63 /// Create a new condition with a field reference
64 pub fn new(field: String, operator: Operator, value: Value) -> Self {
65 Self {
66 expression: ConditionExpression::Field(field.clone()),
67 operator,
68 value,
69 field, // Keep for backward compatibility
70 }
71 }
72
73 /// Create a new condition with a function call
74 pub fn with_function(
75 function_name: String,
76 args: Vec<String>,
77 operator: Operator,
78 value: Value,
79 ) -> Self {
80 Self {
81 expression: ConditionExpression::FunctionCall {
82 name: function_name.clone(),
83 args,
84 },
85 operator,
86 value,
87 field: function_name, // Use function name for backward compat
88 }
89 }
90
91 /// Create a new Test CE condition
92 /// The function must return a boolean value
93 pub fn with_test(function_name: String, args: Vec<String>) -> Self {
94 Self {
95 expression: ConditionExpression::Test {
96 name: function_name.clone(),
97 args,
98 },
99 operator: Operator::Equal, // Not used for Test CE
100 value: Value::Boolean(true), // Not used for Test CE
101 field: format!("test({})", function_name), // For backward compat
102 }
103 }
104
105 /// Create multi-field collect condition
106 /// Example: Order.items $?all_items
107 pub fn with_multifield_collect(field: String, variable: String) -> Self {
108 Self {
109 expression: ConditionExpression::MultiField {
110 field: field.clone(),
111 operation: "collect".to_string(),
112 variable: Some(variable),
113 },
114 operator: Operator::Equal, // Not used for MultiField
115 value: Value::Boolean(true), // Not used
116 field, // For backward compat
117 }
118 }
119
120 /// Create multi-field count condition
121 /// Example: Order.items count > 0
122 pub fn with_multifield_count(field: String, operator: Operator, value: Value) -> Self {
123 Self {
124 expression: ConditionExpression::MultiField {
125 field: field.clone(),
126 operation: "count".to_string(),
127 variable: None,
128 },
129 operator,
130 value,
131 field, // For backward compat
132 }
133 }
134
135 /// Create multi-field first condition
136 /// Example: Queue.tasks first $first_task
137 pub fn with_multifield_first(field: String, variable: Option<String>) -> Self {
138 Self {
139 expression: ConditionExpression::MultiField {
140 field: field.clone(),
141 operation: "first".to_string(),
142 variable,
143 },
144 operator: Operator::Equal, // Not used
145 value: Value::Boolean(true), // Not used
146 field, // For backward compat
147 }
148 }
149
150 /// Create multi-field last condition
151 /// Example: Queue.tasks last $last_task
152 pub fn with_multifield_last(field: String, variable: Option<String>) -> Self {
153 Self {
154 expression: ConditionExpression::MultiField {
155 field: field.clone(),
156 operation: "last".to_string(),
157 variable,
158 },
159 operator: Operator::Equal, // Not used
160 value: Value::Boolean(true), // Not used
161 field, // For backward compat
162 }
163 }
164
165 /// Create multi-field empty condition
166 /// Example: ShoppingCart.items empty
167 pub fn with_multifield_empty(field: String) -> Self {
168 Self {
169 expression: ConditionExpression::MultiField {
170 field: field.clone(),
171 operation: "empty".to_string(),
172 variable: None,
173 },
174 operator: Operator::Equal, // Not used
175 value: Value::Boolean(true), // Not used
176 field, // For backward compat
177 }
178 }
179
180 /// Create multi-field not_empty condition
181 /// Example: ShoppingCart.items not_empty
182 pub fn with_multifield_not_empty(field: String) -> Self {
183 Self {
184 expression: ConditionExpression::MultiField {
185 field: field.clone(),
186 operation: "not_empty".to_string(),
187 variable: None,
188 },
189 operator: Operator::Equal, // Not used
190 value: Value::Boolean(true), // Not used
191 field, // For backward compat
192 }
193 }
194
195 /// Evaluate this condition against the given facts
196 pub fn evaluate(&self, facts: &HashMap<String, Value>) -> bool {
197 match &self.expression {
198 ConditionExpression::Field(field_name) => {
199 // Get field value, or treat as Null if not found
200 let field_value = get_nested_value(facts, field_name)
201 .cloned()
202 .unwrap_or(Value::Null);
203
204 self.operator.evaluate(&field_value, &self.value)
205 }
206 ConditionExpression::FunctionCall { .. }
207 | ConditionExpression::Test { .. }
208 | ConditionExpression::MultiField { .. } => {
209 // Function calls, Test CE, and MultiField need engine context
210 // Will be handled by evaluate_with_engine
211 false
212 }
213 }
214 }
215
216 /// Evaluate condition with access to engine's function registry
217 /// This is needed for function call evaluation
218 pub fn evaluate_with_engine(
219 &self,
220 facts: &HashMap<String, Value>,
221 function_registry: &HashMap<
222 String,
223 std::sync::Arc<
224 dyn Fn(Vec<Value>, &HashMap<String, Value>) -> crate::errors::Result<Value>
225 + Send
226 + Sync,
227 >,
228 >,
229 ) -> bool {
230 match &self.expression {
231 ConditionExpression::Field(field_name) => {
232 // Get field value, or treat as Null if not found
233 let field_value = get_nested_value(facts, field_name)
234 .cloned()
235 .unwrap_or(Value::Null);
236
237 self.operator.evaluate(&field_value, &self.value)
238 }
239 ConditionExpression::FunctionCall { name, args } => {
240 // Call the function with arguments
241 if let Some(function) = function_registry.get(name) {
242 // Resolve arguments from facts
243 let arg_values: Vec<Value> = args
244 .iter()
245 .map(|arg| {
246 get_nested_value(facts, arg)
247 .cloned()
248 .unwrap_or(Value::String(arg.clone()))
249 })
250 .collect();
251
252 // Call function
253 if let Ok(result) = function(arg_values, facts) {
254 // Compare function result with expected value
255 return self.operator.evaluate(&result, &self.value);
256 }
257 }
258 false
259 }
260 ConditionExpression::Test { name, args } => {
261 // Test CE: Call the function and expect boolean result
262 if let Some(function) = function_registry.get(name) {
263 // Resolve arguments from facts
264 let arg_values: Vec<Value> = args
265 .iter()
266 .map(|arg| {
267 get_nested_value(facts, arg)
268 .cloned()
269 .unwrap_or(Value::String(arg.clone()))
270 })
271 .collect();
272
273 // Call function
274 if let Ok(result) = function(arg_values, facts) {
275 // Test CE expects boolean result directly
276 match result {
277 Value::Boolean(b) => return b,
278 Value::Integer(i) => return i != 0,
279 Value::Number(f) => return f != 0.0,
280 Value::String(s) => return !s.is_empty(),
281 _ => return false,
282 }
283 }
284 }
285 false
286 }
287 ConditionExpression::MultiField {
288 field,
289 operation,
290 variable: _,
291 } => {
292 // MultiField operations for array/collection handling
293 if let Some(field_value) = get_nested_value(facts, field) {
294 match operation.as_str() {
295 "empty" => {
296 // Check if array is empty
297 matches!(field_value, Value::Array(arr) if arr.is_empty())
298 }
299 "not_empty" => {
300 // Check if array is not empty
301 matches!(field_value, Value::Array(arr) if !arr.is_empty())
302 }
303 "count" => {
304 // Get count and compare with value
305 if let Value::Array(arr) = field_value {
306 let count = Value::Integer(arr.len() as i64);
307 self.operator.evaluate(&count, &self.value)
308 } else {
309 false
310 }
311 }
312 "first" => {
313 // Get first element and compare with value
314 if let Value::Array(arr) = field_value {
315 if let Some(first) = arr.first() {
316 self.operator.evaluate(first, &self.value)
317 } else {
318 false
319 }
320 } else {
321 false
322 }
323 }
324 "last" => {
325 // Get last element and compare with value
326 if let Value::Array(arr) = field_value {
327 if let Some(last) = arr.last() {
328 self.operator.evaluate(last, &self.value)
329 } else {
330 false
331 }
332 } else {
333 false
334 }
335 }
336 "contains" => {
337 // Check if array contains the specified value
338 if let Value::Array(arr) = field_value {
339 arr.iter()
340 .any(|item| self.operator.evaluate(item, &self.value))
341 } else {
342 false
343 }
344 }
345 "collect" => {
346 // Collect operation: just check if array exists and has values
347 // Variable binding happens in RETE engine context
348 matches!(field_value, Value::Array(arr) if !arr.is_empty())
349 }
350 _ => {
351 // Unknown operation
352 false
353 }
354 }
355 } else {
356 false
357 }
358 }
359 }
360 }
361}
362
363/// Group of conditions with logical operators
364#[derive(Debug, Clone)]
365pub enum ConditionGroup {
366 /// A single condition
367 Single(Condition),
368 /// A compound condition with two sub-conditions and a logical operator
369 Compound {
370 /// The left side condition
371 left: Box<ConditionGroup>,
372 /// The logical operator (AND, OR)
373 operator: LogicalOperator,
374 /// The right side condition
375 right: Box<ConditionGroup>,
376 },
377 /// A negated condition group
378 Not(Box<ConditionGroup>),
379 /// Pattern matching: check if at least one fact matches the condition
380 Exists(Box<ConditionGroup>),
381 /// Pattern matching: check if all facts of the target type match the condition
382 Forall(Box<ConditionGroup>),
383 /// Accumulate pattern: aggregate values from matching facts
384 /// Example: accumulate(Order($amount: amount, status == "completed"), sum($amount))
385 Accumulate {
386 /// Variable to bind the result to (e.g., "$total")
387 result_var: String,
388 /// Source pattern to match facts (e.g., "Order")
389 source_pattern: String,
390 /// Field to extract from matching facts (e.g., "$amount: amount")
391 extract_field: String,
392 /// Conditions on the source pattern
393 source_conditions: Vec<String>,
394 /// Accumulate function to apply (sum, avg, count, min, max)
395 function: String,
396 /// Variable passed to function (e.g., "$amount" in "sum($amount)")
397 function_arg: String,
398 },
399}
400
401impl ConditionGroup {
402 /// Create a single condition group
403 pub fn single(condition: Condition) -> Self {
404 ConditionGroup::Single(condition)
405 }
406
407 /// Create a compound condition using logical AND operator
408 pub fn and(left: ConditionGroup, right: ConditionGroup) -> Self {
409 ConditionGroup::Compound {
410 left: Box::new(left),
411 operator: LogicalOperator::And,
412 right: Box::new(right),
413 }
414 }
415
416 /// Create a compound condition using logical OR operator
417 pub fn or(left: ConditionGroup, right: ConditionGroup) -> Self {
418 ConditionGroup::Compound {
419 left: Box::new(left),
420 operator: LogicalOperator::Or,
421 right: Box::new(right),
422 }
423 }
424
425 /// Create a negated condition using logical NOT operator
426 #[allow(clippy::should_implement_trait)]
427 pub fn not(condition: ConditionGroup) -> Self {
428 ConditionGroup::Not(Box::new(condition))
429 }
430
431 /// Create an exists condition - checks if at least one fact matches
432 pub fn exists(condition: ConditionGroup) -> Self {
433 ConditionGroup::Exists(Box::new(condition))
434 }
435
436 /// Create a forall condition - checks if all facts of target type match
437 pub fn forall(condition: ConditionGroup) -> Self {
438 ConditionGroup::Forall(Box::new(condition))
439 }
440
441 /// Create an accumulate condition - aggregates values from matching facts
442 pub fn accumulate(
443 result_var: String,
444 source_pattern: String,
445 extract_field: String,
446 source_conditions: Vec<String>,
447 function: String,
448 function_arg: String,
449 ) -> Self {
450 ConditionGroup::Accumulate {
451 result_var,
452 source_pattern,
453 extract_field,
454 source_conditions,
455 function,
456 function_arg,
457 }
458 }
459
460 /// Evaluate this condition group against facts
461 pub fn evaluate(&self, facts: &HashMap<String, Value>) -> bool {
462 match self {
463 ConditionGroup::Single(condition) => condition.evaluate(facts),
464 ConditionGroup::Compound {
465 left,
466 operator,
467 right,
468 } => {
469 let left_result = left.evaluate(facts);
470 let right_result = right.evaluate(facts);
471 match operator {
472 LogicalOperator::And => left_result && right_result,
473 LogicalOperator::Or => left_result || right_result,
474 LogicalOperator::Not => !left_result, // For Not, we ignore right side
475 }
476 }
477 ConditionGroup::Not(condition) => !condition.evaluate(facts),
478 ConditionGroup::Exists(_)
479 | ConditionGroup::Forall(_)
480 | ConditionGroup::Accumulate { .. } => {
481 // Pattern matching and accumulate conditions need Facts struct, not HashMap
482 // For now, return false - these will be handled by the engine
483 false
484 }
485 }
486 }
487
488 /// Evaluate this condition group against Facts (supports pattern matching)
489 pub fn evaluate_with_facts(&self, facts: &crate::engine::facts::Facts) -> bool {
490 use crate::engine::pattern_matcher::PatternMatcher;
491
492 match self {
493 ConditionGroup::Single(condition) => {
494 let fact_map = facts.get_all_facts();
495 condition.evaluate(&fact_map)
496 }
497 ConditionGroup::Compound {
498 left,
499 operator,
500 right,
501 } => {
502 let left_result = left.evaluate_with_facts(facts);
503 let right_result = right.evaluate_with_facts(facts);
504 match operator {
505 LogicalOperator::And => left_result && right_result,
506 LogicalOperator::Or => left_result || right_result,
507 LogicalOperator::Not => !left_result,
508 }
509 }
510 ConditionGroup::Not(condition) => !condition.evaluate_with_facts(facts),
511 ConditionGroup::Exists(condition) => PatternMatcher::evaluate_exists(condition, facts),
512 ConditionGroup::Forall(condition) => PatternMatcher::evaluate_forall(condition, facts),
513 ConditionGroup::Accumulate { .. } => {
514 // Accumulate conditions need special handling - they will be evaluated
515 // during the engine execution phase, not here
516 // For now, return true to allow the rule to continue evaluation
517 true
518 }
519 }
520 }
521}
522
523/// A rule with conditions and actions
524#[derive(Debug, Clone)]
525pub struct Rule {
526 /// The unique name of the rule
527 pub name: String,
528 /// Optional description of what the rule does
529 pub description: Option<String>,
530 /// Priority of the rule (higher values execute first)
531 pub salience: i32,
532 /// Whether the rule is enabled for execution
533 pub enabled: bool,
534 /// Prevents the rule from activating itself in the same cycle
535 pub no_loop: bool,
536 /// Prevents the rule from firing again until agenda group changes
537 pub lock_on_active: bool,
538 /// Agenda group this rule belongs to (for workflow control)
539 pub agenda_group: Option<String>,
540 /// Activation group - only one rule in group can fire
541 pub activation_group: Option<String>,
542 /// Rule becomes effective from this date
543 pub date_effective: Option<DateTime<Utc>>,
544 /// Rule expires after this date
545 pub date_expires: Option<DateTime<Utc>>,
546 /// The conditions that must be met for the rule to fire
547 pub conditions: ConditionGroup,
548 /// The actions to execute when the rule fires
549 pub actions: Vec<ActionType>,
550}
551
552impl Rule {
553 /// Create a new rule with the given name, conditions, and actions
554 pub fn new(name: String, conditions: ConditionGroup, actions: Vec<ActionType>) -> Self {
555 Self {
556 name,
557 description: None,
558 salience: 0,
559 enabled: true,
560 no_loop: false,
561 lock_on_active: false,
562 agenda_group: None,
563 activation_group: None,
564 date_effective: None,
565 date_expires: None,
566 conditions,
567 actions,
568 }
569 }
570
571 /// Add a description to the rule
572 pub fn with_description(mut self, description: String) -> Self {
573 self.description = Some(description);
574 self
575 }
576
577 /// Set the salience (priority) of the rule
578 pub fn with_salience(mut self, salience: i32) -> Self {
579 self.salience = salience;
580 self
581 }
582
583 /// Set the priority of the rule (alias for salience)
584 pub fn with_priority(mut self, priority: i32) -> Self {
585 self.salience = priority;
586 self
587 }
588
589 /// Enable or disable no-loop behavior for this rule
590 pub fn with_no_loop(mut self, no_loop: bool) -> Self {
591 self.no_loop = no_loop;
592 self
593 }
594
595 /// Enable or disable lock-on-active behavior for this rule
596 pub fn with_lock_on_active(mut self, lock_on_active: bool) -> Self {
597 self.lock_on_active = lock_on_active;
598 self
599 }
600
601 /// Set the agenda group for this rule
602 pub fn with_agenda_group(mut self, agenda_group: String) -> Self {
603 self.agenda_group = Some(agenda_group);
604 self
605 }
606
607 /// Set the activation group for this rule
608 pub fn with_activation_group(mut self, activation_group: String) -> Self {
609 self.activation_group = Some(activation_group);
610 self
611 }
612
613 /// Set the effective date for this rule
614 pub fn with_date_effective(mut self, date_effective: DateTime<Utc>) -> Self {
615 self.date_effective = Some(date_effective);
616 self
617 }
618
619 /// Set the expiration date for this rule
620 pub fn with_date_expires(mut self, date_expires: DateTime<Utc>) -> Self {
621 self.date_expires = Some(date_expires);
622 self
623 }
624
625 /// Parse and set the effective date from ISO string
626 pub fn with_date_effective_str(mut self, date_str: &str) -> Result<Self, chrono::ParseError> {
627 let date = DateTime::parse_from_rfc3339(date_str)?.with_timezone(&Utc);
628 self.date_effective = Some(date);
629 Ok(self)
630 }
631
632 /// Parse and set the expiration date from ISO string
633 pub fn with_date_expires_str(mut self, date_str: &str) -> Result<Self, chrono::ParseError> {
634 let date = DateTime::parse_from_rfc3339(date_str)?.with_timezone(&Utc);
635 self.date_expires = Some(date);
636 Ok(self)
637 }
638
639 /// Check if this rule is active at the given timestamp
640 pub fn is_active_at(&self, timestamp: DateTime<Utc>) -> bool {
641 // Check if rule is effective
642 if let Some(effective) = self.date_effective {
643 if timestamp < effective {
644 return false;
645 }
646 }
647
648 // Check if rule has expired
649 if let Some(expires) = self.date_expires {
650 if timestamp >= expires {
651 return false;
652 }
653 }
654
655 true
656 }
657
658 /// Check if this rule is currently active (using current time)
659 pub fn is_active(&self) -> bool {
660 self.is_active_at(Utc::now())
661 }
662
663 /// Check if this rule matches the given facts
664 pub fn matches(&self, facts: &HashMap<String, Value>) -> bool {
665 self.enabled && self.conditions.evaluate(facts)
666 }
667}
668
669/// Result of rule execution
670#[derive(Debug, Clone)]
671pub struct RuleExecutionResult {
672 /// The name of the rule that was executed
673 pub rule_name: String,
674 /// Whether the rule's conditions matched and it fired
675 pub matched: bool,
676 /// List of actions that were executed
677 pub actions_executed: Vec<String>,
678 /// Time taken to execute the rule in milliseconds
679 pub execution_time_ms: f64,
680}
681
682impl RuleExecutionResult {
683 /// Create a new rule execution result
684 pub fn new(rule_name: String) -> Self {
685 Self {
686 rule_name,
687 matched: false,
688 actions_executed: Vec::new(),
689 execution_time_ms: 0.0,
690 }
691 }
692
693 /// Mark the rule as matched
694 pub fn matched(mut self) -> Self {
695 self.matched = true;
696 self
697 }
698
699 /// Set the actions that were executed
700 pub fn with_actions(mut self, actions: Vec<String>) -> Self {
701 self.actions_executed = actions;
702 self
703 }
704
705 /// Set the execution time in milliseconds
706 pub fn with_execution_time(mut self, time_ms: f64) -> Self {
707 self.execution_time_ms = time_ms;
708 self
709 }
710}
711
712/// Helper function to get nested values from a HashMap
713fn get_nested_value<'a>(data: &'a HashMap<String, Value>, path: &str) -> Option<&'a Value> {
714 let parts: Vec<&str> = path.split('.').collect();
715 let mut current = data.get(parts[0])?;
716
717 for part in parts.iter().skip(1) {
718 match current {
719 Value::Object(obj) => {
720 current = obj.get(*part)?;
721 }
722 _ => return None,
723 }
724 }
725
726 Some(current)
727}