1use crate::error::Result;
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct Rule {
10 pub id: String,
12
13 pub name: String,
15
16 pub conditions: HashMap<String, RuleCondition>,
18
19 pub actions: Vec<RuleAction>,
21
22 pub priority: i32,
24
25 pub enabled: bool,
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
31pub enum RuleCondition {
32 Equals(String),
34
35 Contains(String),
37
38 Matches(String),
40
41 GreaterThan(f64),
43 LessThan(f64),
44
45 In(Vec<String>),
47
48 Custom(String),
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
54pub enum RuleAction {
55 SetValue { key: String, value: String },
57
58 AddToList { key: String, value: String },
60
61 Log { level: String, message: String },
63
64 CallFunction { function: String, args: Vec<String> },
66
67 Stop,
69}
70
71pub struct RuleEngine {
73 rules: Vec<Rule>,
74 context: HashMap<String, String>,
75}
76
77impl RuleEngine {
78 pub fn new() -> Self {
80 Self {
81 rules: Vec::new(),
82 context: HashMap::new(),
83 }
84 }
85
86 pub fn add_rule(&mut self, rule: Rule) {
88 self.rules.push(rule);
89 self.rules.sort_by_key(|r| std::cmp::Reverse(r.priority));
91 }
92
93 pub fn set_context(&mut self, key: String, value: String) {
95 self.context.insert(key, value);
96 }
97
98 pub fn get_context(&self, key: &str) -> Option<&String> {
100 self.context.get(key)
101 }
102
103 fn evaluate_condition(&self, key: &str, condition: &RuleCondition) -> bool {
105 let value = match self.context.get(key) {
106 Some(v) => v,
107 None => return false,
108 };
109
110 match condition {
111 RuleCondition::Equals(expected) => value == expected,
112 RuleCondition::Contains(substring) => value.contains(substring),
113 RuleCondition::Matches(pattern) => {
114 value.contains(pattern)
116 }
117 RuleCondition::GreaterThan(threshold) => value
118 .parse::<f64>()
119 .map(|v| v > *threshold)
120 .unwrap_or(false),
121 RuleCondition::LessThan(threshold) => value
122 .parse::<f64>()
123 .map(|v| v < *threshold)
124 .unwrap_or(false),
125 RuleCondition::In(list) => list.contains(value),
126 RuleCondition::Custom(_) => {
127 false
129 }
130 }
131 }
132
133 fn execute_actions(&mut self, actions: &[RuleAction]) -> Result<bool> {
135 for action in actions {
136 match action {
137 RuleAction::SetValue { key, value } => {
138 self.context.insert(key.clone(), value.clone());
139 }
140 RuleAction::AddToList { key, value } => {
141 let current = self.context.get(key).cloned().unwrap_or_default();
142 let new_value = if current.is_empty() {
143 value.clone()
144 } else {
145 format!("{},{}", current, value)
146 };
147 self.context.insert(key.clone(), new_value);
148 }
149 RuleAction::Log {
150 level: _,
151 message: _,
152 } => {
153 }
155 RuleAction::CallFunction {
156 function: _,
157 args: _,
158 } => {
159 }
161 RuleAction::Stop => {
162 return Ok(true); }
164 }
165 }
166 Ok(false) }
168
169 pub fn execute(&mut self) -> Result<Vec<String>> {
171 let mut executed_rules = Vec::new();
172
173 for rule in &self.rules.clone() {
174 if !rule.enabled {
175 continue;
176 }
177
178 let all_conditions_met = rule
180 .conditions
181 .iter()
182 .all(|(key, condition)| self.evaluate_condition(key, condition));
183
184 if all_conditions_met {
185 executed_rules.push(rule.id.clone());
186 let should_stop = self.execute_actions(&rule.actions)?;
187 if should_stop {
188 break;
189 }
190 }
191 }
192
193 Ok(executed_rules)
194 }
195
196 pub fn clear_rules(&mut self) {
198 self.rules.clear();
199 }
200
201 pub fn get_rules(&self) -> &[Rule] {
203 &self.rules
204 }
205}
206
207impl Default for RuleEngine {
208 fn default() -> Self {
209 Self::new()
210 }
211}
212
213#[cfg(test)]
214mod tests {
215 use super::*;
216
217 fn make_rule(id: &str, key: &str, cond: RuleCondition, actions: Vec<RuleAction>) -> Rule {
218 let mut conditions = HashMap::new();
219 conditions.insert(key.to_string(), cond);
220 Rule {
221 id: id.to_string(),
222 name: format!("rule_{}", id),
223 conditions,
224 actions,
225 priority: 0,
226 enabled: true,
227 }
228 }
229
230 #[test]
231 fn test_rule_engine_default() {
232 let engine = RuleEngine::default();
233 assert!(engine.get_rules().is_empty());
234 }
235
236 #[test]
237 fn test_rule_engine_set_get_context() {
238 let mut engine = RuleEngine::new();
239 engine.set_context("key".to_string(), "value".to_string());
240 assert_eq!(engine.get_context("key"), Some(&"value".to_string()));
241 assert_eq!(engine.get_context("missing"), None);
242 }
243
244 #[test]
245 fn test_evaluate_condition_equals() {
246 let mut engine = RuleEngine::new();
247 engine.set_context("status".to_string(), "active".to_string());
248
249 let rule = make_rule(
250 "r1",
251 "status",
252 RuleCondition::Equals("active".to_string()),
253 vec![RuleAction::SetValue {
254 key: "result".to_string(),
255 value: "matched".to_string(),
256 }],
257 );
258 engine.add_rule(rule);
259 let executed = engine.execute().unwrap();
260 assert_eq!(executed, vec!["r1"]);
261 assert_eq!(engine.get_context("result"), Some(&"matched".to_string()));
262 }
263
264 #[test]
265 fn test_evaluate_condition_contains() {
266 let mut engine = RuleEngine::new();
267 engine.set_context("text".to_string(), "hello world".to_string());
268
269 let rule = make_rule(
270 "r1",
271 "text",
272 RuleCondition::Contains("world".to_string()),
273 vec![],
274 );
275 engine.add_rule(rule);
276 let executed = engine.execute().unwrap();
277 assert_eq!(executed, vec!["r1"]);
278 }
279
280 #[test]
281 fn test_evaluate_condition_matches() {
282 let mut engine = RuleEngine::new();
283 engine.set_context("name".to_string(), "test-model-v2".to_string());
284
285 let rule = make_rule(
286 "r1",
287 "name",
288 RuleCondition::Matches("model".to_string()),
289 vec![],
290 );
291 engine.add_rule(rule);
292 assert_eq!(engine.execute().unwrap(), vec!["r1"]);
293 }
294
295 #[test]
296 fn test_evaluate_condition_greater_than() {
297 let mut engine = RuleEngine::new();
298 engine.set_context("score".to_string(), "85.5".to_string());
299
300 let rule = make_rule("r1", "score", RuleCondition::GreaterThan(80.0), vec![]);
301 engine.add_rule(rule);
302 assert_eq!(engine.execute().unwrap(), vec!["r1"]);
303 }
304
305 #[test]
306 fn test_evaluate_condition_less_than() {
307 let mut engine = RuleEngine::new();
308 engine.set_context("latency".to_string(), "50".to_string());
309
310 let rule = make_rule("r1", "latency", RuleCondition::LessThan(100.0), vec![]);
311 engine.add_rule(rule);
312 assert_eq!(engine.execute().unwrap(), vec!["r1"]);
313 }
314
315 #[test]
316 fn test_evaluate_condition_in_list() {
317 let mut engine = RuleEngine::new();
318 engine.set_context("format".to_string(), "onnx".to_string());
319
320 let rule = make_rule(
321 "r1",
322 "format",
323 RuleCondition::In(vec![
324 "safetensors".to_string(),
325 "onnx".to_string(),
326 "gguf".to_string(),
327 ]),
328 vec![],
329 );
330 engine.add_rule(rule);
331 assert_eq!(engine.execute().unwrap(), vec!["r1"]);
332 }
333
334 #[test]
335 fn test_evaluate_condition_custom() {
336 let mut engine = RuleEngine::new();
337 engine.set_context("x".to_string(), "y".to_string());
338
339 let rule = make_rule(
340 "r1",
341 "x",
342 RuleCondition::Custom("custom_fn".to_string()),
343 vec![],
344 );
345 engine.add_rule(rule);
346 assert!(engine.execute().unwrap().is_empty());
348 }
349
350 #[test]
351 fn test_action_add_to_list() {
352 let mut engine = RuleEngine::new();
353 engine.set_context("status".to_string(), "go".to_string());
354
355 let rule = make_rule(
356 "r1",
357 "status",
358 RuleCondition::Equals("go".to_string()),
359 vec![
360 RuleAction::AddToList {
361 key: "log".to_string(),
362 value: "first".to_string(),
363 },
364 RuleAction::AddToList {
365 key: "log".to_string(),
366 value: "second".to_string(),
367 },
368 ],
369 );
370 engine.add_rule(rule);
371 engine.execute().unwrap();
372 assert_eq!(engine.get_context("log"), Some(&"first,second".to_string()));
373 }
374
375 #[test]
376 fn test_action_log_and_call_function() {
377 let mut engine = RuleEngine::new();
378 engine.set_context("x".to_string(), "y".to_string());
379
380 let rule = make_rule(
381 "r1",
382 "x",
383 RuleCondition::Equals("y".to_string()),
384 vec![
385 RuleAction::Log {
386 level: "info".to_string(),
387 message: "matched".to_string(),
388 },
389 RuleAction::CallFunction {
390 function: "noop".to_string(),
391 args: vec![],
392 },
393 ],
394 );
395 engine.add_rule(rule);
396 assert_eq!(engine.execute().unwrap(), vec!["r1"]);
397 }
398
399 #[test]
400 fn test_action_stop() {
401 let mut engine = RuleEngine::new();
402 engine.set_context("a".to_string(), "1".to_string());
403
404 let mut rule1 = make_rule(
405 "r1",
406 "a",
407 RuleCondition::Equals("1".to_string()),
408 vec![RuleAction::Stop],
409 );
410 rule1.priority = 10;
411
412 let mut rule2 = make_rule("r2", "a", RuleCondition::Equals("1".to_string()), vec![]);
413 rule2.priority = 5;
414
415 engine.add_rule(rule1);
416 engine.add_rule(rule2);
417
418 let executed = engine.execute().unwrap();
420 assert_eq!(executed, vec!["r1"]);
421 }
422
423 #[test]
424 fn test_disabled_rule_skipped() {
425 let mut engine = RuleEngine::new();
426 engine.set_context("x".to_string(), "y".to_string());
427
428 let mut rule = make_rule("r1", "x", RuleCondition::Equals("y".to_string()), vec![]);
429 rule.enabled = false;
430 engine.add_rule(rule);
431
432 assert!(engine.execute().unwrap().is_empty());
433 }
434
435 #[test]
436 fn test_clear_rules() {
437 let mut engine = RuleEngine::new();
438 engine.add_rule(make_rule(
439 "r1",
440 "x",
441 RuleCondition::Equals("y".to_string()),
442 vec![],
443 ));
444 engine.clear_rules();
445 assert!(engine.get_rules().is_empty());
446 }
447
448 #[test]
449 fn test_missing_context_key() {
450 let mut engine = RuleEngine::new();
451 let rule = make_rule(
453 "r1",
454 "missing_key",
455 RuleCondition::Equals("val".to_string()),
456 vec![],
457 );
458 engine.add_rule(rule);
459 assert!(engine.execute().unwrap().is_empty());
460 }
461
462 #[test]
463 fn test_priority_ordering() {
464 let mut engine = RuleEngine::new();
465 engine.set_context("x".to_string(), "1".to_string());
466
467 let mut r1 = make_rule("low", "x", RuleCondition::Equals("1".to_string()), vec![]);
468 r1.priority = 1;
469 let mut r2 = make_rule("high", "x", RuleCondition::Equals("1".to_string()), vec![]);
470 r2.priority = 10;
471
472 engine.add_rule(r1);
473 engine.add_rule(r2);
474
475 let rules = engine.get_rules();
476 assert_eq!(rules[0].id, "high");
477 assert_eq!(rules[1].id, "low");
478 }
479}