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    #[allow(clippy::inherent_to_string)]
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    trimmed.strip_prefix("$?").map(|s| s.to_string())
266}
267
268/// Check if a string is a multi-field variable
269pub fn is_multifield_variable(input: &str) -> bool {
270    input.trim().starts_with("$?")
271}
272
273#[cfg(test)]
274mod tests {
275    use super::*;
276
277    fn create_test_facts_with_array() -> TypedFacts {
278        let mut facts = TypedFacts::new();
279        facts.set(
280            "items",
281            FactValue::Array(vec![
282                FactValue::String("item1".to_string()),
283                FactValue::String("item2".to_string()),
284                FactValue::String("item3".to_string()),
285            ]),
286        );
287        facts.set(
288            "tags",
289            FactValue::Array(vec![
290                FactValue::String("electronics".to_string()),
291                FactValue::String("gadgets".to_string()),
292            ]),
293        );
294        facts
295    }
296
297    #[test]
298    fn test_collect_operation() {
299        let facts = create_test_facts_with_array();
300        let op = MultifieldOp::Collect;
301
302        let result = op.evaluate(&facts, "items", None);
303        assert!(result.is_some());
304
305        let values = result.unwrap();
306        assert_eq!(values.len(), 3);
307        assert_eq!(values[0], FactValue::String("item1".to_string()));
308    }
309
310    #[test]
311    fn test_contains_operation() {
312        let facts = create_test_facts_with_array();
313        let op = MultifieldOp::Contains;
314        let search = FactValue::String("electronics".to_string());
315
316        let result = op.evaluate(&facts, "tags", Some(&search));
317        assert!(result.is_some());
318
319        let values = result.unwrap();
320        assert_eq!(values.len(), 1);
321        assert_eq!(values[0], FactValue::Boolean(true));
322    }
323
324    #[test]
325    fn test_count_operation() {
326        let facts = create_test_facts_with_array();
327        let op = MultifieldOp::Count;
328
329        let result = op.evaluate(&facts, "items", None);
330        assert!(result.is_some());
331
332        let values = result.unwrap();
333        assert_eq!(values[0], FactValue::Integer(3));
334    }
335
336    #[test]
337    fn test_first_last_operations() {
338        let facts = create_test_facts_with_array();
339
340        let first = MultifieldOp::First.evaluate(&facts, "items", None).unwrap();
341        assert_eq!(first[0], FactValue::String("item1".to_string()));
342
343        let last = MultifieldOp::Last.evaluate(&facts, "items", None).unwrap();
344        assert_eq!(last[0], FactValue::String("item3".to_string()));
345    }
346
347    #[test]
348    fn test_index_operation() {
349        let facts = create_test_facts_with_array();
350        let op = MultifieldOp::Index(1);
351
352        let result = op.evaluate(&facts, "items", None).unwrap();
353        assert_eq!(result[0], FactValue::String("item2".to_string()));
354    }
355
356    #[test]
357    fn test_slice_operation() {
358        let facts = create_test_facts_with_array();
359        let op = MultifieldOp::Slice(0, 2);
360
361        let result = op.evaluate(&facts, "items", None).unwrap();
362        assert_eq!(result.len(), 2);
363        assert_eq!(result[0], FactValue::String("item1".to_string()));
364        assert_eq!(result[1], FactValue::String("item2".to_string()));
365    }
366
367    #[test]
368    fn test_is_empty_operation() {
369        let mut facts = TypedFacts::new();
370        facts.set("empty_array", FactValue::Array(Vec::new()));
371
372        let op = MultifieldOp::IsEmpty;
373        let result = op.evaluate(&facts, "empty_array", None).unwrap();
374        assert_eq!(result[0], FactValue::Boolean(true));
375    }
376
377    #[test]
378    fn test_parse_multifield_variable() {
379        assert_eq!(
380            parse_multifield_variable("$?items"),
381            Some("items".to_string())
382        );
383        assert_eq!(parse_multifield_variable("$?all"), Some("all".to_string()));
384        assert_eq!(parse_multifield_variable("$single"), None);
385        assert_eq!(parse_multifield_variable("items"), None);
386    }
387
388    #[test]
389    fn test_is_multifield_variable() {
390        assert!(is_multifield_variable("$?items"));
391        assert!(is_multifield_variable("$?all"));
392        assert!(!is_multifield_variable("$single"));
393        assert!(!is_multifield_variable("items"));
394    }
395
396    #[test]
397    fn test_evaluate_multifield_pattern_with_binding() {
398        let facts = create_test_facts_with_array();
399        let bindings = HashMap::new();
400
401        let result = evaluate_multifield_pattern(
402            &facts,
403            "items",
404            &MultifieldOp::Collect,
405            Some("$?all_items"),
406            None,
407            &bindings,
408        );
409
410        assert!(result.is_some());
411        let new_bindings = result.unwrap();
412
413        assert!(new_bindings.contains_key("$?all_items"));
414        if let FactValue::Array(arr) = &new_bindings["$?all_items"] {
415            assert_eq!(arr.len(), 3);
416        } else {
417            panic!("Expected array binding");
418        }
419    }
420
421    #[test]
422    fn test_evaluate_multifield_pattern_contains() {
423        let facts = create_test_facts_with_array();
424        let bindings = HashMap::new();
425        let search = FactValue::String("electronics".to_string());
426
427        let result = evaluate_multifield_pattern(
428            &facts,
429            "tags",
430            &MultifieldOp::Contains,
431            None,
432            Some(&search),
433            &bindings,
434        );
435
436        assert!(result.is_some()); // Should match
437
438        // Test with non-existent value
439        let search2 = FactValue::String("nonexistent".to_string());
440        let result2 = evaluate_multifield_pattern(
441            &facts,
442            "tags",
443            &MultifieldOp::Contains,
444            None,
445            Some(&search2),
446            &bindings,
447        );
448
449        assert!(result2.is_none()); // Should not match
450    }
451}