1use crate::DEFAULT_TIMESTAMP_TOLERANCE_SECS;
4use chrono::{DateTime, Utc};
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
12#[serde(transparent)]
13pub struct Timestamp(pub i64);
14
15impl Timestamp {
16 pub fn now() -> Self {
18 Self(Utc::now().timestamp())
19 }
20
21 pub fn plus_secs(self, secs: i64) -> Self {
23 Self(self.0.saturating_add(secs))
24 }
25
26 pub fn is_within_tolerance_of(self, reference: Timestamp, tolerance_secs: i64) -> bool {
34 let tolerance = tolerance_secs.max(0) as u64;
35 match self.0.checked_sub(reference.0) {
36 Some(diff) => diff.unsigned_abs() <= tolerance,
37 None => false,
39 }
40 }
41
42 pub fn is_fresh(self, reference: Timestamp) -> bool {
44 self.is_within_tolerance_of(reference, DEFAULT_TIMESTAMP_TOLERANCE_SECS)
45 }
46
47 pub fn is_in_the_past(self, reference: Timestamp) -> bool {
49 self.0 < reference.0
50 }
51
52 pub fn is_in_the_future(self, reference: Timestamp) -> bool {
54 self.0 > reference.0
55 }
56}
57
58impl From<DateTime<Utc>> for Timestamp {
59 fn from(dt: DateTime<Utc>) -> Self {
60 Self(dt.timestamp())
61 }
62}
63
64impl From<i64> for Timestamp {
65 fn from(secs: i64) -> Self {
66 Self(secs)
67 }
68}
69
70#[cfg(test)]
71mod tests {
72 use super::*;
73
74 #[test]
75 fn freshness_within_tolerance() {
76 let now = Timestamp(1_700_000_000);
77 assert!(Timestamp(1_700_000_100).is_fresh(now));
78 assert!(Timestamp(1_699_999_900).is_fresh(now));
79 assert!(!Timestamp(1_700_000_400).is_fresh(now));
80 }
81
82 #[test]
83 fn tolerance_does_not_overflow_on_extremes() {
84 let now = Timestamp(1_700_000_000);
85 assert!(!Timestamp(i64::MIN).is_within_tolerance_of(now, 300));
88 assert!(!Timestamp(i64::MAX).is_within_tolerance_of(now, 300));
89 assert!(!Timestamp(i64::MIN).is_within_tolerance_of(Timestamp(i64::MAX), 300));
90 assert!(!Timestamp(1_700_000_001).is_within_tolerance_of(now, -5));
92 assert!(now.is_within_tolerance_of(now, -5));
93 }
94
95 #[test]
96 fn ordering_works_as_expected() {
97 assert!(Timestamp(100) < Timestamp(200));
98 }
99
100 #[test]
101 fn now_is_close_to_chrono_now() {
102 let a = Timestamp::now().0;
103 let b = chrono::Utc::now().timestamp();
104 assert!((a - b).abs() <= 5, "Timestamp::now() drift: {} vs {}", a, b);
105 }
106
107 #[test]
108 fn serializes_as_json_integer_not_string() {
109 let s = serde_json::to_string(&Timestamp(1_700_000_000)).unwrap();
110 assert_eq!(s, "1700000000");
111 let back: Timestamp = serde_json::from_str(&s).unwrap();
112 assert_eq!(back, Timestamp(1_700_000_000));
113 }
114}