1use std::{error::Error, fmt};
2
3use serde::{Deserialize, Serialize};
4
5#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
7#[serde(transparent)]
8pub struct UnixMillis(i64);
9
10impl UnixMillis {
11 pub const fn new(value: i64) -> Self {
12 Self(value)
13 }
14
15 pub const fn get(self) -> i64 {
16 self.0
17 }
18
19 pub fn checked_add(self, duration: DurationMillis) -> Result<Self, TimeError> {
25 let duration = i64::try_from(duration.0).map_err(|_| TimeError::Overflow)?;
26 self.0
27 .checked_add(duration)
28 .map(Self)
29 .ok_or(TimeError::Overflow)
30 }
31}
32
33#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
35#[serde(transparent)]
36pub struct DurationMillis(u64);
37
38impl DurationMillis {
39 pub const fn new(value: u64) -> Self {
40 Self(value)
41 }
42
43 pub const fn get(self) -> u64 {
44 self.0
45 }
46}
47
48#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
50pub struct FreshWindow {
51 observed_at: UnixMillis,
52 expires_at: UnixMillis,
53}
54
55impl FreshWindow {
56 pub fn new(observed_at: UnixMillis, expires_at: UnixMillis) -> Result<Self, TimeError> {
62 if expires_at < observed_at {
63 return Err(TimeError::InvertedWindow);
64 }
65 Ok(Self {
66 observed_at,
67 expires_at,
68 })
69 }
70
71 pub fn for_duration(
77 observed_at: UnixMillis,
78 duration: DurationMillis,
79 ) -> Result<Self, TimeError> {
80 Self::new(observed_at, observed_at.checked_add(duration)?)
81 }
82
83 pub const fn observed_at(self) -> UnixMillis {
84 self.observed_at
85 }
86
87 pub const fn expires_at(self) -> UnixMillis {
88 self.expires_at
89 }
90
91 pub const fn state_at(self, now: UnixMillis) -> TemporalState {
93 if now.0 < self.observed_at.0 {
94 TemporalState::NotYetValid
95 } else if now.0 <= self.expires_at.0 {
96 TemporalState::Fresh
97 } else {
98 TemporalState::Stale
99 }
100 }
101}
102
103#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
104#[serde(rename_all = "snake_case")]
105pub enum TemporalState {
106 NotYetValid,
107 Fresh,
108 Stale,
109}
110
111#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
112#[serde(rename_all = "snake_case")]
113pub enum TimeError {
114 InvertedWindow,
115 Overflow,
116}
117
118impl fmt::Display for TimeError {
119 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
120 let message = match self {
121 Self::InvertedWindow => "freshness expiry precedes observation time",
122 Self::Overflow => "timestamp arithmetic overflowed",
123 };
124 formatter.write_str(message)
125 }
126}
127
128impl Error for TimeError {}
129
130#[cfg(test)]
131mod tests {
132 use super::*;
133
134 #[test]
135 fn freshness_boundaries_are_explicit() {
136 let window = FreshWindow::new(UnixMillis::new(10), UnixMillis::new(20)).unwrap();
137 assert_eq!(
138 window.state_at(UnixMillis::new(9)),
139 TemporalState::NotYetValid
140 );
141 assert_eq!(window.state_at(UnixMillis::new(10)), TemporalState::Fresh);
142 assert_eq!(window.state_at(UnixMillis::new(20)), TemporalState::Fresh);
143 assert_eq!(window.state_at(UnixMillis::new(21)), TemporalState::Stale);
144 }
145}