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