rust_rule_engine/rete/
multifield.rs

1//! Multi-field (Multislot) Variables - CLIPS-inspired Feature
2//!
3//! This module implements CLIPS-style multi-field variables for pattern matching
4//! on arrays and collections. Multi-field variables allow:
5//!
6//! - Collecting all values: `Order.items $?all_items`
7//! - Checking containment: `Product.tags contains "electronics"`
8//! - Getting array length: `Order.items count > 0`
9//! - Accessing elements: `Order.items first`, `Order.items last`
10//!
11//! ## CLIPS Reference
12//!
13//! ```clips
14//! (deftemplate order
15//!   (slot order-id)
16//!   (multislot items))
17//!
18//! (defrule process-order
19//!   (order (order-id ?id) (items $?all-items))
20//!   =>
21//!   (foreach ?item $?all-items
22//!     (process ?item)))
23//! ```
24//!
25//! ## Rust API
26//!
27//! ```rust,ignore
28//! use rust_rule_engine::rete::{MultifieldOp, PatternConstraint};
29//!
30//! // Pattern: Order.items $?all_items
31//! let constraint = PatternConstraint::MultiField {
32//!     field: "items".to_string(),
33//!     variable: Some("$?all_items".to_string()),
34//!     operator: MultifieldOp::Collect,
35//!     value: None,
36//! };
37//!
38//! // Pattern: Product.tags contains "electronics"
39//! let constraint = PatternConstraint::MultiField {
40//!     field: "tags".to_string(),
41//!     variable: None,
42//!     operator: MultifieldOp::Contains,
43//!     value: Some(FactValue::String("electronics".to_string())),
44//! };
45//! ```
46
47use crate::rete::facts::{FactValue, TypedFacts};
48use crate::errors::{Result, RuleEngineError};
49use std::collections::HashMap;
50
51/// Multi-field operations for pattern matching
52///
53/// These operations enable CLIPS-style multi-field variable matching and manipulation.
54#[derive(Debug, Clone, PartialEq)]
55pub enum MultifieldOp {
56    /// Collect all values into a variable: `$?var`
57    ///
58    /// Example: `Order.items $?all_items` binds all items to `$?all_items`
59    Collect,
60
61    /// Check if array contains a specific value
62    ///
63    /// Example: `Product.tags contains "electronics"`
64    Contains,
65
66    /// Get the count/length of the array
67    ///
68    /// Example: `Order.items count` returns the number of items
69    Count,
70
71    /// Get the first element of the array
72    ///
73    /// Example: `Order.items first` returns the first item
74    First,
75
76    /// Get the last element of the array
77    ///
78    /// Example: `Order.items last` returns the last item
79    Last,
80
81    /// Get a specific element by index (0-based)
82    ///
83    /// Example: `Order.items[0]` returns the first item
84    Index(usize),
85
86    /// Get a slice of the array [start:end]
87    ///
88    /// Example: `Order.items[1:3]` returns items at index 1 and 2
89    Slice(usize, usize),
90
91    /// Check if array is empty
92    ///
93    /// Example: `Order.items empty`
94    IsEmpty,
95
96    /// Check if array is not empty
97    ///
98    /// Example: `Order.items not_empty`
99    NotEmpty,
100}
101
102impl MultifieldOp {
103    /// Evaluate a multi-field operation on facts
104    ///
105    /// Returns:
106    /// - `Some(Vec<FactValue>)` - Collection of values (for Collect, Slice)
107    /// - `Some(vec![FactValue::Integer(n)])` - Numeric result (for Count, Index)
108    /// - `Some(vec![FactValue::Boolean(b)])` - Boolean result (for Contains, IsEmpty, etc.)
109    /// - `None` - Operation failed (field not found, invalid type, etc.)
110    pub fn evaluate(
111        &self,
112        facts: &TypedFacts,
113        field: &str,
114        value: Option<&FactValue>,
115    ) -> Option<Vec<FactValue>> {
116        // Get the field value
117        let field_value = facts.get(field)?;
118
119        // Ensure it's an array
120        let array = match field_value {
121            FactValue::Array(arr) => arr,
122            _ => return None, // Not an array
123        };
124
125        match self {
126            MultifieldOp::Collect => {
127                // Return all values
128                Some(array.clone())
129            }
130
131            MultifieldOp::Contains => {
132                // Check if array contains the specified value
133                let search_value = value?;
134                let contains = array.contains(search_value);
135                Some(vec![FactValue::Boolean(contains)])
136            }
137
138            MultifieldOp::Count => {
139                // Return array length
140                Some(vec![FactValue::Integer(array.len() as i64)])
141            }
142
143            MultifieldOp::First => {
144                // Return first element
145                array.first().cloned().map(|v| vec![v])
146            }
147
148            MultifieldOp::Last => {
149                // Return last element
150                array.last().cloned().map(|v| vec![v])
151            }
152
153            MultifieldOp::Index(idx) => {
154                // Return element at index
155                array.get(*idx).cloned().map(|v| vec![v])
156            }
157
158            MultifieldOp::Slice(start, end) => {
159                // Return slice of array
160                let end = (*end).min(array.len());
161                if *start >= end {
162                    return Some(Vec::new());
163                }
164                Some(array[*start..end].to_vec())
165            }
166
167            MultifieldOp::IsEmpty => {
168                // Check if array is empty
169                Some(vec![FactValue::Boolean(array.is_empty())])
170            }
171
172            MultifieldOp::NotEmpty => {
173                // Check if array is not empty
174                Some(vec![FactValue::Boolean(!array.is_empty())])
175            }
176        }
177    }
178
179    /// Get a string representation of the operation
180    pub fn to_string(&self) -> String {
181        match self {
182            MultifieldOp::Collect => "collect".to_string(),
183            MultifieldOp::Contains => "contains".to_string(),
184            MultifieldOp::Count => "count".to_string(),
185            MultifieldOp::First => "first".to_string(),
186            MultifieldOp::Last => "last".to_string(),
187            MultifieldOp::Index(idx) => format!("[{}]", idx),
188            MultifieldOp::Slice(start, end) => format!("[{}:{}]", start, end),
189            MultifieldOp::IsEmpty => "empty".to_string(),
190            MultifieldOp::NotEmpty => "not_empty".to_string(),
191        }
192    }
193}
194
195/// Helper function to evaluate multi-field patterns in rules
196///
197/// This function combines the multi-field operation with variable binding.
198/// It returns both the result values and optional variable bindings.
199///
200/// # Arguments
201///
202/// * `facts` - The facts to evaluate against
203/// * `field` - The field name (e.g., "items")
204/// * `operator` - The multi-field operation
205/// * `variable` - Optional variable name for binding (e.g., "$?all_items")
206/// * `value` - Optional value for operations like Contains
207/// * `bindings` - Existing variable bindings
208///
209/// # Returns
210///
211/// `Some(HashMap<Variable, FactValue>)` - New bindings including multi-field results
212/// `None` - Pattern doesn't match
213pub fn evaluate_multifield_pattern(
214    facts: &TypedFacts,
215    field: &str,
216    operator: &MultifieldOp,
217    variable: Option<&str>,
218    value: Option<&FactValue>,
219    bindings: &HashMap<String, FactValue>,
220) -> Option<HashMap<String, FactValue>> {
221    // Evaluate the multi-field operation
222    let result = operator.evaluate(facts, field, value)?;
223
224    // Create new bindings
225    let mut new_bindings = bindings.clone();
226
227    // If there's a variable, bind the result
228    if let Some(var_name) = variable {
229        // For Collect operation, bind as array
230        if matches!(operator, MultifieldOp::Collect) {
231            new_bindings.insert(var_name.to_string(), FactValue::Array(result));
232        } else {
233            // For single-value results, unwrap
234            if result.len() == 1 {
235                new_bindings.insert(var_name.to_string(), result[0].clone());
236            } else {
237                // Multiple values, bind as array
238                new_bindings.insert(var_name.to_string(), FactValue::Array(result));
239            }
240        }
241    } else {
242        // No variable binding, just check if operation succeeded
243        // For boolean operations, check the result
244        if result.len() == 1 {
245            if let FactValue::Boolean(b) = result[0] {
246                if !b {
247                    return None; // Pattern doesn't match
248                }
249            }
250        }
251    }
252
253    Some(new_bindings)
254}
255
256/// Parse multi-field variable syntax
257///
258/// Recognizes patterns like:
259/// - `$?var` - Multi-field variable (collects all values)
260/// - `$var` - Single-value binding (not multi-field)
261///
262/// Returns `Some(variable_name)` if it's a multi-field variable, `None` otherwise
263pub fn parse_multifield_variable(input: &str) -> Option<String> {
264    let trimmed = input.trim();
265    if trimmed.starts_with("$?") {
266        Some(trimmed[2..].to_string())
267    } else {
268        None
269    }
270}
271
272/// Check if a string is a multi-field variable
273pub fn is_multifield_variable(input: &str) -> bool {
274    input.trim().starts_with("$?")
275}
276
277#[cfg(test)]
278mod tests {
279    use super::*;
280
281    fn create_test_facts_with_array() -> TypedFacts {
282        let mut facts = TypedFacts::new();
283        facts.set("items", FactValue::Array(vec![
284            FactValue::String("item1".to_string()),
285            FactValue::String("item2".to_string()),
286            FactValue::String("item3".to_string()),
287        ]));
288        facts.set("tags", FactValue::Array(vec![
289            FactValue::String("electronics".to_string()),
290            FactValue::String("gadgets".to_string()),
291        ]));
292        facts
293    }
294
295    #[test]
296    fn test_collect_operation() {
297        let facts = create_test_facts_with_array();
298        let op = MultifieldOp::Collect;
299
300        let result = op.evaluate(&facts, "items", None);
301        assert!(result.is_some());
302
303        let values = result.unwrap();
304        assert_eq!(values.len(), 3);
305        assert_eq!(values[0], FactValue::String("item1".to_string()));
306    }
307
308    #[test]
309    fn test_contains_operation() {
310        let facts = create_test_facts_with_array();
311        let op = MultifieldOp::Contains;
312        let search = FactValue::String("electronics".to_string());
313
314        let result = op.evaluate(&facts, "tags", Some(&search));
315        assert!(result.is_some());
316
317        let values = result.unwrap();
318        assert_eq!(values.len(), 1);
319        assert_eq!(values[0], FactValue::Boolean(true));
320    }
321
322    #[test]
323    fn test_count_operation() {
324        let facts = create_test_facts_with_array();
325        let op = MultifieldOp::Count;
326
327        let result = op.evaluate(&facts, "items", None);
328        assert!(result.is_some());
329
330        let values = result.unwrap();
331        assert_eq!(values[0], FactValue::Integer(3));
332    }
333
334    #[test]
335    fn test_first_last_operations() {
336        let facts = create_test_facts_with_array();
337
338        let first = MultifieldOp::First.evaluate(&facts, "items", None).unwrap();
339        assert_eq!(first[0], FactValue::String("item1".to_string()));
340
341        let last = MultifieldOp::Last.evaluate(&facts, "items", None).unwrap();
342        assert_eq!(last[0], FactValue::String("item3".to_string()));
343    }
344
345    #[test]
346    fn test_index_operation() {
347        let facts = create_test_facts_with_array();
348        let op = MultifieldOp::Index(1);
349
350        let result = op.evaluate(&facts, "items", None).unwrap();
351        assert_eq!(result[0], FactValue::String("item2".to_string()));
352    }
353
354    #[test]
355    fn test_slice_operation() {
356        let facts = create_test_facts_with_array();
357        let op = MultifieldOp::Slice(0, 2);
358
359        let result = op.evaluate(&facts, "items", None).unwrap();
360        assert_eq!(result.len(), 2);
361        assert_eq!(result[0], FactValue::String("item1".to_string()));
362        assert_eq!(result[1], FactValue::String("item2".to_string()));
363    }
364
365    #[test]
366    fn test_is_empty_operation() {
367        let mut facts = TypedFacts::new();
368        facts.set("empty_array", FactValue::Array(Vec::new()));
369
370        let op = MultifieldOp::IsEmpty;
371        let result = op.evaluate(&facts, "empty_array", None).unwrap();
372        assert_eq!(result[0], FactValue::Boolean(true));
373    }
374
375    #[test]
376    fn test_parse_multifield_variable() {
377        assert_eq!(parse_multifield_variable("$?items"), Some("items".to_string()));
378        assert_eq!(parse_multifield_variable("$?all"), Some("all".to_string()));
379        assert_eq!(parse_multifield_variable("$single"), None);
380        assert_eq!(parse_multifield_variable("items"), None);
381    }
382
383    #[test]
384    fn test_is_multifield_variable() {
385        assert!(is_multifield_variable("$?items"));
386        assert!(is_multifield_variable("$?all"));
387        assert!(!is_multifield_variable("$single"));
388        assert!(!is_multifield_variable("items"));
389    }
390
391    #[test]
392    fn test_evaluate_multifield_pattern_with_binding() {
393        let facts = create_test_facts_with_array();
394        let bindings = HashMap::new();
395
396        let result = evaluate_multifield_pattern(
397            &facts,
398            "items",
399            &MultifieldOp::Collect,
400            Some("$?all_items"),
401            None,
402            &bindings,
403        );
404
405        assert!(result.is_some());
406        let new_bindings = result.unwrap();
407
408        assert!(new_bindings.contains_key("$?all_items"));
409        if let FactValue::Array(arr) = &new_bindings["$?all_items"] {
410            assert_eq!(arr.len(), 3);
411        } else {
412            panic!("Expected array binding");
413        }
414    }
415
416    #[test]
417    fn test_evaluate_multifield_pattern_contains() {
418        let facts = create_test_facts_with_array();
419        let bindings = HashMap::new();
420        let search = FactValue::String("electronics".to_string());
421
422        let result = evaluate_multifield_pattern(
423            &facts,
424            "tags",
425            &MultifieldOp::Contains,
426            None,
427            Some(&search),
428            &bindings,
429        );
430
431        assert!(result.is_some()); // Should match
432
433        // Test with non-existent value
434        let search2 = FactValue::String("nonexistent".to_string());
435        let result2 = evaluate_multifield_pattern(
436            &facts,
437            "tags",
438            &MultifieldOp::Contains,
439            None,
440            Some(&search2),
441            &bindings,
442        );
443
444        assert!(result2.is_none()); // Should not match
445    }
446}