use std::fmt;
use serde::{Deserialize, Serialize};
use time::OffsetDateTime;
use uuid::Uuid;
use crate::model::{ProjectId, VarId};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct AuditId(Uuid);
impl AuditId {
#[must_use]
pub fn new_v4() -> Self {
Self(Uuid::new_v4())
}
#[must_use]
pub const fn from_uuid(id: Uuid) -> Self {
Self(id)
}
#[must_use]
pub const fn as_uuid(&self) -> &Uuid {
&self.0
}
}
impl fmt::Display for AuditId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.0, f)
}
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum AuditAction {
Created,
Updated,
Deleted,
Linked,
Unlinked,
Materialized,
Run,
Copied,
Custom(String),
}
impl AuditAction {
#[must_use]
pub const fn as_str(&self) -> &str {
match self {
Self::Created => "created",
Self::Updated => "updated",
Self::Deleted => "deleted",
Self::Linked => "linked",
Self::Unlinked => "unlinked",
Self::Materialized => "materialized",
Self::Run => "run",
Self::Copied => "copied",
Self::Custom(s) => s.as_str(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AuditEntry {
id: AuditId,
action: AuditAction,
var_id: Option<VarId>,
project_id: Option<ProjectId>,
note: Option<String>,
#[serde(with = "time::serde::rfc3339")]
at: OffsetDateTime,
}
impl AuditEntry {
#[must_use]
pub fn for_var(var_id: VarId, action: AuditAction) -> Self {
Self {
id: AuditId::new_v4(),
action,
var_id: Some(var_id),
project_id: None,
note: None,
at: OffsetDateTime::now_utc(),
}
}
#[must_use]
pub fn for_project(project_id: ProjectId, var_id: Option<VarId>, action: AuditAction) -> Self {
Self {
id: AuditId::new_v4(),
action,
var_id,
project_id: Some(project_id),
note: None,
at: OffsetDateTime::now_utc(),
}
}
#[must_use]
pub fn with_note(mut self, note: impl Into<String>) -> Self {
self.note = Some(note.into());
self
}
#[must_use]
#[allow(clippy::too_many_arguments)]
pub const fn from_parts(
id: AuditId,
action: AuditAction,
var_id: Option<VarId>,
project_id: Option<ProjectId>,
note: Option<String>,
at: OffsetDateTime,
) -> Self {
Self {
id,
action,
var_id,
project_id,
note,
at,
}
}
#[must_use]
pub const fn id(&self) -> AuditId {
self.id
}
#[must_use]
pub const fn action(&self) -> &AuditAction {
&self.action
}
#[must_use]
pub const fn var_id(&self) -> Option<VarId> {
self.var_id
}
#[must_use]
pub const fn project_id(&self) -> Option<ProjectId> {
self.project_id
}
#[must_use]
pub fn note(&self) -> Option<&str> {
self.note.as_deref()
}
#[must_use]
pub const fn at(&self) -> OffsetDateTime {
self.at
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn for_var_records_action_and_var() {
let v = VarId::new_v4();
let e = AuditEntry::for_var(v, AuditAction::Created);
assert_eq!(e.var_id(), Some(v));
assert_eq!(e.project_id(), None);
assert_eq!(e.action(), &AuditAction::Created);
}
#[test]
fn for_project_records_both_ids() {
let p = ProjectId::new_v4();
let v = VarId::new_v4();
let e = AuditEntry::for_project(p, Some(v), AuditAction::Linked);
assert_eq!(e.project_id(), Some(p));
assert_eq!(e.var_id(), Some(v));
}
#[test]
fn with_note_attaches_text() {
let e =
AuditEntry::for_var(VarId::new_v4(), AuditAction::Updated).with_note("changed length");
assert_eq!(e.note(), Some("changed length"));
}
#[test]
fn action_as_str_uses_canonical_names() {
assert_eq!(AuditAction::Created.as_str(), "created");
assert_eq!(AuditAction::Custom("imported".into()).as_str(), "imported");
}
}