use std::fmt;
use serde::{Deserialize, Deserializer, Serialize};
pub const MAX_TIMESTAMP_MS: i64 = 253_402_300_799_999;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
#[serde(transparent)]
pub struct Timestamp(i64);
impl schemars::JsonSchema for Timestamp {
fn schema_name() -> std::borrow::Cow<'static, str> {
"Timestamp".into()
}
fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
schemars::json_schema!({
"type": "integer",
"minimum": 0,
"maximum": MAX_TIMESTAMP_MS
})
}
}
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
PartialOrd,
Ord,
Hash,
Serialize,
Deserialize,
schemars::JsonSchema,
)]
#[serde(transparent)]
pub struct DurationMs(pub u64);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TimeRangeError;
impl fmt::Display for TimeRangeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "timestamp outside 0..={MAX_TIMESTAMP_MS} ms")
}
}
impl std::error::Error for TimeRangeError {}
impl Timestamp {
pub fn from_ms(ms: i64) -> Result<Self, TimeRangeError> {
if (0..=MAX_TIMESTAMP_MS).contains(&ms) {
Ok(Self(ms))
} else {
Err(TimeRangeError)
}
}
pub fn as_ms(self) -> i64 {
self.0
}
pub fn since(self, earlier: Timestamp) -> Result<DurationMs, TimeRangeError> {
if self.0 < earlier.0 {
return Err(TimeRangeError);
}
Ok(DurationMs((self.0 - earlier.0) as u64))
}
pub fn checked_add(self, d: DurationMs) -> Result<Timestamp, TimeRangeError> {
let d = i64::try_from(d.0).map_err(|_| TimeRangeError)?;
Timestamp::from_ms(self.0.checked_add(d).ok_or(TimeRangeError)?)
}
pub fn checked_sub(self, d: DurationMs) -> Result<Timestamp, TimeRangeError> {
let d = i64::try_from(d.0).map_err(|_| TimeRangeError)?;
Timestamp::from_ms(self.0.checked_sub(d).ok_or(TimeRangeError)?)
}
}
impl<'de> Deserialize<'de> for Timestamp {
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
let ms = i64::deserialize(d)?;
Timestamp::from_ms(ms).map_err(serde::de::Error::custom)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn range_is_enforced_at_both_ends() {
assert!(Timestamp::from_ms(0).is_ok());
assert!(Timestamp::from_ms(MAX_TIMESTAMP_MS).is_ok());
assert!(Timestamp::from_ms(-1).is_err());
assert!(Timestamp::from_ms(MAX_TIMESTAMP_MS + 1).is_err());
assert!(serde_json::from_str::<Timestamp>("-1").is_err());
assert!(serde_json::from_str::<Timestamp>("1.5").is_err());
}
#[test]
fn arithmetic_is_checked() {
let t = Timestamp::from_ms(1_000).unwrap();
assert_eq!(
t.since(Timestamp::from_ms(400).unwrap()),
Ok(DurationMs(600))
);
assert!(t.since(Timestamp::from_ms(1_001).unwrap()).is_err());
assert!(t.checked_sub(DurationMs(1_001)).is_err());
assert!(
Timestamp::from_ms(MAX_TIMESTAMP_MS)
.unwrap()
.checked_add(DurationMs(1))
.is_err()
);
assert!(t.checked_add(DurationMs(u64::MAX)).is_err());
}
}