1use std::fmt;
5
6use serde::{Deserialize, Deserializer, Serialize};
7
8pub const MAX_TIMESTAMP_MS: i64 = 253_402_300_799_999;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
13#[serde(transparent)]
14pub struct Timestamp(i64);
15
16impl schemars::JsonSchema for Timestamp {
17 fn schema_name() -> std::borrow::Cow<'static, str> {
18 "Timestamp".into()
19 }
20
21 fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
23 schemars::json_schema!({
24 "type": "integer",
25 "minimum": 0,
26 "maximum": MAX_TIMESTAMP_MS
27 })
28 }
29}
30
31#[derive(
33 Debug,
34 Clone,
35 Copy,
36 PartialEq,
37 Eq,
38 PartialOrd,
39 Ord,
40 Hash,
41 Serialize,
42 Deserialize,
43 schemars::JsonSchema,
44)]
45#[serde(transparent)]
46pub struct DurationMs(pub u64);
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub struct TimeRangeError;
52
53impl fmt::Display for TimeRangeError {
54 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
55 write!(f, "timestamp outside 0..={MAX_TIMESTAMP_MS} ms")
56 }
57}
58
59impl std::error::Error for TimeRangeError {}
60
61impl Timestamp {
62 pub fn from_ms(ms: i64) -> Result<Self, TimeRangeError> {
64 if (0..=MAX_TIMESTAMP_MS).contains(&ms) {
65 Ok(Self(ms))
66 } else {
67 Err(TimeRangeError)
68 }
69 }
70
71 pub fn as_ms(self) -> i64 {
72 self.0
73 }
74
75 pub fn since(self, earlier: Timestamp) -> Result<DurationMs, TimeRangeError> {
77 if self.0 < earlier.0 {
78 return Err(TimeRangeError);
79 }
80 Ok(DurationMs((self.0 - earlier.0) as u64))
82 }
83
84 pub fn checked_add(self, d: DurationMs) -> Result<Timestamp, TimeRangeError> {
85 let d = i64::try_from(d.0).map_err(|_| TimeRangeError)?;
86 Timestamp::from_ms(self.0.checked_add(d).ok_or(TimeRangeError)?)
87 }
88
89 pub fn checked_sub(self, d: DurationMs) -> Result<Timestamp, TimeRangeError> {
90 let d = i64::try_from(d.0).map_err(|_| TimeRangeError)?;
91 Timestamp::from_ms(self.0.checked_sub(d).ok_or(TimeRangeError)?)
92 }
93}
94
95impl<'de> Deserialize<'de> for Timestamp {
96 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
97 let ms = i64::deserialize(d)?;
98 Timestamp::from_ms(ms).map_err(serde::de::Error::custom)
99 }
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105
106 #[test]
107 fn range_is_enforced_at_both_ends() {
108 assert!(Timestamp::from_ms(0).is_ok());
109 assert!(Timestamp::from_ms(MAX_TIMESTAMP_MS).is_ok());
110 assert!(Timestamp::from_ms(-1).is_err());
111 assert!(Timestamp::from_ms(MAX_TIMESTAMP_MS + 1).is_err());
112 assert!(serde_json::from_str::<Timestamp>("-1").is_err());
113 assert!(serde_json::from_str::<Timestamp>("1.5").is_err());
114 }
115
116 #[test]
117 fn arithmetic_is_checked() {
118 let t = Timestamp::from_ms(1_000).unwrap();
119 assert_eq!(
120 t.since(Timestamp::from_ms(400).unwrap()),
121 Ok(DurationMs(600))
122 );
123 assert!(t.since(Timestamp::from_ms(1_001).unwrap()).is_err());
124 assert!(t.checked_sub(DurationMs(1_001)).is_err());
125 assert!(
126 Timestamp::from_ms(MAX_TIMESTAMP_MS)
127 .unwrap()
128 .checked_add(DurationMs(1))
129 .is_err()
130 );
131 assert!(t.checked_add(DurationMs(u64::MAX)).is_err());
132 }
133}