use std::{error::Error, fmt};
use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
#[serde(transparent)]
pub struct UnixMillis(i64);
impl UnixMillis {
pub const fn new(value: i64) -> Self {
Self(value)
}
pub const fn get(self) -> i64 {
self.0
}
pub fn checked_add(self, duration: DurationMillis) -> Result<Self, TimeError> {
let duration = i64::try_from(duration.0).map_err(|_| TimeError::Overflow)?;
self.0
.checked_add(duration)
.map(Self)
.ok_or(TimeError::Overflow)
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
#[serde(transparent)]
pub struct DurationMillis(u64);
impl DurationMillis {
pub const fn new(value: u64) -> Self {
Self(value)
}
pub const fn get(self) -> u64 {
self.0
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct FreshWindow {
observed_at: UnixMillis,
expires_at: UnixMillis,
}
impl FreshWindow {
pub fn new(observed_at: UnixMillis, expires_at: UnixMillis) -> Result<Self, TimeError> {
if expires_at < observed_at {
return Err(TimeError::InvertedWindow);
}
Ok(Self {
observed_at,
expires_at,
})
}
pub fn for_duration(
observed_at: UnixMillis,
duration: DurationMillis,
) -> Result<Self, TimeError> {
Self::new(observed_at, observed_at.checked_add(duration)?)
}
pub const fn observed_at(self) -> UnixMillis {
self.observed_at
}
pub const fn expires_at(self) -> UnixMillis {
self.expires_at
}
pub const fn state_at(self, now: UnixMillis) -> TemporalState {
if now.0 < self.observed_at.0 {
TemporalState::NotYetValid
} else if now.0 <= self.expires_at.0 {
TemporalState::Fresh
} else {
TemporalState::Stale
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TemporalState {
NotYetValid,
Fresh,
Stale,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TimeError {
InvertedWindow,
Overflow,
}
impl fmt::Display for TimeError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
let message = match self {
Self::InvertedWindow => "freshness expiry precedes observation time",
Self::Overflow => "timestamp arithmetic overflowed",
};
formatter.write_str(message)
}
}
impl Error for TimeError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn freshness_boundaries_are_explicit() {
let window = FreshWindow::new(UnixMillis::new(10), UnixMillis::new(20)).unwrap();
assert_eq!(
window.state_at(UnixMillis::new(9)),
TemporalState::NotYetValid
);
assert_eq!(window.state_at(UnixMillis::new(10)), TemporalState::Fresh);
assert_eq!(window.state_at(UnixMillis::new(20)), TemporalState::Fresh);
assert_eq!(window.state_at(UnixMillis::new(21)), TemporalState::Stale);
}
}