#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct FencingToken(u64);
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct InvalidFencingToken(i64);
impl InvalidFencingToken {
#[must_use]
pub const fn value(self) -> i64 {
self.0
}
}
impl std::fmt::Display for InvalidFencingToken {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{} is not a fencing token (a token is 1 or more)",
self.0
)
}
}
impl std::error::Error for InvalidFencingToken {}
impl FencingToken {
#[must_use]
pub const fn get(self) -> u64 {
self.0
}
#[must_use]
#[allow(
clippy::cast_possible_wrap,
reason = "construction bounds the value to 1..=i64::MAX"
)]
pub const fn as_i64(self) -> i64 {
self.0 as i64
}
#[must_use]
pub const fn admits(self, incoming: Self) -> bool {
incoming.0 >= self.0
}
}
impl TryFrom<i64> for FencingToken {
type Error = InvalidFencingToken;
fn try_from(value: i64) -> Result<Self, Self::Error> {
u64::try_from(value)
.ok()
.filter(|&v| v > 0)
.map(Self)
.ok_or(InvalidFencingToken(value))
}
}
impl From<FencingToken> for i64 {
fn from(token: FencingToken) -> Self {
token.as_i64()
}
}
impl std::fmt::Display for FencingToken {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
#[test]
fn token_round_trips_through_i64() {
let token = FencingToken::try_from(7_i64).expect("positive");
assert_eq!(token.get(), 7);
assert_eq!(token.as_i64(), 7);
assert_eq!(i64::from(token), 7);
}
#[test]
fn zero_and_negative_are_not_tokens() {
assert!(FencingToken::try_from(0_i64).is_err());
assert_eq!(
FencingToken::try_from(-1_i64).map_err(InvalidFencingToken::value),
Err(-1)
);
}
#[test]
fn tokens_order_by_generation() {
let older = FencingToken::try_from(3_i64).expect("token");
let newer = FencingToken::try_from(4_i64).expect("token");
assert!(older < newer);
assert_eq!(newer.to_string(), "4");
}
#[test]
fn stored_token_admits_equal_and_newer_only() {
let stored = FencingToken::try_from(5_i64).expect("token");
assert!(stored.admits(stored), "the holder can write again");
assert!(stored.admits(FencingToken::try_from(6_i64).expect("token")));
assert!(!stored.admits(FencingToken::try_from(4_i64).expect("token")));
}
proptest! {
#[test]
fn resource_rejects_every_stale_write(writes in prop::collection::vec(1_i64..50, 1..64)) {
let mut highest: Option<FencingToken> = None;
for raw in writes {
let incoming = FencingToken::try_from(raw).expect("positive");
let accepted = highest.is_none_or(|stored| stored.admits(incoming));
prop_assert_eq!(accepted, highest.is_none_or(|stored| incoming >= stored));
if accepted {
prop_assert!(highest.is_none_or(|stored| incoming >= stored));
highest = Some(incoming);
}
}
}
}
}