1use serde::{Deserialize, Serialize};
27use std::fmt;
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
37#[serde(rename_all = "lowercase")]
38#[repr(u8)]
39pub enum Phase {
40 Open = 0,
41 Doing = 1,
42 Done = 2,
43 Archived = 3,
44}
45
46impl Phase {
47 #[must_use]
49 pub const fn rank(self) -> u8 {
50 self as u8
51 }
52
53 #[must_use]
55 pub const fn as_str(self) -> &'static str {
56 match self {
57 Self::Open => "open",
58 Self::Doing => "doing",
59 Self::Done => "done",
60 Self::Archived => "archived",
61 }
62 }
63
64 pub const ALL: [Self; 4] = [Self::Open, Self::Doing, Self::Done, Self::Archived];
66}
67
68impl PartialOrd for Phase {
69 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
70 Some(self.cmp(other))
71 }
72}
73
74impl Ord for Phase {
75 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
76 self.rank().cmp(&other.rank())
77 }
78}
79
80impl fmt::Display for Phase {
81 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
82 f.write_str(self.as_str())
83 }
84}
85
86impl std::str::FromStr for Phase {
87 type Err = String;
88
89 fn from_str(s: &str) -> Result<Self, Self::Err> {
90 match s {
91 "open" => Ok(Self::Open),
92 "doing" => Ok(Self::Doing),
93 "done" => Ok(Self::Done),
94 "archived" => Ok(Self::Archived),
95 _ => Err(format!("unknown phase: {s}")),
96 }
97 }
98}
99
100#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
109pub struct EpochPhaseState {
110 pub epoch: u64,
112 pub phase: Phase,
114}
115
116impl EpochPhaseState {
117 #[must_use]
119 pub const fn new() -> Self {
120 Self {
121 epoch: 0,
122 phase: Phase::Open,
123 }
124 }
125
126 #[must_use]
128 pub const fn with(epoch: u64, phase: Phase) -> Self {
129 Self { epoch, phase }
130 }
131
132 pub fn advance(&mut self, target: Phase) -> Result<(), StateError> {
142 if target <= self.phase {
143 return Err(StateError::InvalidTransition {
144 from: self.phase,
145 to: target,
146 epoch: self.epoch,
147 });
148 }
149 self.phase = target;
150 Ok(())
151 }
152
153 pub const fn reopen(&mut self) {
158 self.epoch += 1;
159 self.phase = Phase::Open;
160 }
161
162 pub fn merge(&mut self, other: &Self) {
168 match self.epoch.cmp(&other.epoch) {
169 std::cmp::Ordering::Less => {
170 self.epoch = other.epoch;
172 self.phase = other.phase;
173 }
174 std::cmp::Ordering::Equal => {
175 if other.phase > self.phase {
177 self.phase = other.phase;
178 }
179 }
180 std::cmp::Ordering::Greater => {
181 }
183 }
184 }
185}
186
187impl Default for EpochPhaseState {
188 fn default() -> Self {
189 Self::new()
190 }
191}
192
193impl fmt::Display for EpochPhaseState {
194 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
195 write!(f, "epoch={} phase={}", self.epoch, self.phase)
196 }
197}
198
199#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
205pub enum StateError {
206 #[error("invalid transition from {from} to {to} in epoch {epoch}")]
207 InvalidTransition { from: Phase, to: Phase, epoch: u64 },
208}
209
210#[cfg(test)]
215mod tests {
216 use super::*;
217
218 #[test]
221 fn phase_ranking() {
222 assert!(Phase::Open < Phase::Doing);
223 assert!(Phase::Doing < Phase::Done);
224 assert!(Phase::Done < Phase::Archived);
225 }
226
227 #[test]
228 fn phase_rank_values() {
229 assert_eq!(Phase::Open.rank(), 0);
230 assert_eq!(Phase::Doing.rank(), 1);
231 assert_eq!(Phase::Done.rank(), 2);
232 assert_eq!(Phase::Archived.rank(), 3);
233 }
234
235 #[test]
236 fn phase_display_and_parse() {
237 for phase in Phase::ALL {
238 let s = phase.to_string();
239 let parsed: Phase = s.parse().unwrap();
240 assert_eq!(phase, parsed);
241 }
242 }
243
244 #[test]
247 fn new_state_is_epoch_0_open() {
248 let s = EpochPhaseState::new();
249 assert_eq!(s.epoch, 0);
250 assert_eq!(s.phase, Phase::Open);
251 }
252
253 #[test]
254 fn advance_forward() {
255 let mut s = EpochPhaseState::new();
256 s.advance(Phase::Doing).unwrap();
257 assert_eq!(s.phase, Phase::Doing);
258 s.advance(Phase::Done).unwrap();
259 assert_eq!(s.phase, Phase::Done);
260 s.advance(Phase::Archived).unwrap();
261 assert_eq!(s.phase, Phase::Archived);
262 }
263
264 #[test]
265 fn advance_backward_fails() {
266 let mut s = EpochPhaseState::with(0, Phase::Done);
267 let err = s.advance(Phase::Doing).unwrap_err();
268 assert!(matches!(err, StateError::InvalidTransition { .. }));
269 }
270
271 #[test]
272 fn advance_same_phase_fails() {
273 let mut s = EpochPhaseState::with(0, Phase::Doing);
274 let err = s.advance(Phase::Doing).unwrap_err();
275 assert!(matches!(err, StateError::InvalidTransition { .. }));
276 }
277
278 #[test]
281 fn reopen_increments_epoch() {
282 let mut s = EpochPhaseState::with(0, Phase::Done);
283 s.reopen();
284 assert_eq!(s.epoch, 1);
285 assert_eq!(s.phase, Phase::Open);
286 }
287
288 #[test]
289 fn reopen_from_archived() {
290 let mut s = EpochPhaseState::with(0, Phase::Archived);
291 s.reopen();
292 assert_eq!(s.epoch, 1);
293 assert_eq!(s.phase, Phase::Open);
294 }
295
296 #[test]
297 fn multiple_reopens_monotonic_epochs() {
298 let mut s = EpochPhaseState::new();
299 for expected_epoch in 1..=5 {
300 s.advance(Phase::Done).unwrap_or(()); s.reopen();
302 assert_eq!(s.epoch, expected_epoch);
303 assert_eq!(s.phase, Phase::Open);
304 }
305 }
306
307 #[test]
310 fn merge_same_epoch_higher_phase_wins() {
311 let mut a = EpochPhaseState::with(0, Phase::Open);
312 let b = EpochPhaseState::with(0, Phase::Done);
313 a.merge(&b);
314 assert_eq!(a.phase, Phase::Done);
315 assert_eq!(a.epoch, 0);
316 }
317
318 #[test]
319 fn merge_same_epoch_lower_phase_no_change() {
320 let mut a = EpochPhaseState::with(0, Phase::Done);
321 let b = EpochPhaseState::with(0, Phase::Open);
322 a.merge(&b);
323 assert_eq!(a.phase, Phase::Done);
324 }
325
326 #[test]
327 fn merge_same_epoch_same_phase_idempotent() {
328 let mut a = EpochPhaseState::with(0, Phase::Doing);
329 let b = EpochPhaseState::with(0, Phase::Doing);
330 a.merge(&b);
331 assert_eq!(a.phase, Phase::Doing);
332 assert_eq!(a.epoch, 0);
333 }
334
335 #[test]
338 fn merge_higher_epoch_wins() {
339 let mut a = EpochPhaseState::with(1, Phase::Open);
340 let b = EpochPhaseState::with(2, Phase::Doing);
341 a.merge(&b);
342 assert_eq!(a.epoch, 2);
343 assert_eq!(a.phase, Phase::Doing);
344 }
345
346 #[test]
347 fn merge_lower_epoch_no_change() {
348 let mut a = EpochPhaseState::with(3, Phase::Doing);
349 let b = EpochPhaseState::with(1, Phase::Archived);
350 a.merge(&b);
351 assert_eq!(a.epoch, 3);
352 assert_eq!(a.phase, Phase::Doing);
353 }
354
355 #[test]
358 fn concurrent_close_and_reopen_reopen_wins() {
359 let close = EpochPhaseState::with(0, Phase::Done);
361 let reopen = EpochPhaseState::with(1, Phase::Open);
363
364 let mut m1 = close.clone();
366 m1.merge(&reopen);
367 assert_eq!(m1.epoch, 1);
368 assert_eq!(m1.phase, Phase::Open);
369
370 let mut m2 = reopen.clone();
372 m2.merge(&close);
373 assert_eq!(m2.epoch, 1);
374 assert_eq!(m2.phase, Phase::Open);
375
376 assert_eq!(m1, m2);
378 }
379
380 #[test]
383 fn semilattice_commutative() {
384 let cases = vec![
385 (
386 EpochPhaseState::with(0, Phase::Open),
387 EpochPhaseState::with(0, Phase::Done),
388 ),
389 (
390 EpochPhaseState::with(1, Phase::Doing),
391 EpochPhaseState::with(0, Phase::Archived),
392 ),
393 (
394 EpochPhaseState::with(2, Phase::Open),
395 EpochPhaseState::with(2, Phase::Doing),
396 ),
397 ];
398 for (a, b) in cases {
399 let mut ab = a.clone();
400 ab.merge(&b);
401 let mut ba = b.clone();
402 ba.merge(&a);
403 assert_eq!(ab, ba, "commutative failed for {a:?} and {b:?}");
404 }
405 }
406
407 #[test]
408 fn semilattice_associative() {
409 let a = EpochPhaseState::with(1, Phase::Open);
410 let b = EpochPhaseState::with(0, Phase::Done);
411 let c = EpochPhaseState::with(1, Phase::Doing);
412
413 let mut left = a.clone();
415 left.merge(&b);
416 left.merge(&c);
417
418 let mut bc = b.clone();
420 bc.merge(&c);
421 let mut right = a.clone();
422 right.merge(&bc);
423
424 assert_eq!(left, right);
425 }
426
427 #[test]
428 fn semilattice_idempotent() {
429 let a = EpochPhaseState::with(2, Phase::Doing);
430 let mut m = a.clone();
431 m.merge(&a);
432 assert_eq!(m, a);
433 }
434
435 #[test]
438 fn merge_default_with_default() {
439 let mut a = EpochPhaseState::default();
440 let b = EpochPhaseState::default();
441 a.merge(&b);
442 assert_eq!(a, EpochPhaseState::new());
443 }
444
445 #[test]
446 fn display() {
447 let s = EpochPhaseState::with(3, Phase::Done);
448 assert_eq!(s.to_string(), "epoch=3 phase=done");
449 }
450
451 #[test]
452 fn serde_roundtrip() {
453 let s = EpochPhaseState::with(5, Phase::Archived);
454 let json = serde_json::to_string(&s).unwrap();
455 let deserialized: EpochPhaseState = serde_json::from_str(&json).unwrap();
456 assert_eq!(s, deserialized);
457 }
458
459 #[test]
460 fn phase_serde_roundtrip() {
461 for phase in Phase::ALL {
462 let json = serde_json::to_string(&phase).unwrap();
463 let deserialized: Phase = serde_json::from_str(&json).unwrap();
464 assert_eq!(phase, deserialized);
465 }
466 }
467}