1use std::collections::HashMap;
7use std::fmt;
8
9use serde::{Deserialize, Serialize};
10use thiserror::Error;
11use uuid::Uuid;
12use chrono::{DateTime, Utc};
13use regex::Regex;
14use async_trait::async_trait;
15
16pub mod engine;
17pub mod conditions;
18pub mod actions;
19pub mod context;
20pub mod storage;
21
22#[cfg(feature = "scripting")]
23pub mod scripting;
24
25#[cfg(feature = "database")]
26pub mod database;
27
28#[derive(Error, Debug)]
30pub enum RulesError {
31 #[error("Rule not found: {0}")]
32 RuleNotFound(String),
33
34 #[error("Invalid rule definition: {0}")]
35 InvalidRule(String),
36
37 #[error("Condition evaluation failed: {0}")]
38 ConditionEvaluation(String),
39
40 #[error("Action execution failed: {0}")]
41 ActionExecution(String),
42
43 #[error("Context error: {0}")]
44 Context(String),
45
46 #[error("Storage error: {0}")]
47 Storage(String),
48
49 #[error("Scripting error: {0}")]
50 Scripting(String),
51
52 #[error("Validation error: {0}")]
53 Validation(String),
54
55 #[error("Parsing error: {0}")]
56 Parsing(String),
57}
58
59pub type Result<T> = std::result::Result<T, RulesError>;
60
61#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct Rule {
64 pub id: String,
66
67 pub name: String,
69
70 pub description: String,
72
73 pub conditions: Vec<RuleCondition>,
75
76 pub actions: Vec<RuleAction>,
78
79 pub priority: u32,
81
82 pub enabled: bool,
84
85 pub created_at: DateTime<Utc>,
87
88 pub updated_at: DateTime<Utc>,
90
91 pub metadata: HashMap<String, String>,
93}
94
95impl Rule {
96 pub fn new(
98 id: String,
99 name: String,
100 conditions: Vec<RuleCondition>,
101 actions: Vec<RuleAction>,
102 ) -> Self {
103 let now = Utc::now();
104
105 Self {
106 id,
107 name,
108 description: String::new(),
109 conditions,
110 actions,
111 priority: 0,
112 enabled: true,
113 created_at: now,
114 updated_at: now,
115 metadata: HashMap::new(),
116 }
117 }
118
119 pub fn new_with_generated_id(
121 name: String,
122 conditions: Vec<RuleCondition>,
123 actions: Vec<RuleAction>,
124 ) -> Self {
125 Self::new(Uuid::new_v4().to_string(), name, conditions, actions)
126 }
127
128 pub fn is_valid(&self) -> Result<()> {
130 if self.id.is_empty() {
131 return Err(RulesError::InvalidRule("Rule ID cannot be empty".to_string()));
132 }
133
134 if self.name.is_empty() {
135 return Err(RulesError::InvalidRule("Rule name cannot be empty".to_string()));
136 }
137
138 if self.conditions.is_empty() {
139 return Err(RulesError::InvalidRule("Rule must have at least one condition".to_string()));
140 }
141
142 if self.actions.is_empty() {
143 return Err(RulesError::InvalidRule("Rule must have at least one action".to_string()));
144 }
145
146 for condition in &self.conditions {
148 condition.validate()?;
149 }
150
151 for action in &self.actions {
153 action.validate()?;
154 }
155
156 Ok(())
157 }
158}
159
160#[derive(Debug, Clone, Serialize, Deserialize)]
162pub enum RuleCondition {
163 Equals {
165 field: String,
166 value: String,
167 },
168
169 NotEquals {
171 field: String,
172 value: String,
173 },
174
175 GreaterThan {
177 field: String,
178 value: f64,
179 },
180
181 LessThan {
183 field: String,
184 value: f64,
185 },
186
187 Matches {
189 field: String,
190 pattern: String,
191 },
192
193 Exists {
195 field: String,
196 },
197
198 In {
200 field: String,
201 values: Vec<String>,
202 },
203
204 TimeCondition {
206 field: String,
207 operator: TimeOperator,
208 value: DateTime<Utc>,
209 },
210
211 And {
213 conditions: Vec<RuleCondition>,
214 },
215
216 Or {
218 conditions: Vec<RuleCondition>,
219 },
220
221 Not {
223 condition: Box<RuleCondition>,
224 },
225
226 Custom {
228 condition_type: String,
229 parameters: HashMap<String, String>,
230 },
231}
232
233impl RuleCondition {
234 pub fn validate(&self) -> Result<()> {
236 match self {
237 RuleCondition::Matches { pattern, .. } => {
238 Regex::new(pattern)
239 .map_err(|e| RulesError::InvalidRule(format!("Invalid regex pattern: {}", e)))?;
240 }
241 RuleCondition::And { conditions } | RuleCondition::Or { conditions } => {
242 if conditions.is_empty() {
243 return Err(RulesError::InvalidRule("Logical conditions must have at least one sub-condition".to_string()));
244 }
245 for condition in conditions {
246 condition.validate()?;
247 }
248 }
249 RuleCondition::Not { condition } => {
250 condition.validate()?;
251 }
252 _ => {} }
254 Ok(())
255 }
256}
257
258#[derive(Debug, Clone, Serialize, Deserialize)]
260pub enum TimeOperator {
261 Before,
262 After,
263 Between { end: DateTime<Utc> },
264}
265
266#[derive(Debug, Clone, Serialize, Deserialize)]
268pub enum RuleAction {
269 SetField {
271 field: String,
272 value: String,
273 },
274
275 Log {
277 level: LogLevel,
278 message: String,
279 },
280
281 Notify {
283 recipient: String,
284 message: String,
285 channel: NotificationChannel,
286 },
287
288 #[cfg(feature = "scripting")]
290 Script {
291 script_type: String,
292 script: String,
293 },
294
295 Webhook {
297 url: String,
298 method: String,
299 headers: HashMap<String, String>,
300 body: String,
301 },
302
303 ModifyContext {
305 modifications: HashMap<String, String>,
306 },
307
308 Abort {
310 reason: String,
311 },
312
313 Custom {
315 action_type: String,
316 parameters: HashMap<String, String>,
317 },
318}
319
320impl RuleAction {
321 pub fn validate(&self) -> Result<()> {
323 match self {
324 RuleAction::Webhook { url, .. } => {
325 if url.is_empty() {
326 return Err(RulesError::InvalidRule("Webhook URL cannot be empty".to_string()));
327 }
328 }
329 _ => {} }
331 Ok(())
332 }
333}
334
335#[derive(Debug, Clone, Serialize, Deserialize)]
337pub enum LogLevel {
338 Trace,
339 Debug,
340 Info,
341 Warn,
342 Error,
343}
344
345#[derive(Debug, Clone, Serialize, Deserialize)]
347pub enum NotificationChannel {
348 Email,
349 Slack,
350 Discord,
351 Webhook,
352 Internal,
353}
354
355#[derive(Debug, Clone, Serialize, Deserialize)]
357pub enum RuleResult {
358 Allow,
360
361 Deny(String),
363
364 Modified(HashMap<String, String>),
366
367 Skipped,
369
370 Failed(String),
372}
373
374impl fmt::Display for RuleResult {
375 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
376 match self {
377 RuleResult::Allow => write!(f, "Allow"),
378 RuleResult::Deny(reason) => write!(f, "Deny: {}", reason),
379 RuleResult::Modified(changes) => write!(f, "Modified: {:?}", changes),
380 RuleResult::Skipped => write!(f, "Skipped"),
381 RuleResult::Failed(error) => write!(f, "Failed: {}", error),
382 }
383 }
384}
385
386pub use context::ExecutionContext;
388
389pub use engine::RuleEngine;
391
392pub use conditions::ConditionEvaluator;
394pub use actions::ActionExecutor;
395
396#[cfg(test)]
397mod tests {
398 use super::*;
399
400 #[test]
401 fn test_rule_creation() {
402 let rule = Rule::new_with_generated_id(
403 "Test Rule".to_string(),
404 vec![RuleCondition::Equals {
405 field: "status".to_string(),
406 value: "active".to_string(),
407 }],
408 vec![RuleAction::Log {
409 level: LogLevel::Info,
410 message: "Rule triggered".to_string(),
411 }],
412 );
413
414 assert!(!rule.id.is_empty());
415 assert_eq!(rule.name, "Test Rule");
416 assert!(rule.enabled);
417 assert_eq!(rule.conditions.len(), 1);
418 assert_eq!(rule.actions.len(), 1);
419 }
420
421 #[test]
422 fn test_rule_validation() {
423 let rule = Rule::new_with_generated_id(
424 "Valid Rule".to_string(),
425 vec![RuleCondition::Equals {
426 field: "test".to_string(),
427 value: "value".to_string(),
428 }],
429 vec![RuleAction::Log {
430 level: LogLevel::Info,
431 message: "test".to_string(),
432 }],
433 );
434
435 assert!(rule.is_valid().is_ok());
436 }
437
438 #[test]
439 fn test_invalid_rule_validation() {
440 let rule = Rule::new(
441 String::new(), "Invalid Rule".to_string(),
443 vec![RuleCondition::Equals {
444 field: "test".to_string(),
445 value: "value".to_string(),
446 }],
447 vec![RuleAction::Log {
448 level: LogLevel::Info,
449 message: "test".to_string(),
450 }],
451 );
452
453 assert!(rule.is_valid().is_err());
454 }
455
456 #[test]
457 fn test_condition_validation() {
458 let valid_condition = RuleCondition::Matches {
459 field: "email".to_string(),
460 pattern: r"^[^@]+@[^@]+\.[^@]+$".to_string(),
461 };
462 assert!(valid_condition.validate().is_ok());
463
464 let invalid_condition = RuleCondition::Matches {
465 field: "email".to_string(),
466 pattern: "[".to_string(), };
468 assert!(invalid_condition.validate().is_err());
469 }
470
471 #[test]
472 fn test_rule_result_display() {
473 assert_eq!(RuleResult::Allow.to_string(), "Allow");
474 assert_eq!(RuleResult::Deny("test".to_string()).to_string(), "Deny: test");
475 assert_eq!(RuleResult::Skipped.to_string(), "Skipped");
476 }
477}