use crate::rete::facts::{FactValue, TypedFacts};
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq)]
pub enum MultifieldOp {
Collect,
Contains,
Count,
First,
Last,
Index(usize),
Slice(usize, usize),
IsEmpty,
NotEmpty,
}
impl MultifieldOp {
pub fn evaluate(
&self,
facts: &TypedFacts,
field: &str,
value: Option<&FactValue>,
) -> Option<Vec<FactValue>> {
let field_value = facts.get(field)?;
let array = match field_value {
FactValue::Array(arr) => arr,
_ => return None, };
match self {
MultifieldOp::Collect => {
Some(array.clone())
}
MultifieldOp::Contains => {
let search_value = value?;
let contains = array.contains(search_value);
Some(vec![FactValue::Boolean(contains)])
}
MultifieldOp::Count => {
Some(vec![FactValue::Integer(array.len() as i64)])
}
MultifieldOp::First => {
array.first().cloned().map(|v| vec![v])
}
MultifieldOp::Last => {
array.last().cloned().map(|v| vec![v])
}
MultifieldOp::Index(idx) => {
array.get(*idx).cloned().map(|v| vec![v])
}
MultifieldOp::Slice(start, end) => {
let end = (*end).min(array.len());
if *start >= end {
return Some(Vec::new());
}
Some(array[*start..end].to_vec())
}
MultifieldOp::IsEmpty => {
Some(vec![FactValue::Boolean(array.is_empty())])
}
MultifieldOp::NotEmpty => {
Some(vec![FactValue::Boolean(!array.is_empty())])
}
}
}
#[allow(clippy::inherent_to_string)]
pub fn to_string(&self) -> String {
match self {
MultifieldOp::Collect => "collect".to_string(),
MultifieldOp::Contains => "contains".to_string(),
MultifieldOp::Count => "count".to_string(),
MultifieldOp::First => "first".to_string(),
MultifieldOp::Last => "last".to_string(),
MultifieldOp::Index(idx) => format!("[{}]", idx),
MultifieldOp::Slice(start, end) => format!("[{}:{}]", start, end),
MultifieldOp::IsEmpty => "empty".to_string(),
MultifieldOp::NotEmpty => "not_empty".to_string(),
}
}
}
pub fn evaluate_multifield_pattern(
facts: &TypedFacts,
field: &str,
operator: &MultifieldOp,
variable: Option<&str>,
value: Option<&FactValue>,
bindings: &HashMap<String, FactValue>,
) -> Option<HashMap<String, FactValue>> {
let result = operator.evaluate(facts, field, value)?;
let mut new_bindings = bindings.clone();
if let Some(var_name) = variable {
if matches!(operator, MultifieldOp::Collect) {
new_bindings.insert(var_name.to_string(), FactValue::Array(result));
} else {
if result.len() == 1 {
new_bindings.insert(var_name.to_string(), result[0].clone());
} else {
new_bindings.insert(var_name.to_string(), FactValue::Array(result));
}
}
} else {
if result.len() == 1 {
if let FactValue::Boolean(b) = result[0] {
if !b {
return None; }
}
}
}
Some(new_bindings)
}
pub fn parse_multifield_variable(input: &str) -> Option<String> {
let trimmed = input.trim();
trimmed.strip_prefix("$?").map(|s| s.to_string())
}
pub fn is_multifield_variable(input: &str) -> bool {
input.trim().starts_with("$?")
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_facts_with_array() -> TypedFacts {
let mut facts = TypedFacts::new();
facts.set(
"items",
FactValue::Array(vec![
FactValue::String("item1".to_string()),
FactValue::String("item2".to_string()),
FactValue::String("item3".to_string()),
]),
);
facts.set(
"tags",
FactValue::Array(vec![
FactValue::String("electronics".to_string()),
FactValue::String("gadgets".to_string()),
]),
);
facts
}
#[test]
fn test_collect_operation() {
let facts = create_test_facts_with_array();
let op = MultifieldOp::Collect;
let result = op.evaluate(&facts, "items", None);
assert!(result.is_some());
let values = result.unwrap();
assert_eq!(values.len(), 3);
assert_eq!(values[0], FactValue::String("item1".to_string()));
}
#[test]
fn test_contains_operation() {
let facts = create_test_facts_with_array();
let op = MultifieldOp::Contains;
let search = FactValue::String("electronics".to_string());
let result = op.evaluate(&facts, "tags", Some(&search));
assert!(result.is_some());
let values = result.unwrap();
assert_eq!(values.len(), 1);
assert_eq!(values[0], FactValue::Boolean(true));
}
#[test]
fn test_count_operation() {
let facts = create_test_facts_with_array();
let op = MultifieldOp::Count;
let result = op.evaluate(&facts, "items", None);
assert!(result.is_some());
let values = result.unwrap();
assert_eq!(values[0], FactValue::Integer(3));
}
#[test]
fn test_first_last_operations() {
let facts = create_test_facts_with_array();
let first = MultifieldOp::First.evaluate(&facts, "items", None).unwrap();
assert_eq!(first[0], FactValue::String("item1".to_string()));
let last = MultifieldOp::Last.evaluate(&facts, "items", None).unwrap();
assert_eq!(last[0], FactValue::String("item3".to_string()));
}
#[test]
fn test_index_operation() {
let facts = create_test_facts_with_array();
let op = MultifieldOp::Index(1);
let result = op.evaluate(&facts, "items", None).unwrap();
assert_eq!(result[0], FactValue::String("item2".to_string()));
}
#[test]
fn test_slice_operation() {
let facts = create_test_facts_with_array();
let op = MultifieldOp::Slice(0, 2);
let result = op.evaluate(&facts, "items", None).unwrap();
assert_eq!(result.len(), 2);
assert_eq!(result[0], FactValue::String("item1".to_string()));
assert_eq!(result[1], FactValue::String("item2".to_string()));
}
#[test]
fn test_is_empty_operation() {
let mut facts = TypedFacts::new();
facts.set("empty_array", FactValue::Array(Vec::new()));
let op = MultifieldOp::IsEmpty;
let result = op.evaluate(&facts, "empty_array", None).unwrap();
assert_eq!(result[0], FactValue::Boolean(true));
}
#[test]
fn test_parse_multifield_variable() {
assert_eq!(
parse_multifield_variable("$?items"),
Some("items".to_string())
);
assert_eq!(parse_multifield_variable("$?all"), Some("all".to_string()));
assert_eq!(parse_multifield_variable("$single"), None);
assert_eq!(parse_multifield_variable("items"), None);
}
#[test]
fn test_is_multifield_variable() {
assert!(is_multifield_variable("$?items"));
assert!(is_multifield_variable("$?all"));
assert!(!is_multifield_variable("$single"));
assert!(!is_multifield_variable("items"));
}
#[test]
fn test_evaluate_multifield_pattern_with_binding() {
let facts = create_test_facts_with_array();
let bindings = HashMap::new();
let result = evaluate_multifield_pattern(
&facts,
"items",
&MultifieldOp::Collect,
Some("$?all_items"),
None,
&bindings,
);
assert!(result.is_some());
let new_bindings = result.unwrap();
assert!(new_bindings.contains_key("$?all_items"));
if let FactValue::Array(arr) = &new_bindings["$?all_items"] {
assert_eq!(arr.len(), 3);
} else {
panic!("Expected array binding");
}
}
#[test]
fn test_evaluate_multifield_pattern_contains() {
let facts = create_test_facts_with_array();
let bindings = HashMap::new();
let search = FactValue::String("electronics".to_string());
let result = evaluate_multifield_pattern(
&facts,
"tags",
&MultifieldOp::Contains,
None,
Some(&search),
&bindings,
);
assert!(result.is_some());
let search2 = FactValue::String("nonexistent".to_string());
let result2 = evaluate_multifield_pattern(
&facts,
"tags",
&MultifieldOp::Contains,
None,
Some(&search2),
&bindings,
);
assert!(result2.is_none()); }
}