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