rust_rule_engine/engine/
agenda.rs

1#![allow(deprecated)]
2
3use crate::engine::rule::Rule;
4use std::collections::{HashMap, HashSet};
5
6/// Manages agenda groups for workflow control
7#[derive(Debug, Clone)]
8pub struct AgendaManager {
9    /// Currently active agenda group
10    active_group: String,
11    /// Stack of focused agenda groups
12    focus_stack: Vec<String>,
13    /// Groups that have been activated for lock-on-active tracking
14    activated_groups: HashSet<String>,
15    /// Rules fired per agenda group activation (for lock-on-active)
16    fired_rules_per_activation: HashMap<String, HashSet<String>>,
17}
18
19impl Default for AgendaManager {
20    fn default() -> Self {
21        Self::new()
22    }
23}
24
25impl AgendaManager {
26    /// Create a new agenda manager with "MAIN" as default group
27    pub fn new() -> Self {
28        Self {
29            active_group: "MAIN".to_string(),
30            focus_stack: vec!["MAIN".to_string()],
31            activated_groups: HashSet::new(),
32            fired_rules_per_activation: HashMap::new(),
33        }
34    }
35
36    /// Set focus to a specific agenda group
37    pub fn set_focus(&mut self, group: &str) {
38        let group = group.to_string();
39
40        // Remove from stack if already exists
41        self.focus_stack.retain(|g| g != &group);
42
43        // Add to top of stack
44        self.focus_stack.push(group.clone());
45        self.active_group = group.clone();
46
47        // Mark group as activated
48        self.activated_groups.insert(group.clone());
49
50        // Clear fired rules for new activation
51        self.fired_rules_per_activation
52            .insert(group, HashSet::new());
53    }
54
55    /// Get the currently active agenda group
56    pub fn get_active_group(&self) -> &str {
57        &self.active_group
58    }
59
60    /// Check if a rule should be evaluated based on agenda group
61    pub fn should_evaluate_rule(&self, rule: &Rule) -> bool {
62        match &rule.agenda_group {
63            Some(group) => group == &self.active_group,
64            None => self.active_group == "MAIN", // Rules without group go to MAIN
65        }
66    }
67
68    /// Check if a rule can fire considering lock-on-active
69    pub fn can_fire_rule(&self, rule: &Rule) -> bool {
70        if !rule.lock_on_active {
71            return true;
72        }
73
74        let main_group = "MAIN".to_string();
75        let group = rule.agenda_group.as_ref().unwrap_or(&main_group);
76
77        // If group hasn't been activated yet, rule can fire
78        if !self.activated_groups.contains(group) {
79            return true;
80        }
81
82        // Check if rule has already fired in this activation
83        if let Some(fired_rules) = self.fired_rules_per_activation.get(group) {
84            !fired_rules.contains(&rule.name)
85        } else {
86            true
87        }
88    }
89
90    /// Mark a rule as fired for lock-on-active tracking
91    pub fn mark_rule_fired(&mut self, rule: &Rule) {
92        if rule.lock_on_active {
93            let main_group = "MAIN".to_string();
94            let group = rule.agenda_group.as_ref().unwrap_or(&main_group);
95
96            // Ensure group is activated
97            self.activated_groups.insert(group.clone());
98
99            // Track fired rule
100            self.fired_rules_per_activation
101                .entry(group.clone())
102                .or_default()
103                .insert(rule.name.clone());
104        }
105    }
106
107    /// Pop the focus stack (return to previous agenda group)
108    pub fn pop_focus(&mut self) -> Option<String> {
109        if self.focus_stack.len() > 1 {
110            self.focus_stack.pop();
111            if let Some(previous) = self.focus_stack.last() {
112                self.active_group = previous.clone();
113                Some(previous.clone())
114            } else {
115                None
116            }
117        } else {
118            None
119        }
120    }
121
122    /// Clear all focus and return to MAIN
123    pub fn clear_focus(&mut self) {
124        self.focus_stack.clear();
125        self.focus_stack.push("MAIN".to_string());
126        self.active_group = "MAIN".to_string();
127    }
128
129    /// Get all agenda groups with rules
130    pub fn get_agenda_groups(&self, rules: &[Rule]) -> Vec<String> {
131        let mut groups = HashSet::new();
132        groups.insert("MAIN".to_string());
133
134        for rule in rules {
135            if let Some(group) = &rule.agenda_group {
136                groups.insert(group.clone());
137            }
138        }
139
140        groups.into_iter().collect()
141    }
142
143    /// Filter rules by current agenda group
144    pub fn filter_rules<'a>(&self, rules: &'a [Rule]) -> Vec<&'a Rule> {
145        rules
146            .iter()
147            .filter(|rule| self.should_evaluate_rule(rule))
148            .collect()
149    }
150
151    /// Reset for new execution cycle
152    pub fn reset_cycle(&mut self) {
153        // For lock-on-active, we DON'T clear fired rules until agenda group changes
154        // Only clear for rules that are not lock-on-active
155        // This is different from activation groups which reset per cycle
156    }
157}
158
159/// Manages activation groups for mutually exclusive rule execution
160#[derive(Debug, Clone)]
161pub struct ActivationGroupManager {
162    /// Groups that have had a rule fire (only one rule per group can fire)
163    fired_groups: HashSet<String>,
164}
165
166impl Default for ActivationGroupManager {
167    fn default() -> Self {
168        Self::new()
169    }
170}
171
172impl ActivationGroupManager {
173    /// Create a new activation group manager
174    pub fn new() -> Self {
175        Self {
176            fired_groups: HashSet::new(),
177        }
178    }
179
180    /// Check if a rule can fire based on activation group constraints
181    pub fn can_fire(&self, rule: &Rule) -> bool {
182        if let Some(group) = &rule.activation_group {
183            !self.fired_groups.contains(group)
184        } else {
185            true // Rules without activation group can always fire
186        }
187    }
188
189    /// Mark that a rule has fired, preventing other rules in same activation group
190    pub fn mark_fired(&mut self, rule: &Rule) {
191        if let Some(group) = &rule.activation_group {
192            self.fired_groups.insert(group.clone());
193        }
194    }
195
196    /// Reset for new execution cycle
197    pub fn reset_cycle(&mut self) {
198        self.fired_groups.clear();
199    }
200
201    /// Get all activation groups
202    pub fn get_activation_groups(&self, rules: &[Rule]) -> Vec<String> {
203        rules
204            .iter()
205            .filter_map(|rule| rule.activation_group.clone())
206            .collect::<HashSet<_>>()
207            .into_iter()
208            .collect()
209    }
210
211    /// Check if any rule in an activation group has fired
212    pub fn has_group_fired(&self, group: &str) -> bool {
213        self.fired_groups.contains(group)
214    }
215}
216
217#[cfg(test)]
218mod tests {
219    use super::*;
220    use crate::engine::rule::{Condition, ConditionGroup, Rule};
221    use crate::types::{Operator, Value};
222
223    fn create_dummy_condition() -> ConditionGroup {
224        let condition = Condition {
225            expression: crate::engine::rule::ConditionExpression::Field("test".to_string()),
226            field: "test".to_string(),
227            operator: Operator::Equal,
228            value: Value::Boolean(true),
229        };
230        ConditionGroup::single(condition)
231    }
232
233    #[test]
234    fn test_agenda_manager_basic() {
235        let mut manager = AgendaManager::new();
236        assert_eq!(manager.get_active_group(), "MAIN");
237
238        manager.set_focus("validation");
239        assert_eq!(manager.get_active_group(), "validation");
240
241        manager.set_focus("processing");
242        assert_eq!(manager.get_active_group(), "processing");
243
244        manager.pop_focus();
245        assert_eq!(manager.get_active_group(), "validation");
246    }
247
248    #[test]
249    fn test_agenda_manager_rule_filtering() {
250        let mut manager = AgendaManager::new();
251
252        let rule1 = Rule::new("Rule1".to_string(), create_dummy_condition(), vec![])
253            .with_agenda_group("validation".to_string());
254        let rule2 = Rule::new("Rule2".to_string(), create_dummy_condition(), vec![]);
255
256        // Initially in MAIN, only rule2 should be evaluated
257        let rules = vec![rule1.clone(), rule2.clone()];
258        let filtered = manager.filter_rules(&rules);
259        assert_eq!(filtered.len(), 1);
260        assert_eq!(filtered[0].name, "Rule2");
261
262        // Switch to validation, only rule1 should be evaluated
263        manager.set_focus("validation");
264        let filtered = manager.filter_rules(&rules);
265        assert_eq!(filtered.len(), 1);
266        assert_eq!(filtered[0].name, "Rule1");
267    }
268
269    #[test]
270    fn test_activation_group_manager() {
271        let mut manager = ActivationGroupManager::new();
272
273        let rule1 = Rule::new("Rule1".to_string(), create_dummy_condition(), vec![])
274            .with_activation_group("discount".to_string());
275        let rule2 = Rule::new("Rule2".to_string(), create_dummy_condition(), vec![])
276            .with_activation_group("discount".to_string());
277
278        // Both rules can fire initially
279        assert!(manager.can_fire(&rule1));
280        assert!(manager.can_fire(&rule2));
281
282        // After rule1 fires, rule2 cannot fire
283        manager.mark_fired(&rule1);
284        assert!(!manager.can_fire(&rule2));
285        assert!(manager.has_group_fired("discount"));
286
287        // After reset, both can fire again
288        manager.reset_cycle();
289        assert!(manager.can_fire(&rule1));
290        assert!(manager.can_fire(&rule2));
291    }
292
293    #[test]
294    fn test_lock_on_active() {
295        let mut manager = AgendaManager::new();
296
297        let rule = Rule::new("TestRule".to_string(), create_dummy_condition(), vec![])
298            .with_lock_on_active(true);
299
300        // Rule can fire initially (MAIN group not activated yet)
301        assert!(manager.can_fire_rule(&rule));
302
303        // Mark rule as fired - this should activate MAIN group and track the rule
304        manager.mark_rule_fired(&rule);
305
306        // Now rule cannot fire again in the same activation
307        assert!(!manager.can_fire_rule(&rule));
308
309        // After cycle reset, still cannot fire (lock-on-active persists until group change)
310        manager.reset_cycle();
311        assert!(!manager.can_fire_rule(&rule));
312
313        // After switching to different group and back, rule can fire again
314        manager.set_focus("validation");
315        manager.set_focus("MAIN");
316        assert!(manager.can_fire_rule(&rule));
317    }
318}