use std::{collections::BTreeSet, error::Error, fmt};
use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum BatchState {
Planned,
Running,
Complete,
Partial,
Cancelled,
Expired,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Completion {
Succeeded,
Failed,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct VerificationBatch {
expected: u32,
succeeded: BTreeSet<u32>,
failed: BTreeSet<u32>,
state: BatchState,
}
impl VerificationBatch {
pub const fn planned(expected: u32) -> Self {
Self {
expected,
succeeded: BTreeSet::new(),
failed: BTreeSet::new(),
state: BatchState::Planned,
}
}
pub fn start(&mut self) -> Result<(), VerificationTransitionError> {
if self.state != BatchState::Planned {
return Err(VerificationTransitionError::InvalidState);
}
if self.expected == 0 {
self.state = BatchState::Complete;
} else {
self.state = BatchState::Running;
}
Ok(())
}
pub fn record(
&mut self,
index: u32,
completion: Completion,
) -> Result<(), VerificationTransitionError> {
if self.state != BatchState::Running {
return Err(VerificationTransitionError::InvalidState);
}
if index >= self.expected {
return Err(VerificationTransitionError::IndexOutOfBounds);
}
if self.succeeded.contains(&index) || self.failed.contains(&index) {
return Err(VerificationTransitionError::DuplicateCompletion);
}
match completion {
Completion::Succeeded => self.succeeded.insert(index),
Completion::Failed => self.failed.insert(index),
};
if self.completed() == self.expected {
self.state = if self.failed.is_empty() {
BatchState::Complete
} else {
BatchState::Partial
};
}
Ok(())
}
pub fn cancel(&mut self) -> Result<(), VerificationTransitionError> {
self.finish_early(BatchState::Cancelled)
}
pub fn expire(&mut self) -> Result<(), VerificationTransitionError> {
self.finish_early(BatchState::Expired)
}
pub const fn state(&self) -> BatchState {
self.state
}
pub const fn expected(&self) -> u32 {
self.expected
}
pub fn completed(&self) -> u32 {
u32::try_from(self.succeeded.len() + self.failed.len()).unwrap_or(self.expected)
}
pub fn succeeded_indices(&self) -> &BTreeSet<u32> {
&self.succeeded
}
pub fn failed_indices(&self) -> &BTreeSet<u32> {
&self.failed
}
fn finish_early(&mut self, terminal: BatchState) -> Result<(), VerificationTransitionError> {
if self.state != BatchState::Running {
return Err(VerificationTransitionError::InvalidState);
}
self.state = terminal;
Ok(())
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum VerificationTransitionError {
InvalidState,
IndexOutOfBounds,
DuplicateCompletion,
}
impl fmt::Display for VerificationTransitionError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
let message = match self {
Self::InvalidState => "verification transition is invalid for the current state",
Self::IndexOutOfBounds => "verification result index is outside the batch",
Self::DuplicateCompletion => "verification result index was already completed",
};
formatter.write_str(message)
}
}
impl Error for VerificationTransitionError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn batch_reaches_complete_only_when_every_item_succeeds() {
let mut batch = VerificationBatch::planned(2);
batch.start().unwrap();
batch.record(1, Completion::Succeeded).unwrap();
assert_eq!(batch.state(), BatchState::Running);
batch.record(0, Completion::Succeeded).unwrap();
assert_eq!(batch.state(), BatchState::Complete);
}
#[test]
fn failure_is_partial_and_duplicate_completion_is_rejected() {
let mut batch = VerificationBatch::planned(2);
batch.start().unwrap();
batch.record(0, Completion::Failed).unwrap();
assert_eq!(
batch.record(0, Completion::Succeeded),
Err(VerificationTransitionError::DuplicateCompletion)
);
batch.record(1, Completion::Succeeded).unwrap();
assert_eq!(batch.state(), BatchState::Partial);
}
#[test]
fn terminal_states_cannot_be_reopened() {
let mut batch = VerificationBatch::planned(1);
batch.start().unwrap();
batch.cancel().unwrap();
assert_eq!(
batch.start(),
Err(VerificationTransitionError::InvalidState)
);
assert_eq!(
batch.expire(),
Err(VerificationTransitionError::InvalidState)
);
}
}