1use crate::ip::ClientInfo;
2
3#[derive(Debug, Clone)]
17pub struct AuditEntry {
18 actor: String,
19 action: String,
20 resource_type: String,
21 resource_id: String,
22 metadata: Option<serde_json::Value>,
23 client_info: Option<ClientInfo>,
24 tenant_id: Option<String>,
25}
26
27impl AuditEntry {
28 pub fn new(
30 actor: impl Into<String>,
31 action: impl Into<String>,
32 resource_type: impl Into<String>,
33 resource_id: impl Into<String>,
34 ) -> Self {
35 Self {
36 actor: actor.into(),
37 action: action.into(),
38 resource_type: resource_type.into(),
39 resource_id: resource_id.into(),
40 metadata: None,
41 client_info: None,
42 tenant_id: None,
43 }
44 }
45
46 pub fn metadata(mut self, meta: serde_json::Value) -> Self {
52 self.metadata = Some(meta);
53 self
54 }
55
56 pub fn client_info(mut self, info: ClientInfo) -> Self {
58 self.client_info = Some(info);
59 self
60 }
61
62 pub fn tenant_id(mut self, id: impl Into<String>) -> Self {
64 self.tenant_id = Some(id.into());
65 self
66 }
67
68 pub fn actor(&self) -> &str {
70 &self.actor
71 }
72
73 pub fn action(&self) -> &str {
75 &self.action
76 }
77
78 pub fn resource_type(&self) -> &str {
80 &self.resource_type
81 }
82
83 pub fn resource_id(&self) -> &str {
85 &self.resource_id
86 }
87
88 pub fn metadata_value(&self) -> Option<&serde_json::Value> {
90 self.metadata.as_ref()
91 }
92
93 pub fn client_info_value(&self) -> Option<&ClientInfo> {
95 self.client_info.as_ref()
96 }
97
98 pub fn tenant_id_value(&self) -> Option<&str> {
100 self.tenant_id.as_deref()
101 }
102}
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107
108 #[test]
109 fn new_sets_required_fields() {
110 let entry = AuditEntry::new("user_123", "user.created", "user", "usr_abc");
111 assert_eq!(entry.actor(), "user_123");
112 assert_eq!(entry.action(), "user.created");
113 assert_eq!(entry.resource_type(), "user");
114 assert_eq!(entry.resource_id(), "usr_abc");
115 assert!(entry.metadata_value().is_none());
116 assert!(entry.client_info_value().is_none());
117 assert!(entry.tenant_id_value().is_none());
118 }
119
120 #[test]
121 fn metadata_with_json_value() {
122 let entry = AuditEntry::new("user_123", "user.role.changed", "user", "usr_abc")
123 .metadata(serde_json::json!({"old_role": "editor", "new_role": "admin"}));
124 let meta = entry.metadata_value().unwrap();
125 assert_eq!(meta["old_role"], "editor");
126 assert_eq!(meta["new_role"], "admin");
127 }
128
129 #[test]
130 fn metadata_with_serializable_struct() {
131 #[derive(serde::Serialize)]
132 struct RoleChange {
133 old_role: String,
134 new_role: String,
135 }
136
137 let entry = AuditEntry::new("user_123", "user.role.changed", "user", "usr_abc").metadata(
138 serde_json::to_value(RoleChange {
139 old_role: "editor".into(),
140 new_role: "admin".into(),
141 })
142 .unwrap(),
143 );
144 let meta = entry.metadata_value().unwrap();
145 assert_eq!(meta["old_role"], "editor");
146 assert_eq!(meta["new_role"], "admin");
147 }
148
149 #[test]
150 fn client_info_attached() {
151 use crate::ip::ClientInfo;
152
153 let info = ClientInfo::new().ip("1.2.3.4").user_agent("Bot/1.0");
154 let entry = AuditEntry::new("system", "job.ran", "job", "job_1").client_info(info);
155 let ci = entry.client_info_value().unwrap();
156 assert_eq!(ci.ip_value(), Some("1.2.3.4"));
157 assert_eq!(ci.user_agent_value(), Some("Bot/1.0"));
158 }
159
160 #[test]
161 fn tenant_id_set() {
162 let entry =
163 AuditEntry::new("user_123", "doc.deleted", "document", "doc_1").tenant_id("tenant_abc");
164 assert_eq!(entry.tenant_id_value(), Some("tenant_abc"));
165 }
166}