use std::time::SystemTime;
use crate::vcs::{Actor, ClientId};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ProvenanceKind {
Create,
Update,
Delete,
Relate,
Rename,
Batch,
}
impl ProvenanceKind {
pub fn as_str(&self) -> &'static str {
match self {
ProvenanceKind::Create => "create",
ProvenanceKind::Update => "update",
ProvenanceKind::Delete => "delete",
ProvenanceKind::Relate => "relate",
ProvenanceKind::Rename => "rename",
ProvenanceKind::Batch => "batch",
}
}
pub fn parse(s: &str) -> Option<Self> {
match s {
"create" => Some(ProvenanceKind::Create),
"update" => Some(ProvenanceKind::Update),
"delete" => Some(ProvenanceKind::Delete),
"relate" => Some(ProvenanceKind::Relate),
"rename" => Some(ProvenanceKind::Rename),
"batch" => Some(ProvenanceKind::Batch),
_ => None,
}
}
}
#[derive(Debug, Clone)]
pub struct Provenance {
pub timestamp: SystemTime,
pub kind: ProvenanceKind,
pub entity: Option<String>,
pub actor: Actor,
pub client: Option<ClientId>,
pub note: Option<String>,
pub logical_operation_id: Option<String>,
}
impl Provenance {
pub fn new(
timestamp: SystemTime,
kind: ProvenanceKind,
entity: Option<String>,
actor: Actor,
client: Option<ClientId>,
note: Option<String>,
) -> Self {
let note = note
.as_deref()
.map(str::trim)
.filter(|n| !n.is_empty())
.map(|s| s.to_string());
Self {
timestamp,
kind,
entity,
actor,
client,
note,
logical_operation_id: None,
}
}
pub fn with_logical_operation_id(mut self, id: String) -> Self {
self.logical_operation_id = Some(id);
self
}
}
pub fn mint_logical_operation_id() -> String {
use std::sync::atomic::{AtomicU64, Ordering};
static LOGICAL_OP_COUNTER: AtomicU64 = AtomicU64::new(0);
let nanos = SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let counter = LOGICAL_OP_COUNTER.fetch_add(1, Ordering::Relaxed);
format!("logop-{nanos:032x}{counter:016x}")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn kind_wire_strings_are_stable() {
assert_eq!(ProvenanceKind::Create.as_str(), "create");
assert_eq!(ProvenanceKind::Update.as_str(), "update");
assert_eq!(ProvenanceKind::Delete.as_str(), "delete");
assert_eq!(ProvenanceKind::Relate.as_str(), "relate");
assert_eq!(ProvenanceKind::Rename.as_str(), "rename");
assert_eq!(ProvenanceKind::Batch.as_str(), "batch");
}
#[test]
fn new_normalises_whitespace_only_note_to_none() {
let r = Provenance::new(
SystemTime::UNIX_EPOCH,
ProvenanceKind::Create,
Some("v:e".into()),
Actor::Cli,
None,
Some(" \t ".into()),
);
assert!(r.note.is_none());
}
#[test]
fn new_preserves_non_blank_note() {
let r = Provenance::new(
SystemTime::UNIX_EPOCH,
ProvenanceKind::Create,
Some("v:e".into()),
Actor::Cli,
None,
Some(" first draft ".into()),
);
assert_eq!(r.note.as_deref(), Some("first draft"));
}
}