artifactize_core/auth/codex/
timestamp.rs1use std::time::{Duration, SystemTime, UNIX_EPOCH};
6
7use serde::{Deserialize, Deserializer, Serialize, Serializer};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
10pub struct Timestamp(Duration);
11
12impl Timestamp {
13 pub fn now() -> Result<Self, String> {
14 SystemTime::now()
15 .duration_since(UNIX_EPOCH)
16 .map(|elapsed| Self::from_seconds(elapsed.as_secs()))
17 .map_err(|_| "The system clock is before the Unix epoch.".into())
18 }
19
20 pub fn from_seconds(seconds: u64) -> Self {
21 Self(Duration::from_secs(seconds))
22 }
23
24 pub fn seconds(self) -> u64 {
25 self.0.as_secs()
26 }
27
28 pub fn checked_add(self, duration: Duration) -> Option<Self> {
29 self.0
30 .checked_add(duration)
31 .map(|elapsed| Self::from_seconds(elapsed.as_secs()))
32 }
33
34 pub fn saturating_add(self, duration: Duration) -> Self {
35 Self::from_seconds(self.0.saturating_add(duration).as_secs())
36 }
37
38 pub fn saturating_since(self, earlier: Self) -> Duration {
40 self.0.saturating_sub(earlier.0)
41 }
42}
43
44impl Serialize for Timestamp {
45 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
46 seconds::serialize(self, serializer)
47 }
48}
49impl<'de> Deserialize<'de> for Timestamp {
50 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
51 seconds::deserialize(deserializer)
52 }
53}
54
55pub(super) mod seconds {
56 use super::*;
57 pub fn serialize<S: Serializer>(time: &Timestamp, serializer: S) -> Result<S::Ok, S::Error> {
58 time.seconds().serialize(serializer)
59 }
60 pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Timestamp, D::Error> {
61 u64::deserialize(deserializer).map(Timestamp::from_seconds)
62 }
63}
64
65pub(super) mod optional_duration {
66 use super::*;
67 pub fn deserialize<'de, D: Deserializer<'de>>(
68 deserializer: D,
69 ) -> Result<Option<Duration>, D::Error> {
70 Option::<u64>::deserialize(deserializer).map(|value| value.map(Duration::from_secs))
71 }
72}
73
74#[cfg(test)]
75mod tests {
76 use super::*;
77
78 #[test]
79 fn integer_seconds_keep_the_entire_saved_domain() {
80 for seconds in [0, 1, 10_000, u64::MAX] {
81 let wire = seconds.to_string();
82 let time: Timestamp = serde_json::from_str(&wire).unwrap();
83 assert_eq!(time.seconds(), seconds);
84 assert_eq!(serde_json::to_string(&time).unwrap(), wire);
85 }
86 for wire in ["-1", "1.5", "\"100\"", "null"] {
87 assert!(serde_json::from_str::<Timestamp>(wire).is_err());
88 }
89 }
90
91 #[test]
92 fn timestamp_margins_preserve_checked_and_saturating_overflow() {
93 let time = Timestamp::from_seconds(10_000);
94 assert_eq!(
95 time.checked_add(Duration::from_secs(3600))
96 .unwrap()
97 .seconds(),
98 13_600
99 );
100 let last = Timestamp::from_seconds(u64::MAX);
101 assert!(last.checked_add(Duration::from_secs(1)).is_none());
102 assert_eq!(last.saturating_add(Duration::from_secs(300)), last);
103 }
104}