rust_rule_engine/backward/
conclusion_index.rs1use crate::engine::rule::Rule;
10use crate::types::ActionType;
11use std::collections::{HashMap, HashSet};
12
13#[derive(Debug, Clone)]
18pub struct ConclusionIndex {
19 field_to_rules: HashMap<String, HashSet<String>>,
22
23 rule_to_conclusions: HashMap<String, HashSet<String>>,
25
26 rule_count: usize,
28}
29
30impl ConclusionIndex {
31 pub fn new() -> Self {
33 Self {
34 field_to_rules: HashMap::new(),
35 rule_to_conclusions: HashMap::new(),
36 rule_count: 0,
37 }
38 }
39
40 pub fn from_rules(rules: &[Rule]) -> Self {
42 let mut index = Self::new();
43 for rule in rules {
44 index.add_rule(rule);
45 }
46 index
47 }
48
49 pub fn add_rule(&mut self, rule: &Rule) {
51 if !rule.enabled {
52 return; }
54
55 let conclusions = self.extract_conclusions(rule);
56
57 if conclusions.is_empty() {
58 return; }
60
61 for conclusion in &conclusions {
63 self.field_to_rules
64 .entry(conclusion.clone())
65 .or_default()
66 .insert(rule.name.clone());
67 }
68
69 self.rule_to_conclusions
70 .insert(rule.name.clone(), conclusions);
71 self.rule_count += 1;
72 }
73
74 pub fn remove_rule(&mut self, rule_name: &str) {
76 if let Some(conclusions) = self.rule_to_conclusions.remove(rule_name) {
77 for conclusion in conclusions {
78 if let Some(rules) = self.field_to_rules.get_mut(&conclusion) {
79 rules.remove(rule_name);
80 if rules.is_empty() {
81 self.field_to_rules.remove(&conclusion);
82 }
83 }
84 }
85 self.rule_count -= 1;
86 }
87 }
88
89 pub fn find_candidates(&self, goal_pattern: &str) -> HashSet<String> {
99 let mut candidates = HashSet::new();
100
101 let field = self.extract_field_from_goal(goal_pattern);
107
108 if let Some(rules) = self.field_to_rules.get(field) {
110 candidates.extend(rules.iter().cloned());
111 }
112
113 if let Some(dot_pos) = field.rfind('.') {
116 let object = &field[..dot_pos];
117
118 for (indexed_field, rules) in &self.field_to_rules {
120 if indexed_field.starts_with(object) {
121 candidates.extend(rules.iter().cloned());
122 }
123 }
124 }
125
126 candidates
127 }
128
129 fn extract_field_from_goal<'a>(&self, goal_pattern: &'a str) -> &'a str {
131 for op in &["==", "!=", ">=", "<=", ">", "<", " contains ", " matches "] {
133 if let Some(pos) = goal_pattern.find(op) {
134 return goal_pattern[..pos].trim();
135 }
136 }
137
138 goal_pattern.trim()
140 }
141
142 fn extract_conclusions(&self, rule: &Rule) -> HashSet<String> {
144 let mut conclusions = HashSet::new();
145
146 for action in &rule.actions {
147 match action {
148 ActionType::Set { field, .. } => {
149 conclusions.insert(field.clone());
150 }
151 ActionType::MethodCall { object, method, .. } => {
152 conclusions.insert(format!("{}.{}", object, method));
154 conclusions.insert(object.clone());
156 }
157 ActionType::Retract { object } => {
158 conclusions.insert(object.clone());
159 }
160 ActionType::SetWorkflowData { key, .. } => {
161 conclusions.insert(key.clone());
162 }
163 _ => {}
165 }
166 }
167
168 conclusions
169 }
170
171 pub fn stats(&self) -> IndexStats {
173 IndexStats {
174 total_rules: self.rule_count,
175 indexed_fields: self.field_to_rules.len(),
176 avg_rules_per_field: if self.field_to_rules.is_empty() {
177 0.0
178 } else {
179 self.field_to_rules.values().map(|s| s.len()).sum::<usize>() as f64
180 / self.field_to_rules.len() as f64
181 },
182 }
183 }
184
185 pub fn clear(&mut self) {
187 self.field_to_rules.clear();
188 self.rule_to_conclusions.clear();
189 self.rule_count = 0;
190 }
191
192 pub fn is_empty(&self) -> bool {
194 self.rule_count == 0
195 }
196}
197
198impl Default for ConclusionIndex {
199 fn default() -> Self {
200 Self::new()
201 }
202}
203
204#[derive(Debug, Clone)]
206pub struct IndexStats {
207 pub total_rules: usize,
209 pub indexed_fields: usize,
211 pub avg_rules_per_field: f64,
213}
214
215impl std::fmt::Display for IndexStats {
216 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
217 writeln!(f, "Conclusion Index Statistics:")?;
218 writeln!(f, " Total Rules: {}", self.total_rules)?;
219 writeln!(f, " Indexed Fields: {}", self.indexed_fields)?;
220 writeln!(f, " Avg Rules/Field: {:.2}", self.avg_rules_per_field)?;
221 Ok(())
222 }
223}
224
225#[cfg(test)]
226mod tests {
227 use super::*;
228 use crate::engine::rule::{Condition, ConditionGroup, Rule};
229 use crate::types::{Operator, Value};
230
231 fn create_test_rule(name: &str, set_field: &str) -> Rule {
232 let conditions = ConditionGroup::Single(Condition::new(
233 "dummy".to_string(),
234 Operator::Equal,
235 Value::Boolean(true),
236 ));
237 let actions = vec![ActionType::Set {
238 field: set_field.to_string(),
239 value: Value::Boolean(true),
240 }];
241 Rule::new(name.to_string(), conditions, actions)
242 }
243
244 #[test]
245 fn test_index_creation() {
246 let index = ConclusionIndex::new();
247 assert!(index.is_empty());
248 assert_eq!(index.rule_count, 0);
249 }
250
251 #[test]
252 fn test_add_single_rule() {
253 let mut index = ConclusionIndex::new();
254 let rule = create_test_rule("TestRule", "User.IsVIP");
255
256 index.add_rule(&rule);
257
258 assert_eq!(index.rule_count, 1);
259 assert_eq!(index.field_to_rules.len(), 1);
260 assert!(index.field_to_rules.contains_key("User.IsVIP"));
261 }
262
263 #[test]
264 fn test_find_candidates_exact_match() {
265 let mut index = ConclusionIndex::new();
266 let rule = create_test_rule("DetermineVIP", "User.IsVIP");
267 index.add_rule(&rule);
268
269 let candidates = index.find_candidates("User.IsVIP == true");
270
271 assert_eq!(candidates.len(), 1);
272 assert!(candidates.contains("DetermineVIP"));
273 }
274
275 #[test]
276 fn test_find_candidates_multiple_rules() {
277 let mut index = ConclusionIndex::new();
278 index.add_rule(&create_test_rule("Rule1", "User.IsVIP"));
279 index.add_rule(&create_test_rule("Rule2", "User.IsVIP"));
280 index.add_rule(&create_test_rule("Rule3", "Order.Status"));
281
282 let candidates = index.find_candidates("User.IsVIP == true");
283
284 assert_eq!(candidates.len(), 2);
285 assert!(candidates.contains("Rule1"));
286 assert!(candidates.contains("Rule2"));
287 assert!(!candidates.contains("Rule3"));
288 }
289
290 #[test]
291 fn test_remove_rule() {
292 let mut index = ConclusionIndex::new();
293 let rule = create_test_rule("TestRule", "User.IsVIP");
294 index.add_rule(&rule);
295
296 assert_eq!(index.rule_count, 1);
297
298 index.remove_rule("TestRule");
299
300 assert_eq!(index.rule_count, 0);
301 assert!(index.is_empty());
302 assert!(index.field_to_rules.is_empty());
303 }
304
305 #[test]
306 fn test_extract_field_from_goal() {
307 let index = ConclusionIndex::new();
308
309 assert_eq!(
310 index.extract_field_from_goal("User.IsVIP == true"),
311 "User.IsVIP"
312 );
313 assert_eq!(
314 index.extract_field_from_goal("Order.Amount > 100"),
315 "Order.Amount"
316 );
317 assert_eq!(index.extract_field_from_goal("User.Name"), "User.Name");
318 assert_eq!(
319 index.extract_field_from_goal("Customer.Email contains '@'"),
320 "Customer.Email"
321 );
322 }
323
324 #[test]
325 fn test_disabled_rules_not_indexed() {
326 let mut index = ConclusionIndex::new();
327 let mut rule = create_test_rule("DisabledRule", "User.IsVIP");
328 rule.enabled = false;
329
330 index.add_rule(&rule);
331
332 assert_eq!(index.rule_count, 0);
333 assert!(index.is_empty());
334 }
335
336 #[test]
337 fn test_from_rules_bulk_creation() {
338 let rules = vec![
339 create_test_rule("Rule1", "User.IsVIP"),
340 create_test_rule("Rule2", "Order.Status"),
341 create_test_rule("Rule3", "Customer.Rating"),
342 ];
343
344 let index = ConclusionIndex::from_rules(&rules);
345
346 assert_eq!(index.rule_count, 3);
347 assert_eq!(index.field_to_rules.len(), 3);
348 }
349
350 #[test]
351 fn test_stats() {
352 let mut index = ConclusionIndex::new();
353 index.add_rule(&create_test_rule("Rule1", "User.IsVIP"));
354 index.add_rule(&create_test_rule("Rule2", "User.IsVIP"));
355 index.add_rule(&create_test_rule("Rule3", "Order.Status"));
356
357 let stats = index.stats();
358
359 assert_eq!(stats.total_rules, 3);
360 assert_eq!(stats.indexed_fields, 2);
361 assert!(stats.avg_rules_per_field > 0.0);
362 }
363}