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