use crate::engine::rule::Rule;
use crate::types::ActionType;
use std::collections::{HashMap, HashSet};
#[derive(Debug, Clone)]
pub struct ConclusionIndex {
field_to_rules: HashMap<String, HashSet<String>>,
rule_to_conclusions: HashMap<String, HashSet<String>>,
rule_count: usize,
}
impl ConclusionIndex {
pub fn new() -> Self {
Self {
field_to_rules: HashMap::new(),
rule_to_conclusions: HashMap::new(),
rule_count: 0,
}
}
pub fn from_rules(rules: &[Rule]) -> Self {
let mut index = Self::new();
for rule in rules {
index.add_rule(rule);
}
index
}
pub fn add_rule(&mut self, rule: &Rule) {
if !rule.enabled {
return; }
let conclusions = self.extract_conclusions(rule);
if conclusions.is_empty() {
return; }
for conclusion in &conclusions {
self.field_to_rules
.entry(conclusion.clone())
.or_default()
.insert(rule.name.clone());
}
self.rule_to_conclusions
.insert(rule.name.clone(), conclusions);
self.rule_count += 1;
}
pub fn remove_rule(&mut self, rule_name: &str) {
if let Some(conclusions) = self.rule_to_conclusions.remove(rule_name) {
for conclusion in conclusions {
if let Some(rules) = self.field_to_rules.get_mut(&conclusion) {
rules.remove(rule_name);
if rules.is_empty() {
self.field_to_rules.remove(&conclusion);
}
}
}
self.rule_count -= 1;
}
}
pub fn find_candidates(&self, goal_pattern: &str) -> HashSet<String> {
let mut candidates = HashSet::new();
let field = self.extract_field_from_goal(goal_pattern);
if let Some(rules) = self.field_to_rules.get(field) {
candidates.extend(rules.iter().cloned());
}
if let Some(dot_pos) = field.rfind('.') {
let object = &field[..dot_pos];
for (indexed_field, rules) in &self.field_to_rules {
if indexed_field.starts_with(object) {
candidates.extend(rules.iter().cloned());
}
}
}
candidates
}
fn extract_field_from_goal<'a>(&self, goal_pattern: &'a str) -> &'a str {
for op in &["==", "!=", ">=", "<=", ">", "<", " contains ", " matches "] {
if let Some(pos) = goal_pattern.find(op) {
return goal_pattern[..pos].trim();
}
}
goal_pattern.trim()
}
fn extract_conclusions(&self, rule: &Rule) -> HashSet<String> {
let mut conclusions = HashSet::new();
for action in &rule.actions {
match action {
ActionType::Set { field, .. } => {
conclusions.insert(field.clone());
}
ActionType::MethodCall { object, method, .. } => {
conclusions.insert(format!("{}.{}", object, method));
conclusions.insert(object.clone());
}
ActionType::Retract { object } => {
conclusions.insert(object.clone());
}
ActionType::SetWorkflowData { key, .. } => {
conclusions.insert(key.clone());
}
_ => {}
}
}
conclusions
}
pub fn stats(&self) -> IndexStats {
IndexStats {
total_rules: self.rule_count,
indexed_fields: self.field_to_rules.len(),
avg_rules_per_field: if self.field_to_rules.is_empty() {
0.0
} else {
self.field_to_rules.values().map(|s| s.len()).sum::<usize>() as f64
/ self.field_to_rules.len() as f64
},
}
}
pub fn clear(&mut self) {
self.field_to_rules.clear();
self.rule_to_conclusions.clear();
self.rule_count = 0;
}
pub fn is_empty(&self) -> bool {
self.rule_count == 0
}
}
impl Default for ConclusionIndex {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct IndexStats {
pub total_rules: usize,
pub indexed_fields: usize,
pub avg_rules_per_field: f64,
}
impl std::fmt::Display for IndexStats {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "Conclusion Index Statistics:")?;
writeln!(f, " Total Rules: {}", self.total_rules)?;
writeln!(f, " Indexed Fields: {}", self.indexed_fields)?;
writeln!(f, " Avg Rules/Field: {:.2}", self.avg_rules_per_field)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::engine::rule::{Condition, ConditionGroup, Rule};
use crate::types::{Operator, Value};
fn create_test_rule(name: &str, set_field: &str) -> Rule {
let conditions = ConditionGroup::Single(Condition::new(
"dummy".to_string(),
Operator::Equal,
Value::Boolean(true),
));
let actions = vec![ActionType::Set {
field: set_field.to_string(),
value: Value::Boolean(true),
}];
Rule::new(name.to_string(), conditions, actions)
}
#[test]
fn test_index_creation() {
let index = ConclusionIndex::new();
assert!(index.is_empty());
assert_eq!(index.rule_count, 0);
}
#[test]
fn test_add_single_rule() {
let mut index = ConclusionIndex::new();
let rule = create_test_rule("TestRule", "User.IsVIP");
index.add_rule(&rule);
assert_eq!(index.rule_count, 1);
assert_eq!(index.field_to_rules.len(), 1);
assert!(index.field_to_rules.contains_key("User.IsVIP"));
}
#[test]
fn test_find_candidates_exact_match() {
let mut index = ConclusionIndex::new();
let rule = create_test_rule("DetermineVIP", "User.IsVIP");
index.add_rule(&rule);
let candidates = index.find_candidates("User.IsVIP == true");
assert_eq!(candidates.len(), 1);
assert!(candidates.contains("DetermineVIP"));
}
#[test]
fn test_find_candidates_multiple_rules() {
let mut index = ConclusionIndex::new();
index.add_rule(&create_test_rule("Rule1", "User.IsVIP"));
index.add_rule(&create_test_rule("Rule2", "User.IsVIP"));
index.add_rule(&create_test_rule("Rule3", "Order.Status"));
let candidates = index.find_candidates("User.IsVIP == true");
assert_eq!(candidates.len(), 2);
assert!(candidates.contains("Rule1"));
assert!(candidates.contains("Rule2"));
assert!(!candidates.contains("Rule3"));
}
#[test]
fn test_remove_rule() {
let mut index = ConclusionIndex::new();
let rule = create_test_rule("TestRule", "User.IsVIP");
index.add_rule(&rule);
assert_eq!(index.rule_count, 1);
index.remove_rule("TestRule");
assert_eq!(index.rule_count, 0);
assert!(index.is_empty());
assert!(index.field_to_rules.is_empty());
}
#[test]
fn test_extract_field_from_goal() {
let index = ConclusionIndex::new();
assert_eq!(
index.extract_field_from_goal("User.IsVIP == true"),
"User.IsVIP"
);
assert_eq!(
index.extract_field_from_goal("Order.Amount > 100"),
"Order.Amount"
);
assert_eq!(index.extract_field_from_goal("User.Name"), "User.Name");
assert_eq!(
index.extract_field_from_goal("Customer.Email contains '@'"),
"Customer.Email"
);
}
#[test]
fn test_disabled_rules_not_indexed() {
let mut index = ConclusionIndex::new();
let mut rule = create_test_rule("DisabledRule", "User.IsVIP");
rule.enabled = false;
index.add_rule(&rule);
assert_eq!(index.rule_count, 0);
assert!(index.is_empty());
}
#[test]
fn test_from_rules_bulk_creation() {
let rules = vec![
create_test_rule("Rule1", "User.IsVIP"),
create_test_rule("Rule2", "Order.Status"),
create_test_rule("Rule3", "Customer.Rating"),
];
let index = ConclusionIndex::from_rules(&rules);
assert_eq!(index.rule_count, 3);
assert_eq!(index.field_to_rules.len(), 3);
}
#[test]
fn test_stats() {
let mut index = ConclusionIndex::new();
index.add_rule(&create_test_rule("Rule1", "User.IsVIP"));
index.add_rule(&create_test_rule("Rule2", "User.IsVIP"));
index.add_rule(&create_test_rule("Rule3", "Order.Status"));
let stats = index.stats();
assert_eq!(stats.total_rules, 3);
assert_eq!(stats.indexed_fields, 2);
assert!(stats.avg_rules_per_field > 0.0);
}
}