rust_rule_engine/rete/
facts.rs

1//! Typed facts system for RETE-UL
2//!
3//! This module provides a strongly-typed facts system that supports:
4//! - Multiple data types (String, Integer, Float, Boolean, Array, Object)
5//! - Type-safe operations
6//! - Efficient conversions
7//! - Better operator support
8
9use std::collections::HashMap;
10use std::fmt;
11
12/// Strongly-typed fact value
13#[derive(Debug, Clone, PartialEq)]
14pub enum FactValue {
15    /// String value
16    String(String),
17    /// Integer value (i64)
18    Integer(i64),
19    /// Float value (f64)
20    Float(f64),
21    /// Boolean value
22    Boolean(bool),
23    /// Array of values
24    Array(Vec<FactValue>),
25    /// Null/None value
26    Null,
27}
28
29impl FactValue {
30    /// Convert to string representation
31    pub fn as_string(&self) -> String {
32        match self {
33            FactValue::String(s) => s.clone(),
34            FactValue::Integer(i) => i.to_string(),
35            FactValue::Float(f) => f.to_string(),
36            FactValue::Boolean(b) => b.to_string(),
37            FactValue::Array(arr) => format!("{:?}", arr),
38            FactValue::Null => "null".to_string(),
39        }
40    }
41
42    /// Try to convert to integer
43    pub fn as_integer(&self) -> Option<i64> {
44        match self {
45            FactValue::Integer(i) => Some(*i),
46            FactValue::Float(f) => Some(*f as i64),
47            FactValue::String(s) => s.parse().ok(),
48            FactValue::Boolean(b) => Some(if *b { 1 } else { 0 }),
49            _ => None,
50        }
51    }
52
53    /// Try to convert to float
54    pub fn as_float(&self) -> Option<f64> {
55        match self {
56            FactValue::Float(f) => Some(*f),
57            FactValue::Integer(i) => Some(*i as f64),
58            FactValue::String(s) => s.parse().ok(),
59            _ => None,
60        }
61    }
62
63    /// Try to convert to boolean
64    pub fn as_boolean(&self) -> Option<bool> {
65        match self {
66            FactValue::Boolean(b) => Some(*b),
67            FactValue::Integer(i) => Some(*i != 0),
68            FactValue::String(s) => match s.to_lowercase().as_str() {
69                "true" | "yes" | "1" => Some(true),
70                "false" | "no" | "0" => Some(false),
71                _ => None,
72            },
73            FactValue::Null => Some(false),
74            _ => None,
75        }
76    }
77
78    /// Check if value is null
79    pub fn is_null(&self) -> bool {
80        matches!(self, FactValue::Null)
81    }
82
83    /// Compare with operator
84    pub fn compare(&self, operator: &str, other: &FactValue) -> bool {
85        match operator {
86            "==" => self == other,
87            "!=" => self != other,
88            ">" => self.compare_gt(other),
89            "<" => self.compare_lt(other),
90            ">=" => self.compare_gte(other),
91            "<=" => self.compare_lte(other),
92            "contains" => self.contains(other),
93            "startsWith" => self.starts_with(other),
94            "endsWith" => self.ends_with(other),
95            "matches" => self.matches_pattern(other),
96            "in" => self.in_array(other),
97            _ => false,
98        }
99    }
100
101    fn compare_gt(&self, other: &FactValue) -> bool {
102        match (self.as_float(), other.as_float()) {
103            (Some(a), Some(b)) => a > b,
104            _ => false,
105        }
106    }
107
108    fn compare_lt(&self, other: &FactValue) -> bool {
109        match (self.as_float(), other.as_float()) {
110            (Some(a), Some(b)) => a < b,
111            _ => false,
112        }
113    }
114
115    fn compare_gte(&self, other: &FactValue) -> bool {
116        match (self.as_float(), other.as_float()) {
117            (Some(a), Some(b)) => a >= b,
118            _ => self == other,
119        }
120    }
121
122    fn compare_lte(&self, other: &FactValue) -> bool {
123        match (self.as_float(), other.as_float()) {
124            (Some(a), Some(b)) => a <= b,
125            _ => self == other,
126        }
127    }
128
129    fn contains(&self, other: &FactValue) -> bool {
130        match (self, other) {
131            (FactValue::String(s), FactValue::String(pattern)) => s.contains(pattern),
132            (FactValue::Array(arr), val) => arr.contains(val),
133            _ => false,
134        }
135    }
136
137    fn starts_with(&self, other: &FactValue) -> bool {
138        match (self, other) {
139            (FactValue::String(s), FactValue::String(prefix)) => s.starts_with(prefix),
140            _ => false,
141        }
142    }
143
144    fn ends_with(&self, other: &FactValue) -> bool {
145        match (self, other) {
146            (FactValue::String(s), FactValue::String(suffix)) => s.ends_with(suffix),
147            _ => false,
148        }
149    }
150
151    fn matches_pattern(&self, other: &FactValue) -> bool {
152        match (self, other) {
153            (FactValue::String(s), FactValue::String(pattern)) => {
154                // Simple wildcard matching (* and ?)
155                wildcard_match(s, pattern)
156            }
157            _ => false,
158        }
159    }
160
161    fn in_array(&self, other: &FactValue) -> bool {
162        match other {
163            FactValue::Array(arr) => arr.contains(self),
164            _ => false,
165        }
166    }
167}
168
169impl fmt::Display for FactValue {
170    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
171        write!(f, "{}", self.as_string())
172    }
173}
174
175impl From<String> for FactValue {
176    fn from(s: String) -> Self {
177        FactValue::String(s)
178    }
179}
180
181impl From<&str> for FactValue {
182    fn from(s: &str) -> Self {
183        FactValue::String(s.to_string())
184    }
185}
186
187impl From<i64> for FactValue {
188    fn from(i: i64) -> Self {
189        FactValue::Integer(i)
190    }
191}
192
193impl From<i32> for FactValue {
194    fn from(i: i32) -> Self {
195        FactValue::Integer(i as i64)
196    }
197}
198
199impl From<f64> for FactValue {
200    fn from(f: f64) -> Self {
201        FactValue::Float(f)
202    }
203}
204
205impl From<bool> for FactValue {
206    fn from(b: bool) -> Self {
207        FactValue::Boolean(b)
208    }
209}
210
211impl From<Vec<FactValue>> for FactValue {
212    fn from(arr: Vec<FactValue>) -> Self {
213        FactValue::Array(arr)
214    }
215}
216
217/// Typed facts collection
218#[derive(Debug, Clone)]
219pub struct TypedFacts {
220    data: HashMap<String, FactValue>,
221}
222
223impl TypedFacts {
224    /// Create new empty facts collection
225    pub fn new() -> Self {
226        Self {
227            data: HashMap::new(),
228        }
229    }
230
231    /// Set a fact
232    pub fn set<K: Into<String>, V: Into<FactValue>>(&mut self, key: K, value: V) {
233        self.data.insert(key.into(), value.into());
234    }
235
236    /// Get a fact
237    pub fn get(&self, key: &str) -> Option<&FactValue> {
238        self.data.get(key)
239    }
240
241    /// Remove a fact
242    pub fn remove(&mut self, key: &str) -> Option<FactValue> {
243        self.data.remove(key)
244    }
245
246    /// Check if key exists
247    pub fn contains(&self, key: &str) -> bool {
248        self.data.contains_key(key)
249    }
250
251    /// Get all facts
252    pub fn get_all(&self) -> &HashMap<String, FactValue> {
253        &self.data
254    }
255
256    /// Clear all facts
257    pub fn clear(&mut self) {
258        self.data.clear();
259    }
260
261    /// Convert to string-based HashMap (for backward compatibility)
262    pub fn to_string_map(&self) -> HashMap<String, String> {
263        self.data
264            .iter()
265            .map(|(k, v)| (k.clone(), v.as_string()))
266            .collect()
267    }
268
269    /// Create from string-based HashMap (for backward compatibility)
270    pub fn from_string_map(map: &HashMap<String, String>) -> Self {
271        let mut facts = Self::new();
272        for (k, v) in map {
273            // Try to parse as different types
274            if let Ok(i) = v.parse::<i64>() {
275                facts.set(k.clone(), i);
276            } else if let Ok(f) = v.parse::<f64>() {
277                facts.set(k.clone(), f);
278            } else if let Ok(b) = v.parse::<bool>() {
279                facts.set(k.clone(), b);
280            } else {
281                facts.set(k.clone(), v.clone());
282            }
283        }
284        facts
285    }
286
287    /// Evaluate condition with typed comparison
288    pub fn evaluate_condition(&self, field: &str, operator: &str, value: &FactValue) -> bool {
289        if let Some(fact_value) = self.get(field) {
290            fact_value.compare(operator, value)
291        } else {
292            false
293        }
294    }
295}
296
297impl Default for TypedFacts {
298    fn default() -> Self {
299        Self::new()
300    }
301}
302
303/// Simple wildcard pattern matching
304/// Supports * (any characters) and ? (single character)
305fn wildcard_match(text: &str, pattern: &str) -> bool {
306    let text_chars: Vec<char> = text.chars().collect();
307    let pattern_chars: Vec<char> = pattern.chars().collect();
308
309    wildcard_match_impl(&text_chars, &pattern_chars, 0, 0)
310}
311
312fn wildcard_match_impl(text: &[char], pattern: &[char], ti: usize, pi: usize) -> bool {
313    if pi == pattern.len() {
314        return ti == text.len();
315    }
316
317    if pattern[pi] == '*' {
318        // Match zero or more characters
319        for i in ti..=text.len() {
320            if wildcard_match_impl(text, pattern, i, pi + 1) {
321                return true;
322            }
323        }
324        false
325    } else if ti < text.len() && (pattern[pi] == '?' || pattern[pi] == text[ti]) {
326        // Match single character or exact match
327        wildcard_match_impl(text, pattern, ti + 1, pi + 1)
328    } else {
329        false
330    }
331}
332
333#[cfg(test)]
334mod tests {
335    use super::*;
336
337    #[test]
338    fn test_fact_value_types() {
339        let s = FactValue::String("hello".to_string());
340        let i = FactValue::Integer(42);
341        let f = FactValue::Float(3.14);
342        let b = FactValue::Boolean(true);
343
344        assert_eq!(s.as_string(), "hello");
345        assert_eq!(i.as_integer(), Some(42));
346        assert_eq!(f.as_float(), Some(3.14));
347        assert_eq!(b.as_boolean(), Some(true));
348    }
349
350    #[test]
351    fn test_comparisons() {
352        let a = FactValue::Integer(10);
353        let b = FactValue::Integer(20);
354
355        assert!(a.compare("<", &b));
356        assert!(b.compare(">", &a));
357        assert!(a.compare("<=", &a));
358        assert!(a.compare("!=", &b));
359    }
360
361    #[test]
362    fn test_string_operations() {
363        let text = FactValue::String("hello world".to_string());
364        let pattern = FactValue::String("world".to_string());
365        let prefix = FactValue::String("hello".to_string());
366
367        assert!(text.compare("contains", &pattern));
368        assert!(text.compare("startsWith", &prefix));
369    }
370
371    #[test]
372    fn test_wildcard_matching() {
373        let text = FactValue::String("hello world".to_string());
374
375        assert!(text.compare("matches", &FactValue::String("hello*".to_string())));
376        assert!(text.compare("matches", &FactValue::String("*world".to_string())));
377        assert!(text.compare("matches", &FactValue::String("hello?world".to_string())));
378        assert!(!text.compare("matches", &FactValue::String("hello?earth".to_string())));
379    }
380
381    #[test]
382    fn test_array_operations() {
383        let arr = FactValue::Array(vec![
384            FactValue::Integer(1),
385            FactValue::Integer(2),
386            FactValue::Integer(3),
387        ]);
388
389        let val = FactValue::Integer(2);
390        assert!(val.compare("in", &arr));
391
392        let val2 = FactValue::Integer(5);
393        assert!(!val2.compare("in", &arr));
394    }
395
396    #[test]
397    fn test_typed_facts() {
398        let mut facts = TypedFacts::new();
399        facts.set("age", 25i64);
400        facts.set("name", "John");
401        facts.set("score", 95.5);
402        facts.set("active", true);
403
404        assert_eq!(facts.get("age").unwrap().as_integer(), Some(25));
405        assert_eq!(facts.get("name").unwrap().as_string(), "John");
406        assert_eq!(facts.get("score").unwrap().as_float(), Some(95.5));
407        assert_eq!(facts.get("active").unwrap().as_boolean(), Some(true));
408    }
409
410    #[test]
411    fn test_evaluate_condition() {
412        let mut facts = TypedFacts::new();
413        facts.set("age", 25i64);
414        facts.set("name", "John Smith");
415
416        assert!(facts.evaluate_condition("age", ">", &FactValue::Integer(18)));
417        assert!(facts.evaluate_condition("age", "<=", &FactValue::Integer(30)));
418        assert!(facts.evaluate_condition("name", "contains", &FactValue::String("Smith".to_string())));
419        assert!(facts.evaluate_condition("name", "startsWith", &FactValue::String("John".to_string())));
420    }
421}