use crate::identity::AgentId;
use serde::{Deserialize, Serialize};
use std::cmp::Ordering;
pub type Result<T> = std::result::Result<T, CheckboxError>;
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum CheckboxError {
#[error("task already claimed by {0}")]
AlreadyClaimed(AgentId),
#[error("task is already done and cannot be modified")]
AlreadyDone,
#[error("task must be claimed before completion")]
MustClaimFirst,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum CheckboxState {
Empty,
Claimed {
agent_id: AgentId,
timestamp: u64,
},
Done {
agent_id: AgentId,
timestamp: u64,
},
}
impl std::fmt::Display for CheckboxState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
CheckboxState::Empty => write!(f, "empty"),
CheckboxState::Claimed { agent_id, .. } => {
write!(f, "claimed:{}", hex::encode(agent_id.as_bytes()))
}
CheckboxState::Done { agent_id, .. } => {
write!(f, "done:{}", hex::encode(agent_id.as_bytes()))
}
}
}
}
impl CheckboxState {
pub fn claim(agent_id: AgentId, timestamp: u64) -> Result<Self> {
Ok(Self::Claimed {
agent_id,
timestamp,
})
}
pub fn complete(agent_id: AgentId, timestamp: u64) -> Result<Self> {
Ok(Self::Done {
agent_id,
timestamp,
})
}
#[must_use]
pub fn is_empty(&self) -> bool {
matches!(self, Self::Empty)
}
#[must_use]
pub fn is_claimed(&self) -> bool {
matches!(self, Self::Claimed { .. })
}
#[must_use]
pub fn is_done(&self) -> bool {
matches!(self, Self::Done { .. })
}
#[must_use]
pub fn claimed_by(&self) -> Option<&AgentId> {
match self {
Self::Empty => None,
Self::Claimed { agent_id, .. } | Self::Done { agent_id, .. } => Some(agent_id),
}
}
#[must_use]
pub fn timestamp(&self) -> Option<u64> {
match self {
Self::Empty => None,
Self::Claimed { timestamp, .. } | Self::Done { timestamp, .. } => Some(*timestamp),
}
}
pub fn transition_to_claimed(&self, agent_id: AgentId, timestamp: u64) -> Result<Self> {
match self {
Self::Empty => Ok(Self::Claimed {
agent_id,
timestamp,
}),
Self::Claimed {
agent_id: existing_agent,
..
} => Err(CheckboxError::AlreadyClaimed(*existing_agent)),
Self::Done { .. } => Err(CheckboxError::AlreadyDone),
}
}
pub fn transition_to_done(&self, agent_id: AgentId, timestamp: u64) -> Result<Self> {
match self {
Self::Empty => Err(CheckboxError::MustClaimFirst),
Self::Claimed { .. } => Ok(Self::Done {
agent_id,
timestamp,
}),
Self::Done { .. } => Err(CheckboxError::AlreadyDone),
}
}
}
impl Ord for CheckboxState {
fn cmp(&self, other: &Self) -> Ordering {
match (self, other) {
(Self::Empty, Self::Empty) => Ordering::Equal,
(Self::Empty, _) => Ordering::Less,
(_, Self::Empty) => Ordering::Greater,
(
Self::Claimed {
agent_id: aid1,
timestamp: ts1,
},
Self::Claimed {
agent_id: aid2,
timestamp: ts2,
},
) => match ts1.cmp(ts2) {
Ordering::Equal => aid1.as_bytes().cmp(aid2.as_bytes()),
ordering => ordering,
},
(Self::Claimed { .. }, Self::Done { .. }) => Ordering::Less,
(Self::Done { .. }, Self::Claimed { .. }) => Ordering::Greater,
(
Self::Done {
agent_id: aid1,
timestamp: ts1,
},
Self::Done {
agent_id: aid2,
timestamp: ts2,
},
) => match ts1.cmp(ts2) {
Ordering::Equal => aid1.as_bytes().cmp(aid2.as_bytes()),
ordering => ordering,
},
}
}
}
impl PartialOrd for CheckboxState {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn agent(n: u8) -> AgentId {
AgentId([n; 32])
}
#[test]
fn test_checkbox_state_constructors() {
let agent = agent(1);
let timestamp = 1000;
let claimed = CheckboxState::claim(agent, timestamp).ok().unwrap();
assert!(claimed.is_claimed());
assert_eq!(claimed.claimed_by(), Some(&agent));
assert_eq!(claimed.timestamp(), Some(timestamp));
let done = CheckboxState::complete(agent, timestamp).ok().unwrap();
assert!(done.is_done());
assert_eq!(done.claimed_by(), Some(&agent));
assert_eq!(done.timestamp(), Some(timestamp));
}
#[test]
fn test_checkbox_state_predicates() {
let agent = agent(1);
let empty = CheckboxState::Empty;
assert!(empty.is_empty());
assert!(!empty.is_claimed());
assert!(!empty.is_done());
assert_eq!(empty.claimed_by(), None);
assert_eq!(empty.timestamp(), None);
let claimed = CheckboxState::claim(agent, 1000).ok().unwrap();
assert!(!claimed.is_empty());
assert!(claimed.is_claimed());
assert!(!claimed.is_done());
let done = CheckboxState::complete(agent, 2000).ok().unwrap();
assert!(!done.is_empty());
assert!(!done.is_claimed());
assert!(done.is_done());
}
#[test]
fn test_valid_transition_empty_to_claimed() {
let empty = CheckboxState::Empty;
let agent = agent(1);
let timestamp = 1000;
let claimed = empty.transition_to_claimed(agent, timestamp).ok().unwrap();
assert!(claimed.is_claimed());
assert_eq!(claimed.claimed_by(), Some(&agent));
}
#[test]
fn test_valid_transition_claimed_to_done() {
let agent1 = agent(1);
let agent2 = agent(2);
let claimed = CheckboxState::claim(agent1, 1000).ok().unwrap();
let done1 = claimed.transition_to_done(agent1, 2000).ok().unwrap();
assert!(done1.is_done());
assert_eq!(done1.claimed_by(), Some(&agent1));
let done2 = claimed.transition_to_done(agent2, 2000).ok().unwrap();
assert!(done2.is_done());
assert_eq!(done2.claimed_by(), Some(&agent2));
}
#[test]
fn test_invalid_transition_empty_to_done() {
let empty = CheckboxState::Empty;
let agent = agent(1);
let result = empty.transition_to_done(agent, 1000);
assert!(result.is_err());
assert_eq!(result.unwrap_err(), CheckboxError::MustClaimFirst);
}
#[test]
fn test_invalid_transition_claimed_to_claimed() {
let agent1 = agent(1);
let agent2 = agent(2);
let claimed = CheckboxState::claim(agent1, 1000).ok().unwrap();
let result = claimed.transition_to_claimed(agent2, 2000);
assert!(result.is_err());
assert_eq!(result.unwrap_err(), CheckboxError::AlreadyClaimed(agent1));
}
#[test]
fn test_invalid_transition_from_done() {
let agent1 = agent(1);
let agent2 = agent(2);
let done = CheckboxState::complete(agent1, 1000).ok().unwrap();
let result = done.transition_to_claimed(agent2, 2000);
assert!(result.is_err());
assert_eq!(result.unwrap_err(), CheckboxError::AlreadyDone);
let result = done.transition_to_done(agent2, 2000);
assert!(result.is_err());
assert_eq!(result.unwrap_err(), CheckboxError::AlreadyDone);
}
#[test]
fn test_checkbox_ord_by_variant() {
let empty = CheckboxState::Empty;
let claimed = CheckboxState::claim(agent(1), 1000).ok().unwrap();
let done = CheckboxState::complete(agent(1), 2000).ok().unwrap();
assert!(empty < claimed);
assert!(claimed < done);
assert!(empty < done);
}
#[test]
fn test_checkbox_ord_by_timestamp() {
let agent = agent(1);
let claimed_early = CheckboxState::claim(agent, 1000).ok().unwrap();
let claimed_late = CheckboxState::claim(agent, 2000).ok().unwrap();
assert!(claimed_early < claimed_late);
let done_early = CheckboxState::complete(agent, 1000).ok().unwrap();
let done_late = CheckboxState::complete(agent, 2000).ok().unwrap();
assert!(done_early < done_late);
}
#[test]
fn test_checkbox_ord_by_agent_id_tiebreak() {
let agent1 = agent(1);
let agent2 = agent(2);
let timestamp = 1000;
let claimed1 = CheckboxState::claim(agent1, timestamp).ok().unwrap();
let claimed2 = CheckboxState::claim(agent2, timestamp).ok().unwrap();
assert!(claimed1 < claimed2);
let done1 = CheckboxState::complete(agent1, timestamp).ok().unwrap();
let done2 = CheckboxState::complete(agent2, timestamp).ok().unwrap();
assert!(done1 < done2);
}
#[test]
fn test_checkbox_equality() {
let agent = agent(1);
let timestamp = 1000;
let claimed1 = CheckboxState::claim(agent, timestamp).ok().unwrap();
let claimed2 = CheckboxState::claim(agent, timestamp).ok().unwrap();
assert_eq!(claimed1, claimed2);
let done1 = CheckboxState::complete(agent, timestamp).ok().unwrap();
let done2 = CheckboxState::complete(agent, timestamp).ok().unwrap();
assert_eq!(done1, done2);
}
#[test]
fn test_concurrent_claims_resolution() {
let agent1 = agent(1);
let agent2 = agent(2);
let claim1 = CheckboxState::claim(agent1, 1000).ok().unwrap();
let claim2 = CheckboxState::claim(agent2, 1100).ok().unwrap();
assert!(claim1 < claim2);
let claim3 = CheckboxState::claim(agent1, 1000).ok().unwrap();
let claim4 = CheckboxState::claim(agent2, 1000).ok().unwrap();
assert!(claim3 < claim4); }
#[test]
fn test_serialization_roundtrip() {
let agent = agent(42);
let timestamp = 1234567890;
let states = vec![
CheckboxState::Empty,
CheckboxState::claim(agent, timestamp).ok().unwrap(),
CheckboxState::complete(agent, timestamp).ok().unwrap(),
];
for state in states {
let serialized = bincode::serialize(&state).ok().unwrap();
let deserialized: CheckboxState = bincode::deserialize(&serialized).ok().unwrap();
assert_eq!(state, deserialized);
}
}
}