use crate::clock::Timestamp;
use crate::hash::Digest;
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub struct RecordId(u64);
impl RecordId {
pub const GENESIS: Self = Self(0);
#[inline]
pub const fn from_u64(value: u64) -> Self {
Self(value)
}
#[inline]
pub const fn as_u64(self) -> u64 {
self.0
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct Actor<'a>(&'a str);
impl<'a> Actor<'a> {
#[inline]
pub const fn new(value: &'a str) -> Self {
Self(value)
}
#[inline]
pub const fn as_str(&self) -> &'a str {
self.0
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct Action<'a>(&'a str);
impl<'a> Action<'a> {
#[inline]
pub const fn new(value: &'a str) -> Self {
Self(value)
}
#[inline]
pub const fn as_str(&self) -> &'a str {
self.0
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct Target<'a>(&'a str);
impl<'a> Target<'a> {
#[inline]
pub const fn new(value: &'a str) -> Self {
Self(value)
}
#[inline]
pub const fn as_str(&self) -> &'a str {
self.0
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
#[repr(u8)]
pub enum Outcome {
Success = 0,
Failure = 1,
Denied = 2,
Error = 3,
}
impl Outcome {
#[inline]
pub const fn as_u8(self) -> u8 {
self as u8
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct Record<'a> {
id: RecordId,
timestamp: Timestamp,
actor: Actor<'a>,
action: Action<'a>,
target: Target<'a>,
outcome: Outcome,
prev_hash: Digest,
hash: Digest,
}
impl<'a> Record<'a> {
#[allow(clippy::too_many_arguments)]
#[inline]
pub const fn new(
id: RecordId,
timestamp: Timestamp,
actor: Actor<'a>,
action: Action<'a>,
target: Target<'a>,
outcome: Outcome,
prev_hash: Digest,
hash: Digest,
) -> Self {
Self {
id,
timestamp,
actor,
action,
target,
outcome,
prev_hash,
hash,
}
}
#[inline]
pub const fn with_hash(mut self, hash: Digest) -> Self {
self.hash = hash;
self
}
#[inline]
pub const fn id(&self) -> RecordId {
self.id
}
#[inline]
pub const fn timestamp(&self) -> Timestamp {
self.timestamp
}
#[inline]
pub const fn actor(&self) -> Actor<'a> {
self.actor
}
#[inline]
pub const fn action(&self) -> Action<'a> {
self.action
}
#[inline]
pub const fn target(&self) -> Target<'a> {
self.target
}
#[inline]
pub const fn outcome(&self) -> Outcome {
self.outcome
}
#[inline]
pub const fn prev_hash(&self) -> Digest {
self.prev_hash
}
#[inline]
pub const fn hash(&self) -> Digest {
self.hash
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::clock::Timestamp;
use crate::hash::{Digest, HASH_LEN};
#[test]
fn record_id_genesis_is_zero() {
assert_eq!(RecordId::GENESIS.as_u64(), 0);
assert_eq!(RecordId::from_u64(0), RecordId::GENESIS);
}
#[test]
fn outcome_as_u8_is_stable() {
assert_eq!(Outcome::Success.as_u8(), 0);
assert_eq!(Outcome::Failure.as_u8(), 1);
assert_eq!(Outcome::Denied.as_u8(), 2);
assert_eq!(Outcome::Error.as_u8(), 3);
}
#[test]
fn record_accessors_return_constructor_values() {
let r = Record::new(
RecordId::from_u64(42),
Timestamp::from_nanos(1_700_000_000),
Actor::new("user-1"),
Action::new("user.login"),
Target::new("session:abc"),
Outcome::Success,
Digest::from_bytes([0xAA; HASH_LEN]),
Digest::from_bytes([0xBB; HASH_LEN]),
);
assert_eq!(r.id().as_u64(), 42);
assert_eq!(r.timestamp().as_nanos(), 1_700_000_000);
assert_eq!(r.actor().as_str(), "user-1");
assert_eq!(r.action().as_str(), "user.login");
assert_eq!(r.target().as_str(), "session:abc");
assert_eq!(r.outcome(), Outcome::Success);
assert_eq!(r.prev_hash().as_bytes(), &[0xAA; HASH_LEN]);
assert_eq!(r.hash().as_bytes(), &[0xBB; HASH_LEN]);
}
#[test]
fn record_with_hash_swaps_only_the_hash_field() {
let r = Record::new(
RecordId::GENESIS,
Timestamp::EPOCH,
Actor::new("a"),
Action::new("x"),
Target::new("t"),
Outcome::Success,
Digest::ZERO,
Digest::ZERO,
);
let new_hash = Digest::from_bytes([0xCC; HASH_LEN]);
let r2 = r.with_hash(new_hash);
assert_eq!(r2.hash(), new_hash);
assert_eq!(r2.id(), r.id());
assert_eq!(r2.actor(), r.actor());
assert_eq!(r2.prev_hash(), r.prev_hash());
}
}