4c6f77b999
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
148 lines
4.2 KiB
Rust
148 lines
4.2 KiB
Rust
use serde::{Deserialize, Serialize};
|
|
use serde_json::Value;
|
|
use time::OffsetDateTime;
|
|
use uuid::Uuid;
|
|
|
|
/// What kind of change an audit entry records.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "lowercase")]
|
|
pub enum AuditAction {
|
|
Created,
|
|
Updated,
|
|
Deleted,
|
|
}
|
|
|
|
impl AuditAction {
|
|
/// The database/text representation.
|
|
pub fn as_str(&self) -> &'static str {
|
|
match self {
|
|
AuditAction::Created => "created",
|
|
AuditAction::Updated => "updated",
|
|
AuditAction::Deleted => "deleted",
|
|
}
|
|
}
|
|
|
|
/// Parse from the database/text representation.
|
|
pub fn from_db(s: &str) -> Option<Self> {
|
|
match s {
|
|
"created" => Some(AuditAction::Created),
|
|
"updated" => Some(AuditAction::Updated),
|
|
"deleted" => Some(AuditAction::Deleted),
|
|
_ => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Who performed the change.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "snake_case", tag = "kind", content = "id")]
|
|
pub enum AuditActor {
|
|
/// A specific user, referenced by id (a `UserId` newtype arrives with auth).
|
|
User(Uuid),
|
|
/// The system itself (migrations, automated processes).
|
|
System,
|
|
}
|
|
|
|
/// One field's before/after values within a change.
|
|
///
|
|
/// Note: after a JSON round-trip, `Some(Value::Null)` is indistinguishable from
|
|
/// `None`. Use `None` to mean "no value"; do not encode an absent value as
|
|
/// `Some(Value::Null)`.
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
pub struct FieldChange {
|
|
/// Field name (catalogue field key or column name).
|
|
pub field: String,
|
|
/// Value before the change (None when newly set).
|
|
pub before: Option<Value>,
|
|
/// Value after the change (None when cleared).
|
|
pub after: Option<Value>,
|
|
}
|
|
|
|
/// An audit event to be recorded.
|
|
#[derive(Debug, Clone, PartialEq)]
|
|
pub struct NewAuditEvent {
|
|
pub actor: AuditActor,
|
|
pub action: AuditAction,
|
|
pub entity_type: String,
|
|
pub entity_id: Uuid,
|
|
pub changes: Vec<FieldChange>,
|
|
}
|
|
|
|
/// A recorded audit entry, read back from the log.
|
|
#[derive(Debug, Clone, PartialEq)]
|
|
pub struct AuditEntry {
|
|
/// Monotonic sequence number (insertion order).
|
|
pub seq: i64,
|
|
/// When it was recorded.
|
|
pub at: OffsetDateTime,
|
|
pub actor: AuditActor,
|
|
pub action: AuditAction,
|
|
pub entity_type: String,
|
|
pub entity_id: Uuid,
|
|
pub changes: Vec<FieldChange>,
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use serde_json::json;
|
|
|
|
#[test]
|
|
fn action_round_trips_via_db_string() {
|
|
for a in [
|
|
AuditAction::Created,
|
|
AuditAction::Updated,
|
|
AuditAction::Deleted,
|
|
] {
|
|
assert_eq!(AuditAction::from_db(a.as_str()), Some(a));
|
|
}
|
|
assert_eq!(AuditAction::from_db("bogus"), None);
|
|
}
|
|
|
|
#[test]
|
|
fn field_change_serde_round_trip() {
|
|
let fc = FieldChange {
|
|
field: "name".into(),
|
|
before: Some(json!("Vase")),
|
|
after: Some(json!("Roman Vase")),
|
|
};
|
|
let v = serde_json::to_value(&fc).unwrap();
|
|
assert_eq!(v["field"], "name");
|
|
assert_eq!(v["before"], "Vase");
|
|
assert_eq!(v["after"], "Roman Vase");
|
|
let back: FieldChange = serde_json::from_value(v).unwrap();
|
|
assert_eq!(back, fc);
|
|
}
|
|
|
|
#[test]
|
|
fn actor_serde_round_trips() {
|
|
for actor in [AuditActor::User(Uuid::nil()), AuditActor::System] {
|
|
let v = serde_json::to_value(actor).unwrap();
|
|
let back: AuditActor = serde_json::from_value(v).unwrap();
|
|
assert_eq!(back, actor);
|
|
}
|
|
assert_eq!(
|
|
serde_json::to_value(AuditActor::User(Uuid::nil())).unwrap()["kind"],
|
|
"user"
|
|
);
|
|
assert_eq!(
|
|
serde_json::to_value(AuditActor::System).unwrap()["kind"],
|
|
"system"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn action_serde_matches_as_str() {
|
|
for a in [
|
|
AuditAction::Created,
|
|
AuditAction::Updated,
|
|
AuditAction::Deleted,
|
|
] {
|
|
assert_eq!(
|
|
serde_json::to_value(a).unwrap(),
|
|
serde_json::Value::String(a.as_str().to_owned())
|
|
);
|
|
}
|
|
}
|
|
}
|