Skip to main content

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    /// Convert to number (f64) for arithmetic operations
64    pub fn as_number(&self) -> Option<f64> {
65        match self {
66            FactValue::Float(f) => Some(*f),
67            FactValue::Integer(i) => Some(*i as f64),
68            FactValue::String(s) => s.parse().ok(),
69            _ => None,
70        }
71    }
72
73    /// Try to convert to boolean
74    pub fn as_boolean(&self) -> Option<bool> {
75        match self {
76            FactValue::Boolean(b) => Some(*b),
77            FactValue::Integer(i) => Some(*i != 0),
78            FactValue::String(s) => match s.to_lowercase().as_str() {
79                "true" | "yes" | "1" => Some(true),
80                "false" | "no" | "0" => Some(false),
81                _ => None,
82            },
83            FactValue::Null => Some(false),
84            _ => None,
85        }
86    }
87
88    /// Check if value is null
89    pub fn is_null(&self) -> bool {
90        matches!(self, FactValue::Null)
91    }
92
93    /// Compare with operator
94    pub fn compare(&self, operator: &str, other: &FactValue) -> bool {
95        match operator {
96            "==" => self == other,
97            "!=" => self != other,
98            ">" => self.compare_gt(other),
99            "<" => self.compare_lt(other),
100            ">=" => self.compare_gte(other),
101            "<=" => self.compare_lte(other),
102            "contains" => self.contains(other),
103            "startsWith" => self.starts_with(other),
104            "endsWith" => self.ends_with(other),
105            "matches" => self.matches_pattern(other),
106            "in" => self.in_array(other),
107            _ => false,
108        }
109    }
110
111    fn compare_gt(&self, other: &FactValue) -> bool {
112        match (self.as_float(), other.as_float()) {
113            (Some(a), Some(b)) => a > b,
114            _ => false,
115        }
116    }
117
118    fn compare_lt(&self, other: &FactValue) -> bool {
119        match (self.as_float(), other.as_float()) {
120            (Some(a), Some(b)) => a < b,
121            _ => false,
122        }
123    }
124
125    fn compare_gte(&self, other: &FactValue) -> bool {
126        match (self.as_float(), other.as_float()) {
127            (Some(a), Some(b)) => a >= b,
128            _ => self == other,
129        }
130    }
131
132    fn compare_lte(&self, other: &FactValue) -> bool {
133        match (self.as_float(), other.as_float()) {
134            (Some(a), Some(b)) => a <= b,
135            _ => self == other,
136        }
137    }
138
139    fn contains(&self, other: &FactValue) -> bool {
140        match (self, other) {
141            (FactValue::String(s), FactValue::String(pattern)) => s.contains(pattern),
142            (FactValue::Array(arr), val) => arr.contains(val),
143            _ => false,
144        }
145    }
146
147    fn starts_with(&self, other: &FactValue) -> bool {
148        match (self, other) {
149            (FactValue::String(s), FactValue::String(prefix)) => s.starts_with(prefix),
150            _ => false,
151        }
152    }
153
154    fn ends_with(&self, other: &FactValue) -> bool {
155        match (self, other) {
156            (FactValue::String(s), FactValue::String(suffix)) => s.ends_with(suffix),
157            _ => false,
158        }
159    }
160
161    fn matches_pattern(&self, other: &FactValue) -> bool {
162        match (self, other) {
163            (FactValue::String(s), FactValue::String(pattern)) => {
164                // Simple wildcard matching (* and ?)
165                wildcard_match(s, pattern)
166            }
167            _ => false,
168        }
169    }
170
171    fn in_array(&self, other: &FactValue) -> bool {
172        match other {
173            FactValue::Array(arr) => arr.contains(self),
174            _ => false,
175        }
176    }
177}
178
179impl fmt::Display for FactValue {
180    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
181        write!(f, "{}", self.as_string())
182    }
183}
184
185impl From<String> for FactValue {
186    fn from(s: String) -> Self {
187        FactValue::String(s)
188    }
189}
190
191impl From<&str> for FactValue {
192    fn from(s: &str) -> Self {
193        FactValue::String(s.to_string())
194    }
195}
196
197impl From<i64> for FactValue {
198    fn from(i: i64) -> Self {
199        FactValue::Integer(i)
200    }
201}
202
203impl From<i32> for FactValue {
204    fn from(i: i32) -> Self {
205        FactValue::Integer(i as i64)
206    }
207}
208
209impl From<f64> for FactValue {
210    fn from(f: f64) -> Self {
211        FactValue::Float(f)
212    }
213}
214
215impl From<bool> for FactValue {
216    fn from(b: bool) -> Self {
217        FactValue::Boolean(b)
218    }
219}
220
221impl From<Vec<FactValue>> for FactValue {
222    fn from(arr: Vec<FactValue>) -> Self {
223        FactValue::Array(arr)
224    }
225}
226
227/// Convert from types::Value to FactValue
228impl From<crate::types::Value> for FactValue {
229    fn from(value: crate::types::Value) -> Self {
230        match value {
231            crate::types::Value::String(s) => FactValue::String(s),
232            crate::types::Value::Number(n) => FactValue::Float(n),
233            crate::types::Value::Integer(i) => FactValue::Integer(i),
234            crate::types::Value::Boolean(b) => FactValue::Boolean(b),
235            crate::types::Value::Array(arr) => {
236                FactValue::Array(arr.into_iter().map(|v| v.into()).collect())
237            }
238            crate::types::Value::Object(obj) => {
239                // Convert object to string representation
240                FactValue::String(format!("{:?}", obj))
241            }
242            crate::types::Value::Null => FactValue::Null,
243            crate::types::Value::Expression(expr) => FactValue::String(expr),
244        }
245    }
246}
247
248/// Typed facts collection
249#[derive(Debug, Clone)]
250pub struct TypedFacts {
251    data: HashMap<String, FactValue>,
252    /// Metadata: mapping from fact type to handle for retraction
253    /// Format: "FactType" -> FactHandle
254    pub(crate) fact_handles: HashMap<String, super::FactHandle>,
255}
256
257impl TypedFacts {
258    /// Create new empty facts collection
259    pub fn new() -> Self {
260        Self {
261            data: HashMap::new(),
262            fact_handles: HashMap::new(),
263        }
264    }
265
266    /// Create new facts collection with estimated capacity
267    pub fn with_capacity(capacity: usize) -> Self {
268        Self {
269            data: HashMap::with_capacity(capacity),
270            fact_handles: HashMap::with_capacity(capacity),
271        }
272    }
273
274    /// Set metadata about which handle corresponds to which fact type
275    pub fn set_fact_handle(&mut self, fact_type: String, handle: super::FactHandle) {
276        self.fact_handles.insert(fact_type, handle);
277    }
278
279    /// Get handle for a fact type (for retraction)
280    pub fn get_fact_handle(&self, fact_type: &str) -> Option<super::FactHandle> {
281        self.fact_handles.get(fact_type).copied()
282    }
283
284    /// Set a fact
285    pub fn set<K: Into<String>, V: Into<FactValue>>(&mut self, key: K, value: V) {
286        self.data.insert(key.into(), value.into());
287    }
288
289    /// Get a fact
290    pub fn get(&self, key: &str) -> Option<&FactValue> {
291        self.data.get(key)
292    }
293
294    /// Remove a fact
295    pub fn remove(&mut self, key: &str) -> Option<FactValue> {
296        self.data.remove(key)
297    }
298
299    /// Check if key exists
300    pub fn contains(&self, key: &str) -> bool {
301        self.data.contains_key(key)
302    }
303
304    /// Get all facts
305    pub fn get_all(&self) -> &HashMap<String, FactValue> {
306        &self.data
307    }
308
309    /// Clear all facts
310    pub fn clear(&mut self) {
311        self.data.clear();
312    }
313
314    /// Convert to string-based HashMap (for backward compatibility)
315    pub fn to_string_map(&self) -> HashMap<String, String> {
316        self.data
317            .iter()
318            .map(|(k, v)| (k.clone(), v.as_string()))
319            .collect()
320    }
321
322    /// Create from string-based HashMap (for backward compatibility)
323    pub fn from_string_map(map: &HashMap<String, String>) -> Self {
324        let mut facts = Self::new();
325        for (k, v) in map {
326            // Try to parse as different types
327            if let Ok(i) = v.parse::<i64>() {
328                facts.set(k.clone(), i);
329            } else if let Ok(f) = v.parse::<f64>() {
330                facts.set(k.clone(), f);
331            } else if let Ok(b) = v.parse::<bool>() {
332                facts.set(k.clone(), b);
333            } else {
334                facts.set(k.clone(), v.clone());
335            }
336        }
337        facts
338    }
339
340    /// Evaluate condition with typed comparison
341    pub fn evaluate_condition(&self, field: &str, operator: &str, value: &FactValue) -> bool {
342        if let Some(fact_value) = self.get(field) {
343            fact_value.compare(operator, value)
344        } else {
345            false
346        }
347    }
348}
349
350impl Default for TypedFacts {
351    fn default() -> Self {
352        Self::new()
353    }
354}
355
356/// Simple wildcard pattern matching
357/// Supports * (any characters) and ? (single character)
358fn wildcard_match(text: &str, pattern: &str) -> bool {
359    let text_chars: Vec<char> = text.chars().collect();
360    let pattern_chars: Vec<char> = pattern.chars().collect();
361
362    wildcard_match_impl(&text_chars, &pattern_chars, 0, 0)
363}
364
365fn wildcard_match_impl(text: &[char], pattern: &[char], ti: usize, pi: usize) -> bool {
366    if pi == pattern.len() {
367        return ti == text.len();
368    }
369
370    if pattern[pi] == '*' {
371        // Match zero or more characters
372        for i in ti..=text.len() {
373            if wildcard_match_impl(text, pattern, i, pi + 1) {
374                return true;
375            }
376        }
377        false
378    } else if ti < text.len() && (pattern[pi] == '?' || pattern[pi] == text[ti]) {
379        // Match single character or exact match
380        wildcard_match_impl(text, pattern, ti + 1, pi + 1)
381    } else {
382        false
383    }
384}
385
386#[cfg(test)]
387mod tests {
388    use super::*;
389
390    #[test]
391    fn test_fact_value_types() {
392        let s = FactValue::String("hello".to_string());
393        let i = FactValue::Integer(42);
394        let f = FactValue::Float(std::f64::consts::PI);
395        let b = FactValue::Boolean(true);
396
397        assert_eq!(s.as_string(), "hello");
398        assert_eq!(i.as_integer(), Some(42));
399        assert_eq!(f.as_float(), Some(std::f64::consts::PI));
400        assert_eq!(b.as_boolean(), Some(true));
401    }
402
403    #[test]
404    fn test_comparisons() {
405        let a = FactValue::Integer(10);
406        let b = FactValue::Integer(20);
407
408        assert!(a.compare("<", &b));
409        assert!(b.compare(">", &a));
410        assert!(a.compare("<=", &a));
411        assert!(a.compare("!=", &b));
412    }
413
414    #[test]
415    fn test_string_operations() {
416        let text = FactValue::String("hello world".to_string());
417        let pattern = FactValue::String("world".to_string());
418        let prefix = FactValue::String("hello".to_string());
419
420        assert!(text.compare("contains", &pattern));
421        assert!(text.compare("startsWith", &prefix));
422    }
423
424    #[test]
425    fn test_wildcard_matching() {
426        let text = FactValue::String("hello world".to_string());
427
428        assert!(text.compare("matches", &FactValue::String("hello*".to_string())));
429        assert!(text.compare("matches", &FactValue::String("*world".to_string())));
430        assert!(text.compare("matches", &FactValue::String("hello?world".to_string())));
431        assert!(!text.compare("matches", &FactValue::String("hello?earth".to_string())));
432    }
433
434    #[test]
435    fn test_array_operations() {
436        let arr = FactValue::Array(vec![
437            FactValue::Integer(1),
438            FactValue::Integer(2),
439            FactValue::Integer(3),
440        ]);
441
442        let val = FactValue::Integer(2);
443        assert!(val.compare("in", &arr));
444
445        let val2 = FactValue::Integer(5);
446        assert!(!val2.compare("in", &arr));
447    }
448
449    #[test]
450    fn test_typed_facts() {
451        let mut facts = TypedFacts::new();
452        facts.set("age", 25i64);
453        facts.set("name", "John");
454        facts.set("score", 95.5);
455        facts.set("active", true);
456
457        assert_eq!(facts.get("age").unwrap().as_integer(), Some(25));
458        assert_eq!(facts.get("name").unwrap().as_string(), "John");
459        assert_eq!(facts.get("score").unwrap().as_float(), Some(95.5));
460        assert_eq!(facts.get("active").unwrap().as_boolean(), Some(true));
461    }
462
463    #[test]
464    fn test_evaluate_condition() {
465        let mut facts = TypedFacts::new();
466        facts.set("age", 25i64);
467        facts.set("name", "John Smith");
468
469        assert!(facts.evaluate_condition("age", ">", &FactValue::Integer(18)));
470        assert!(facts.evaluate_condition("age", "<=", &FactValue::Integer(30)));
471        assert!(facts.evaluate_condition(
472            "name",
473            "contains",
474            &FactValue::String("Smith".to_string())
475        ));
476        assert!(facts.evaluate_condition(
477            "name",
478            "startsWith",
479            &FactValue::String("John".to_string())
480        ));
481    }
482}