rust_rule_engine/engine/
pattern_matcher.rs

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