1use std::fmt;
4
5use serde::{Deserialize, Serialize};
6use time::OffsetDateTime;
7use uuid::Uuid;
8
9use crate::model::{ProjectId, VarId};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
13pub struct AuditId(Uuid);
14
15impl AuditId {
16 #[must_use]
18 pub fn new_v4() -> Self {
19 Self(Uuid::new_v4())
20 }
21
22 #[must_use]
24 pub const fn from_uuid(id: Uuid) -> Self {
25 Self(id)
26 }
27
28 #[must_use]
30 pub const fn as_uuid(&self) -> &Uuid {
31 &self.0
32 }
33}
34
35impl fmt::Display for AuditId {
36 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37 fmt::Display::fmt(&self.0, f)
38 }
39}
40
41#[non_exhaustive]
46#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
47pub enum AuditAction {
48 Created,
50 Updated,
52 Deleted,
54 Linked,
56 Unlinked,
58 Materialized,
60 Run,
62 Copied,
64 Custom(String),
66}
67
68impl AuditAction {
69 #[must_use]
71 pub const fn as_str(&self) -> &str {
72 match self {
73 Self::Created => "created",
74 Self::Updated => "updated",
75 Self::Deleted => "deleted",
76 Self::Linked => "linked",
77 Self::Unlinked => "unlinked",
78 Self::Materialized => "materialized",
79 Self::Run => "run",
80 Self::Copied => "copied",
81 Self::Custom(s) => s.as_str(),
82 }
83 }
84}
85
86#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
98pub struct AuditEntry {
99 id: AuditId,
100 action: AuditAction,
101 var_id: Option<VarId>,
102 project_id: Option<ProjectId>,
103 note: Option<String>,
104 #[serde(with = "time::serde::rfc3339")]
105 at: OffsetDateTime,
106}
107
108impl AuditEntry {
109 #[must_use]
111 pub fn for_var(var_id: VarId, action: AuditAction) -> Self {
112 Self {
113 id: AuditId::new_v4(),
114 action,
115 var_id: Some(var_id),
116 project_id: None,
117 note: None,
118 at: OffsetDateTime::now_utc(),
119 }
120 }
121
122 #[must_use]
124 pub fn for_project(project_id: ProjectId, var_id: Option<VarId>, action: AuditAction) -> Self {
125 Self {
126 id: AuditId::new_v4(),
127 action,
128 var_id,
129 project_id: Some(project_id),
130 note: None,
131 at: OffsetDateTime::now_utc(),
132 }
133 }
134
135 #[must_use]
137 pub fn with_note(mut self, note: impl Into<String>) -> Self {
138 self.note = Some(note.into());
139 self
140 }
141
142 #[must_use]
144 #[allow(clippy::too_many_arguments)]
145 pub const fn from_parts(
146 id: AuditId,
147 action: AuditAction,
148 var_id: Option<VarId>,
149 project_id: Option<ProjectId>,
150 note: Option<String>,
151 at: OffsetDateTime,
152 ) -> Self {
153 Self {
154 id,
155 action,
156 var_id,
157 project_id,
158 note,
159 at,
160 }
161 }
162
163 #[must_use]
165 pub const fn id(&self) -> AuditId {
166 self.id
167 }
168
169 #[must_use]
171 pub const fn action(&self) -> &AuditAction {
172 &self.action
173 }
174
175 #[must_use]
177 pub const fn var_id(&self) -> Option<VarId> {
178 self.var_id
179 }
180
181 #[must_use]
183 pub const fn project_id(&self) -> Option<ProjectId> {
184 self.project_id
185 }
186
187 #[must_use]
189 pub fn note(&self) -> Option<&str> {
190 self.note.as_deref()
191 }
192
193 #[must_use]
195 pub const fn at(&self) -> OffsetDateTime {
196 self.at
197 }
198}
199
200#[cfg(test)]
201mod tests {
202 use super::*;
203
204 #[test]
205 fn for_var_records_action_and_var() {
206 let v = VarId::new_v4();
207 let e = AuditEntry::for_var(v, AuditAction::Created);
208 assert_eq!(e.var_id(), Some(v));
209 assert_eq!(e.project_id(), None);
210 assert_eq!(e.action(), &AuditAction::Created);
211 }
212
213 #[test]
214 fn for_project_records_both_ids() {
215 let p = ProjectId::new_v4();
216 let v = VarId::new_v4();
217 let e = AuditEntry::for_project(p, Some(v), AuditAction::Linked);
218 assert_eq!(e.project_id(), Some(p));
219 assert_eq!(e.var_id(), Some(v));
220 }
221
222 #[test]
223 fn with_note_attaches_text() {
224 let e =
225 AuditEntry::for_var(VarId::new_v4(), AuditAction::Updated).with_note("changed length");
226 assert_eq!(e.note(), Some("changed length"));
227 }
228
229 #[test]
230 fn action_as_str_uses_canonical_names() {
231 assert_eq!(AuditAction::Created.as_str(), "created");
232 assert_eq!(AuditAction::Custom("imported".into()).as_str(), "imported");
233 }
234}