rust_rule_engine/rete/
alpha.rs

1//! AlphaNode: checks single condition on a fact
2
3use super::facts::{FactValue, TypedFacts};
4
5#[derive(Debug, Clone)]
6pub struct AlphaNode {
7    pub field: String,
8    pub operator: String,
9    pub value: String,
10}
11
12
13impl AlphaNode {
14    /// Match with string-based facts (backward compatible)
15    pub fn matches(&self, fact_field: &str, fact_value: &str) -> bool {
16        if self.field != fact_field {
17            return false;
18        }
19        match self.operator.as_str() {
20            "==" => fact_value == self.value,
21            "!=" => fact_value != self.value,
22            ">" => parse_num(fact_value) > parse_num(&self.value),
23            "<" => parse_num(fact_value) < parse_num(&self.value),
24            ">=" => parse_num(fact_value) >= parse_num(&self.value),
25            "<=" => parse_num(fact_value) <= parse_num(&self.value),
26            "contains" => fact_value.contains(&self.value),
27            "startsWith" => fact_value.starts_with(&self.value),
28            "endsWith" => fact_value.ends_with(&self.value),
29            "matches" => wildcard_match(fact_value, &self.value),
30            _ => false,
31        }
32    }
33
34    /// Match with typed facts (new!)
35    pub fn matches_typed(&self, facts: &TypedFacts) -> bool {
36        // Check if the value is a variable reference (field name in facts)
37        // This enables variable-to-variable comparison like "L1 > L1Min"
38        let expected_value = if let Some(var_value) = facts.get(&self.value) {
39            // Value is a field name - use the field's value for comparison
40            var_value.clone()
41        } else {
42            // Value is a literal - parse it
43            self.parse_value_string(&self.value)
44        };
45
46        facts.evaluate_condition(&self.field, &self.operator, &expected_value)
47    }
48
49    /// Parse value string into FactValue
50    fn parse_value_string(&self, s: &str) -> FactValue {
51        // Try to parse as different types
52        if let Ok(i) = s.parse::<i64>() {
53            FactValue::Integer(i)
54        } else if let Ok(f) = s.parse::<f64>() {
55            FactValue::Float(f)
56        } else if let Ok(b) = s.parse::<bool>() {
57            FactValue::Boolean(b)
58        } else if s == "null" {
59            FactValue::Null
60        } else {
61            FactValue::String(s.to_string())
62        }
63    }
64
65    /// Create with typed value
66    pub fn with_typed_value(field: String, operator: String, value: FactValue) -> Self {
67        Self {
68            field,
69            operator,
70            value: value.as_string(),
71        }
72    }
73}
74
75fn parse_num(s: &str) -> f64 {
76    s.parse::<f64>().unwrap_or(0.0)
77}
78
79/// Simple wildcard pattern matching (for backward compatibility)
80fn wildcard_match(text: &str, pattern: &str) -> bool {
81    let text_chars: Vec<char> = text.chars().collect();
82    let pattern_chars: Vec<char> = pattern.chars().collect();
83    wildcard_match_impl(&text_chars, &pattern_chars, 0, 0)
84}
85
86fn wildcard_match_impl(text: &[char], pattern: &[char], ti: usize, pi: usize) -> bool {
87    if pi == pattern.len() {
88        return ti == text.len();
89    }
90
91    if pattern[pi] == '*' {
92        for i in ti..=text.len() {
93            if wildcard_match_impl(text, pattern, i, pi + 1) {
94                return true;
95            }
96        }
97        false
98    } else if ti < text.len() && (pattern[pi] == '?' || pattern[pi] == text[ti]) {
99        wildcard_match_impl(text, pattern, ti + 1, pi + 1)
100    } else {
101        false
102    }
103}