1use crate::RuleId;
2use chrono::{DateTime, Utc};
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct RuleMatchEntry {
8 pub rule_id: RuleId,
9 pub rule_name: String,
10 pub message_id: String,
11 pub actions_applied: Vec<String>,
12 pub timestamp: DateTime<Utc>,
13 pub success: bool,
14 pub error: Option<String>,
15}
16
17pub struct RuleExecutionLog;
20
21impl RuleExecutionLog {
22 pub fn entry(
25 rule_id: &RuleId,
26 rule_name: &str,
27 message_id: &str,
28 actions: &[String],
29 success: bool,
30 error: Option<&str>,
31 ) -> RuleMatchEntry {
32 RuleMatchEntry {
33 rule_id: rule_id.clone(),
34 rule_name: rule_name.to_string(),
35 message_id: message_id.to_string(),
36 actions_applied: actions.to_vec(),
37 timestamp: Utc::now(),
38 success,
39 error: error.map(String::from),
40 }
41 }
42}
43
44#[cfg(test)]
45mod tests {
46 use super::*;
47
48 #[test]
49 fn entry_captures_success() {
50 let entry = RuleExecutionLog::entry(
51 &RuleId("r1".into()),
52 "Archive newsletters",
53 "msg_123",
54 &["archive".into()],
55 true,
56 None,
57 );
58
59 assert_eq!(entry.rule_id, RuleId("r1".into()));
60 assert_eq!(entry.rule_name, "Archive newsletters");
61 assert_eq!(entry.message_id, "msg_123");
62 assert!(entry.success);
63 assert!(entry.error.is_none());
64 assert_eq!(entry.actions_applied, vec!["archive"]);
65 }
66
67 #[test]
68 fn entry_captures_failure() {
69 let entry = RuleExecutionLog::entry(
70 &RuleId("r1".into()),
71 "Shell hook",
72 "msg_456",
73 &["shell_hook".into()],
74 false,
75 Some("command not found"),
76 );
77
78 assert!(!entry.success);
79 assert_eq!(entry.error.as_deref(), Some("command not found"));
80 }
81
82 #[test]
83 fn entry_timestamp_is_recent() {
84 let before = Utc::now();
85 let entry = RuleExecutionLog::entry(&RuleId("r1".into()), "test", "msg", &[], true, None);
86 let after = Utc::now();
87
88 assert!(entry.timestamp >= before);
89 assert!(entry.timestamp <= after);
90 }
91
92 #[test]
93 fn entry_roundtrips_through_json() {
94 let entry = RuleExecutionLog::entry(
95 &RuleId("r1".into()),
96 "test rule",
97 "msg_1",
98 &["archive".into(), "mark_read".into()],
99 true,
100 None,
101 );
102
103 let json = serde_json::to_string(&entry).unwrap();
104 let parsed: RuleMatchEntry = serde_json::from_str(&json).unwrap();
105 assert_eq!(parsed.rule_id, entry.rule_id);
106 assert_eq!(parsed.actions_applied.len(), 2);
107 }
108}