rust_rule_engine/engine/
pattern_matcher.rs

1#![allow(deprecated)]
2
3use crate::engine::facts::Facts;
4use crate::engine::rule::ConditionGroup;
5use std::collections::HashMap;
6
7/// Pattern matching evaluator for advanced condition types
8pub struct PatternMatcher;
9
10impl PatternMatcher {
11    /// Evaluate EXISTS condition - checks if at least one fact matches the condition
12    pub fn evaluate_exists(condition: &ConditionGroup, facts: &Facts) -> bool {
13        let all_facts = facts.get_all_facts();
14
15        // For EXISTS, we need to check if ANY fact matches the condition
16        // We iterate through all facts and check if the condition matches any of them
17        for (fact_name, fact_value) in &all_facts {
18            // Extract the target type from the condition if it's a single condition
19            if let Some(target_type) = Self::extract_target_type(condition) {
20                // Check if fact name starts with target type (e.g., "Customer1" starts with "Customer")
21                if fact_name.starts_with(&target_type) {
22                    // Create a temporary fact context with the target type as key
23                    // This allows condition evaluation to work with "Customer.tier" syntax
24                    let mut temp_facts = HashMap::new();
25                    temp_facts.insert(target_type.clone(), fact_value.clone());
26
27                    // This fact matches the target type, evaluate the condition
28                    if condition.evaluate(&temp_facts) {
29                        return true;
30                    }
31                }
32            } else {
33                // For complex conditions, evaluate against all facts
34                if condition.evaluate(&all_facts) {
35                    return true;
36                }
37            }
38        }
39
40        false
41    }
42
43    /// Evaluate NOT condition - checks if no facts match the condition  
44    pub fn evaluate_not(condition: &ConditionGroup, facts: &Facts) -> bool {
45        // NOT is simply the opposite of EXISTS
46        !Self::evaluate_exists(condition, facts)
47    }
48
49    /// Evaluate FORALL condition - checks if all facts of target type match the condition
50    pub fn evaluate_forall(condition: &ConditionGroup, facts: &Facts) -> bool {
51        let all_facts = facts.get_all_facts();
52
53        // Extract the target type from condition
54        let target_type = match Self::extract_target_type(condition) {
55            Some(t) => t,
56            None => {
57                // If we can't determine target type, evaluate against all facts
58                return condition.evaluate(&all_facts);
59            }
60        };
61
62        // Find all facts of the target type (including numbered variants like Customer1, Customer2)
63        let mut target_facts = Vec::new();
64        for (fact_name, fact_value) in &all_facts {
65            // Check if fact name starts with target type (e.g., Customer1, Customer2, Customer3)
66            // OR exact match (e.g., Customer)
67            if fact_name.starts_with(&target_type) || fact_name == &target_type {
68                target_facts.push((fact_name, fact_value));
69            }
70        }
71
72        // If no facts of target type exist, FORALL is vacuously true
73        if target_facts.is_empty() {
74            return true;
75        }
76
77        // Check if ALL facts of target type match the condition
78        for (_fact_name, fact_value) in target_facts {
79            // Create a temporary fact context with the target type as key
80            // This allows condition evaluation to work with "Order.status" syntax
81            let mut temp_facts = HashMap::new();
82            temp_facts.insert(target_type.clone(), fact_value.clone());
83
84            if !condition.evaluate(&temp_facts) {
85                return false; // Found a fact that doesn't match
86            }
87        }
88
89        true // All facts matched
90    }
91
92    /// Extract the target fact type from a condition (e.g., "Customer" from "Customer.tier == 'VIP'")
93    fn extract_target_type(condition: &ConditionGroup) -> Option<String> {
94        match condition {
95            ConditionGroup::Single(cond) => {
96                // Extract the object name from field path (e.g., "Customer.tier" -> "Customer")
97                if let Some(dot_pos) = cond.field.find('.') {
98                    Some(cond.field[..dot_pos].to_string())
99                } else {
100                    Some(cond.field.clone())
101                }
102            }
103            ConditionGroup::Compound { left, .. } => {
104                // For compound conditions, try to extract from left side
105                Self::extract_target_type(left)
106            }
107            _ => None,
108        }
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115    use crate::engine::rule::Condition;
116    use crate::types::{Operator, Value};
117    use std::collections::HashMap;
118
119    #[test]
120    fn test_exists_pattern_matching() {
121        let facts = Facts::new();
122
123        // Add some test facts
124        let mut customer1 = HashMap::new();
125        customer1.insert("tier".to_string(), Value::String("VIP".to_string()));
126        facts
127            .add_value("Customer1", Value::Object(customer1))
128            .unwrap();
129
130        let mut customer2 = HashMap::new();
131        customer2.insert("tier".to_string(), Value::String("Regular".to_string()));
132        facts
133            .add_value("Customer2", Value::Object(customer2))
134            .unwrap();
135
136        // Test EXISTS condition: exists(Customer.tier == "VIP")
137        let condition = ConditionGroup::Single(Condition::new(
138            "Customer1.tier".to_string(),
139            Operator::Equal,
140            Value::String("VIP".to_string()),
141        ));
142
143        assert!(PatternMatcher::evaluate_exists(&condition, &facts));
144
145        // Test EXISTS condition that should fail
146        let condition_fail = ConditionGroup::Single(Condition::new(
147            "Customer1.tier".to_string(),
148            Operator::Equal,
149            Value::String("Premium".to_string()),
150        ));
151
152        assert!(!PatternMatcher::evaluate_exists(&condition_fail, &facts));
153    }
154
155    #[test]
156    fn test_not_pattern_matching() {
157        let facts = Facts::new();
158
159        // Add test fact
160        let mut customer = HashMap::new();
161        customer.insert("tier".to_string(), Value::String("Regular".to_string()));
162        facts
163            .add_value("Customer", Value::Object(customer))
164            .unwrap();
165
166        // Test NOT condition: not(Customer.tier == "VIP")
167        let condition = ConditionGroup::Single(Condition::new(
168            "Customer.tier".to_string(),
169            Operator::Equal,
170            Value::String("VIP".to_string()),
171        ));
172
173        assert!(PatternMatcher::evaluate_not(&condition, &facts));
174
175        // Test NOT condition that should fail
176        let condition_fail = ConditionGroup::Single(Condition::new(
177            "Customer.tier".to_string(),
178            Operator::Equal,
179            Value::String("Regular".to_string()),
180        ));
181
182        assert!(!PatternMatcher::evaluate_not(&condition_fail, &facts));
183    }
184
185    #[test]
186    fn test_forall_pattern_matching() {
187        let facts = Facts::new();
188
189        // Add multiple customers, all VIP
190        let mut customer1 = HashMap::new();
191        customer1.insert("tier".to_string(), Value::String("VIP".to_string()));
192        facts
193            .add_value("Customer1", Value::Object(customer1))
194            .unwrap();
195
196        let mut customer2 = HashMap::new();
197        customer2.insert("tier".to_string(), Value::String("VIP".to_string()));
198        facts
199            .add_value("Customer2", Value::Object(customer2))
200            .unwrap();
201
202        // Test FORALL condition: forall(Customer.tier == "VIP")
203        // This should match Customer1, Customer2, etc.
204        let condition = ConditionGroup::Single(Condition::new(
205            "Customer.tier".to_string(), // Generic pattern to match all Customer*
206            Operator::Equal,
207            Value::String("VIP".to_string()),
208        ));
209
210        assert!(PatternMatcher::evaluate_forall(&condition, &facts));
211
212        // Add a non-VIP customer
213        let mut customer3 = HashMap::new();
214        customer3.insert("tier".to_string(), Value::String("Regular".to_string()));
215        facts
216            .add_value("Customer3", Value::Object(customer3))
217            .unwrap();
218
219        // Now FORALL should fail
220        assert!(!PatternMatcher::evaluate_forall(&condition, &facts));
221    }
222
223    #[test]
224    fn test_extract_target_type() {
225        let condition = ConditionGroup::Single(Condition::new(
226            "Customer.tier".to_string(),
227            Operator::Equal,
228            Value::String("VIP".to_string()),
229        ));
230
231        assert_eq!(
232            PatternMatcher::extract_target_type(&condition),
233            Some("Customer".to_string())
234        );
235
236        let simple_condition = ConditionGroup::Single(Condition::new(
237            "Customer".to_string(),
238            Operator::Equal,
239            Value::String("VIP".to_string()),
240        ));
241
242        assert_eq!(
243            PatternMatcher::extract_target_type(&simple_condition),
244            Some("Customer".to_string())
245        );
246    }
247}