json_web_tolkien/util/
epoch.rs1use std::ops::{Deref, Sub};
2use std::{fmt, ops::Add};
3
4use chrono::{DateTime, TimeDelta, TimeZone, Utc};
5use serde::de::Unexpected;
6use serde::{Deserialize, Serialize};
7
8#[derive(Clone, Debug, PartialEq, PartialOrd)]
9pub struct UtcDateTime(pub DateTime<Utc>);
10
11impl Add<TimeDelta> for UtcDateTime {
12 type Output = UtcDateTime;
13
14 fn add(self, rhs: TimeDelta) -> Self::Output {
15 let inner: DateTime<Utc> = self.0 + rhs;
16
17 UtcDateTime(inner)
18 }
19}
20
21impl AsRef<DateTime<Utc>> for UtcDateTime {
22 fn as_ref(&self) -> &DateTime<Utc> {
23 &self.0
24 }
25}
26
27impl Deref for UtcDateTime {
28 type Target = DateTime<Utc>;
29
30 fn deref(&self) -> &Self::Target {
31 &self.0
32 }
33}
34
35impl<'de> Deserialize<'de> for UtcDateTime {
36 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
37 where
38 D: serde::Deserializer<'de>,
39 {
40 deserializer.deserialize_any(Visitor)
41 }
42}
43
44impl PartialEq<DateTime<Utc>> for UtcDateTime {
45 fn eq(&self, other: &DateTime<Utc>) -> bool {
46 self.0.eq(other)
47 }
48}
49
50impl PartialOrd<DateTime<Utc>> for UtcDateTime {
51 fn partial_cmp(&self, other: &DateTime<Utc>) -> Option<std::cmp::Ordering> {
52 self.0.partial_cmp(other)
53 }
54}
55
56impl Serialize for UtcDateTime {
57 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
58 where
59 S: serde::Serializer,
60 {
61 let value: i64 = self.timestamp();
62
63 serializer.serialize_i64(value)
64 }
65}
66
67impl Sub for UtcDateTime {
68 type Output = TimeDelta;
69
70 fn sub(self, rhs: Self) -> Self::Output {
71 self.0 - rhs.0
72 }
73}
74
75impl Sub<DateTime<Utc>> for UtcDateTime {
76 type Output = TimeDelta;
77
78 fn sub(self, rhs: DateTime<Utc>) -> Self::Output {
79 self.0 - rhs
80 }
81}
82
83impl Sub<TimeDelta> for UtcDateTime {
84 type Output = UtcDateTime;
85
86 fn sub(self, rhs: TimeDelta) -> Self::Output {
87 let value: DateTime<Utc> = self.0 - rhs;
88
89 UtcDateTime(value)
90 }
91}
92
93impl PartialEq<UtcDateTime> for DateTime<Utc> {
94 fn eq(&self, other: &UtcDateTime) -> bool {
95 self.eq(&other.0)
96 }
97}
98
99impl PartialOrd<UtcDateTime> for DateTime<Utc> {
100 fn partial_cmp(&self, other: &UtcDateTime) -> Option<std::cmp::Ordering> {
101 self.partial_cmp(&other.0)
102 }
103}
104
105impl Sub<UtcDateTime> for DateTime<Utc> {
106 type Output = TimeDelta;
107
108 fn sub(self, rhs: UtcDateTime) -> Self::Output {
109 self - rhs.0
110 }
111}
112
113struct Visitor;
114
115impl<'de> serde::de::Visitor<'de> for Visitor {
116 type Value = UtcDateTime;
117
118 fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
119 f.write_str("an integer between -2^63 and 2^63 that represents epoch seconds")
120 }
121
122 fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
123 where
124 E: serde::de::Error,
125 {
126 let Some(v): Option<DateTime<Utc>> = Utc.timestamp_opt(v, 0).single() else {
127 return Err(E::invalid_value(Unexpected::Signed(v), &self));
128 };
129
130 let v = UtcDateTime(v);
131
132 Ok(v)
133 }
134
135 fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
136 where
137 E: serde::de::Error,
138 {
139 let v = i64::try_from(v).map_err(|_| E::invalid_value(Unexpected::Unsigned(v), &self))?;
140
141 self.visit_i64(v)
142 }
143
144 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
145 where
146 E: serde::de::Error,
147 {
148 let v: i64 = v
149 .parse()
150 .map_err(|_| E::invalid_value(Unexpected::Str(v), &self))?;
151
152 self.visit_i64(v)
153 }
154}
155
156#[cfg(test)]
157mod test {
158 use serde_json::json;
159
160 use super::*;
161
162 #[test]
163 fn it_serializes_and_deserializes() {
164 #[derive(Deserialize, Serialize)]
165 struct Bar {
166 foo: UtcDateTime,
167 }
168
169 let epoch: i64 = 1772144586;
170 let expected: DateTime<Utc> = Utc.with_ymd_and_hms(2026, 2, 26, 22, 23, 6).unwrap();
171 let future = expected.clone() + TimeDelta::days(1);
172
173 let input = json!({ "foo": "1772144586" });
174
175 let actual: UtcDateTime = {
176 let bar: Bar = serde_json::from_value(input).unwrap();
177
178 bar.foo
179 };
180
181 assert_eq!(expected, actual);
182 assert!(actual < future);
183
184 let input = json!({ "foo": epoch });
185
186 let actual: UtcDateTime = {
187 let bar: Bar = serde_json::from_value(input).unwrap();
188
189 bar.foo
190 };
191
192 assert_eq!(expected, actual);
193 }
194}