1#![allow(dead_code)]
4
5use std::collections::HashMap;
8
9#[derive(Debug, Clone, PartialEq)]
11pub enum Condition {
12 Equals(String, String),
13 NotEquals(String, String),
14 Contains(String, String),
15 Absent(String),
16 Present(String),
17}
18
19#[derive(Debug, Clone)]
21pub struct RuleAction {
22 pub name: String,
23 pub payload: String,
24}
25
26#[derive(Debug, Clone)]
28pub struct Rule {
29 pub name: String,
30 pub conditions: Vec<Condition>,
31 pub actions: Vec<RuleAction>,
32 pub priority: i32,
33 pub enabled: bool,
34}
35
36pub struct RuleEngine {
38 rules: Vec<Rule>,
39 fire_count: u64,
40}
41
42fn eval_condition(cond: &Condition, facts: &HashMap<String, String>) -> bool {
43 match cond {
44 Condition::Equals(k, v) => facts.get(k).is_some_and(|fv| fv == v),
45 Condition::NotEquals(k, v) => facts.get(k).is_some_and(|fv| fv != v),
46 Condition::Contains(k, sub) => facts.get(k).is_some_and(|fv| fv.contains(sub.as_str())),
47 Condition::Absent(k) => !facts.contains_key(k),
48 Condition::Present(k) => facts.contains_key(k),
49 }
50}
51
52#[allow(dead_code)]
53impl RuleEngine {
54 pub fn new() -> Self {
55 RuleEngine {
56 rules: Vec::new(),
57 fire_count: 0,
58 }
59 }
60
61 pub fn add_rule(&mut self, rule: Rule) {
62 let pos = self.rules.partition_point(|r| r.priority > rule.priority);
63 self.rules.insert(pos, rule);
64 }
65
66 pub fn evaluate(&mut self, facts: &HashMap<String, String>) -> Vec<RuleAction> {
67 let mut actions = Vec::new();
68 for rule in &self.rules {
69 if !rule.enabled {
70 continue;
71 }
72 if rule.conditions.iter().all(|c| eval_condition(c, facts)) {
73 self.fire_count += 1;
74 actions.extend(rule.actions.clone());
75 }
76 }
77 actions
78 }
79
80 pub fn set_enabled(&mut self, name: &str, enabled: bool) -> bool {
81 if let Some(r) = self.rules.iter_mut().find(|r| r.name == name) {
82 r.enabled = enabled;
83 true
84 } else {
85 false
86 }
87 }
88
89 pub fn remove_rule(&mut self, name: &str) -> bool {
90 let before = self.rules.len();
91 self.rules.retain(|r| r.name != name);
92 self.rules.len() < before
93 }
94
95 pub fn rule_count(&self) -> usize {
96 self.rules.len()
97 }
98
99 pub fn enabled_count(&self) -> usize {
100 self.rules.iter().filter(|r| r.enabled).count()
101 }
102
103 pub fn fire_count(&self) -> u64 {
104 self.fire_count
105 }
106
107 pub fn clear(&mut self) {
108 self.rules.clear();
109 }
110
111 pub fn rule_names(&self) -> Vec<&str> {
112 self.rules.iter().map(|r| r.name.as_str()).collect()
113 }
114}
115
116impl Default for RuleEngine {
117 fn default() -> Self {
118 Self::new()
119 }
120}
121
122pub fn new_rule_engine() -> RuleEngine {
123 RuleEngine::new()
124}
125
126pub fn make_rule(
127 name: &str,
128 conds: Vec<Condition>,
129 actions: Vec<RuleAction>,
130 priority: i32,
131) -> Rule {
132 Rule {
133 name: name.to_string(),
134 conditions: conds,
135 actions,
136 priority,
137 enabled: true,
138 }
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144
145 fn facts(pairs: &[(&str, &str)]) -> HashMap<String, String> {
146 pairs
147 .iter()
148 .map(|(k, v)| (k.to_string(), v.to_string()))
149 .collect()
150 }
151
152 #[test]
153 fn equals_condition_fires() {
154 let mut engine = new_rule_engine();
155 let rule = make_rule(
156 "r1",
157 vec![Condition::Equals(
158 "status".to_string(),
159 "active".to_string(),
160 )],
161 vec![RuleAction {
162 name: "activate".to_string(),
163 payload: String::new(),
164 }],
165 0,
166 );
167 engine.add_rule(rule);
168 let f = facts(&[("status", "active")]);
169 let actions = engine.evaluate(&f);
170 assert_eq!(actions.len(), 1);
171 assert_eq!(actions[0].name, "activate");
172 }
173
174 #[test]
175 fn not_equals_fires() {
176 let mut engine = new_rule_engine();
177 let rule = make_rule(
178 "r1",
179 vec![Condition::NotEquals("x".to_string(), "bad".to_string())],
180 vec![RuleAction {
181 name: "ok".to_string(),
182 payload: String::new(),
183 }],
184 0,
185 );
186 engine.add_rule(rule);
187 let f = facts(&[("x", "good")]);
188 assert_eq!(engine.evaluate(&f).len(), 1);
189 }
190
191 #[test]
192 fn present_condition() {
193 let mut engine = new_rule_engine();
194 engine.add_rule(make_rule(
195 "r1",
196 vec![Condition::Present("key".to_string())],
197 vec![RuleAction {
198 name: "found".to_string(),
199 payload: String::new(),
200 }],
201 0,
202 ));
203 assert_eq!(engine.evaluate(&facts(&[("key", "val")])).len(), 1);
204 assert_eq!(engine.evaluate(&facts(&[])).len(), 0);
205 }
206
207 #[test]
208 fn absent_condition() {
209 let mut engine = new_rule_engine();
210 engine.add_rule(make_rule(
211 "r1",
212 vec![Condition::Absent("ghost".to_string())],
213 vec![RuleAction {
214 name: "ok".to_string(),
215 payload: String::new(),
216 }],
217 0,
218 ));
219 assert_eq!(engine.evaluate(&facts(&[])).len(), 1);
220 }
221
222 #[test]
223 fn disabled_rule_skipped() {
224 let mut engine = new_rule_engine();
225 engine.add_rule(make_rule(
226 "r1",
227 vec![],
228 vec![RuleAction {
229 name: "fire".to_string(),
230 payload: String::new(),
231 }],
232 0,
233 ));
234 engine.set_enabled("r1", false);
235 assert_eq!(engine.evaluate(&facts(&[])).len(), 0);
236 }
237
238 #[test]
239 fn fire_count_tracked() {
240 let mut engine = new_rule_engine();
241 engine.add_rule(make_rule(
242 "r1",
243 vec![],
244 vec![RuleAction {
245 name: "a".to_string(),
246 payload: String::new(),
247 }],
248 0,
249 ));
250 engine.evaluate(&facts(&[]));
251 engine.evaluate(&facts(&[]));
252 assert_eq!(engine.fire_count(), 2);
253 }
254
255 #[test]
256 fn remove_rule() {
257 let mut engine = new_rule_engine();
258 engine.add_rule(make_rule("r1", vec![], vec![], 0));
259 assert!(engine.remove_rule("r1"));
260 assert_eq!(engine.rule_count(), 0);
261 }
262
263 #[test]
264 fn priority_ordering() {
265 let mut engine = new_rule_engine();
266 engine.add_rule(make_rule(
267 "low",
268 vec![],
269 vec![RuleAction {
270 name: "low".to_string(),
271 payload: String::new(),
272 }],
273 1,
274 ));
275 engine.add_rule(make_rule(
276 "high",
277 vec![],
278 vec![RuleAction {
279 name: "high".to_string(),
280 payload: String::new(),
281 }],
282 10,
283 ));
284 let names = engine.rule_names();
285 assert_eq!(names[0], "high");
286 }
287
288 #[test]
289 fn contains_condition() {
290 let mut engine = new_rule_engine();
291 engine.add_rule(make_rule(
292 "r1",
293 vec![Condition::Contains("msg".to_string(), "err".to_string())],
294 vec![RuleAction {
295 name: "alert".to_string(),
296 payload: String::new(),
297 }],
298 0,
299 ));
300 assert_eq!(engine.evaluate(&facts(&[("msg", "some error")])).len(), 1);
301 assert_eq!(engine.evaluate(&facts(&[("msg", "ok")])).len(), 0);
302 }
303}