dnspls_core/
verification.rs1use std::{collections::BTreeSet, error::Error, fmt};
2
3use serde::{Deserialize, Serialize};
4
5#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
7#[serde(rename_all = "snake_case")]
8pub enum BatchState {
9 Planned,
10 Running,
11 Complete,
12 Partial,
13 Cancelled,
14 Expired,
15}
16
17#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
18#[serde(rename_all = "snake_case")]
19pub enum Completion {
20 Succeeded,
21 Failed,
22}
23
24#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
26pub struct VerificationBatch {
27 expected: u32,
28 succeeded: BTreeSet<u32>,
29 failed: BTreeSet<u32>,
30 state: BatchState,
31}
32
33impl VerificationBatch {
34 pub const fn planned(expected: u32) -> Self {
35 Self {
36 expected,
37 succeeded: BTreeSet::new(),
38 failed: BTreeSet::new(),
39 state: BatchState::Planned,
40 }
41 }
42
43 pub fn start(&mut self) -> Result<(), VerificationTransitionError> {
49 if self.state != BatchState::Planned {
50 return Err(VerificationTransitionError::InvalidState);
51 }
52 if self.expected == 0 {
53 self.state = BatchState::Complete;
54 } else {
55 self.state = BatchState::Running;
56 }
57 Ok(())
58 }
59
60 pub fn record(
66 &mut self,
67 index: u32,
68 completion: Completion,
69 ) -> Result<(), VerificationTransitionError> {
70 if self.state != BatchState::Running {
71 return Err(VerificationTransitionError::InvalidState);
72 }
73 if index >= self.expected {
74 return Err(VerificationTransitionError::IndexOutOfBounds);
75 }
76 if self.succeeded.contains(&index) || self.failed.contains(&index) {
77 return Err(VerificationTransitionError::DuplicateCompletion);
78 }
79
80 match completion {
81 Completion::Succeeded => self.succeeded.insert(index),
82 Completion::Failed => self.failed.insert(index),
83 };
84 if self.completed() == self.expected {
85 self.state = if self.failed.is_empty() {
86 BatchState::Complete
87 } else {
88 BatchState::Partial
89 };
90 }
91 Ok(())
92 }
93
94 pub fn cancel(&mut self) -> Result<(), VerificationTransitionError> {
100 self.finish_early(BatchState::Cancelled)
101 }
102
103 pub fn expire(&mut self) -> Result<(), VerificationTransitionError> {
109 self.finish_early(BatchState::Expired)
110 }
111
112 pub const fn state(&self) -> BatchState {
113 self.state
114 }
115
116 pub const fn expected(&self) -> u32 {
117 self.expected
118 }
119
120 pub fn completed(&self) -> u32 {
121 u32::try_from(self.succeeded.len() + self.failed.len()).unwrap_or(self.expected)
122 }
123
124 pub fn succeeded_indices(&self) -> &BTreeSet<u32> {
125 &self.succeeded
126 }
127
128 pub fn failed_indices(&self) -> &BTreeSet<u32> {
129 &self.failed
130 }
131
132 fn finish_early(&mut self, terminal: BatchState) -> Result<(), VerificationTransitionError> {
133 if self.state != BatchState::Running {
134 return Err(VerificationTransitionError::InvalidState);
135 }
136 self.state = terminal;
137 Ok(())
138 }
139}
140
141#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
142#[serde(rename_all = "snake_case")]
143pub enum VerificationTransitionError {
144 InvalidState,
145 IndexOutOfBounds,
146 DuplicateCompletion,
147}
148
149impl fmt::Display for VerificationTransitionError {
150 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
151 let message = match self {
152 Self::InvalidState => "verification transition is invalid for the current state",
153 Self::IndexOutOfBounds => "verification result index is outside the batch",
154 Self::DuplicateCompletion => "verification result index was already completed",
155 };
156 formatter.write_str(message)
157 }
158}
159
160impl Error for VerificationTransitionError {}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165
166 #[test]
167 fn batch_reaches_complete_only_when_every_item_succeeds() {
168 let mut batch = VerificationBatch::planned(2);
169 batch.start().unwrap();
170 batch.record(1, Completion::Succeeded).unwrap();
171 assert_eq!(batch.state(), BatchState::Running);
172 batch.record(0, Completion::Succeeded).unwrap();
173 assert_eq!(batch.state(), BatchState::Complete);
174 }
175
176 #[test]
177 fn failure_is_partial_and_duplicate_completion_is_rejected() {
178 let mut batch = VerificationBatch::planned(2);
179 batch.start().unwrap();
180 batch.record(0, Completion::Failed).unwrap();
181 assert_eq!(
182 batch.record(0, Completion::Succeeded),
183 Err(VerificationTransitionError::DuplicateCompletion)
184 );
185 batch.record(1, Completion::Succeeded).unwrap();
186 assert_eq!(batch.state(), BatchState::Partial);
187 }
188
189 #[test]
190 fn terminal_states_cannot_be_reopened() {
191 let mut batch = VerificationBatch::planned(1);
192 batch.start().unwrap();
193 batch.cancel().unwrap();
194 assert_eq!(
195 batch.start(),
196 Err(VerificationTransitionError::InvalidState)
197 );
198 assert_eq!(
199 batch.expire(),
200 Err(VerificationTransitionError::InvalidState)
201 );
202 }
203}