api_bones/serde/
timestamp.rs1#![cfg(feature = "chrono")]
51
52use chrono::{DateTime, TimeZone, Utc};
53use serde::{Deserializer, Serializer, de};
54
55pub fn serialize<S>(value: &DateTime<Utc>, serializer: S) -> Result<S::Ok, S::Error>
61where
62 S: Serializer,
63{
64 serializer.serialize_str(&value.to_rfc3339())
65}
66
67pub fn deserialize<'de, D>(deserializer: D) -> Result<DateTime<Utc>, D::Error>
74where
75 D: Deserializer<'de>,
76{
77 deserializer.deserialize_any(TimestampVisitor)
78}
79
80struct TimestampVisitor;
81
82impl de::Visitor<'_> for TimestampVisitor {
83 type Value = DateTime<Utc>;
84
85 fn expecting(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
86 formatter.write_str("a Unix epoch integer, float, or RFC 3339 timestamp string")
87 }
88
89 fn visit_i64<E: de::Error>(self, v: i64) -> Result<Self::Value, E> {
90 Utc.timestamp_opt(v, 0)
91 .single()
92 .ok_or_else(|| E::custom(format!("timestamp out of range: {v}")))
93 }
94
95 fn visit_u64<E: de::Error>(self, v: u64) -> Result<Self::Value, E> {
96 let secs = i64::try_from(v).map_err(|_| E::custom("timestamp out of i64 range"))?;
97 self.visit_i64(secs)
98 }
99
100 fn visit_f64<E: de::Error>(self, v: f64) -> Result<Self::Value, E> {
101 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
102 let secs = v.floor() as i64;
103 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
104 let nanos = ((v - v.floor()) * 1_000_000_000.0).round() as u32;
105 Utc.timestamp_opt(secs, nanos)
106 .single()
107 .ok_or_else(|| E::custom(format!("timestamp out of range: {v}")))
108 }
109
110 fn visit_str<E: de::Error>(self, v: &str) -> Result<Self::Value, E> {
111 v.parse::<DateTime<Utc>>().map_err(E::custom)
112 }
113}
114
115#[cfg(test)]
116mod tests {
117 use chrono::{DateTime, TimeZone, Utc};
118 use serde::{Deserialize, Serialize};
119
120 #[derive(Debug, PartialEq, Serialize, Deserialize)]
121 struct Event {
122 #[serde(with = "super")]
123 ts: DateTime<Utc>,
124 }
125
126 fn epoch() -> DateTime<Utc> {
127 Utc.timestamp_opt(0, 0).unwrap()
128 }
129
130 #[test]
133 fn serialize_as_rfc3339() {
134 let e = Event { ts: epoch() };
135 let json = serde_json::to_string(&e).unwrap();
136 assert_eq!(json, r#"{"ts":"1970-01-01T00:00:00+00:00"}"#);
137 }
138
139 #[test]
140 fn serialize_non_epoch() {
141 let ts = Utc.timestamp_opt(1_700_000_000, 0).unwrap();
142 let e = Event { ts };
143 let json = serde_json::to_string(&e).unwrap();
144 assert!(json.contains("2023-"));
145 }
146
147 #[test]
150 fn deserialize_from_i64_zero() {
151 let e: Event = serde_json::from_str(r#"{"ts":0}"#).unwrap();
152 assert_eq!(e.ts, epoch());
153 }
154
155 #[test]
156 fn deserialize_from_i64_positive() {
157 let e: Event = serde_json::from_str(r#"{"ts":1700000000}"#).unwrap();
158 assert_eq!(e.ts, Utc.timestamp_opt(1_700_000_000, 0).unwrap());
159 }
160
161 #[test]
162 fn deserialize_from_i64_negative() {
163 let e: Event = serde_json::from_str(r#"{"ts":-1}"#).unwrap();
164 assert_eq!(e.ts, Utc.timestamp_opt(-1, 0).unwrap());
165 }
166
167 #[test]
170 fn deserialize_from_u64() {
171 let e: Event =
173 serde_json::from_value(serde_json::json!({"ts": 1_700_000_000_u64})).unwrap();
174 assert_eq!(e.ts, Utc.timestamp_opt(1_700_000_000, 0).unwrap());
175 }
176
177 #[test]
180 fn deserialize_from_f64_with_fraction() {
181 let e: Event = serde_json::from_str(r#"{"ts":1700000000.5}"#).unwrap();
182 let expected = Utc.timestamp_opt(1_700_000_000, 500_000_000).unwrap();
183 assert_eq!(e.ts, expected);
184 }
185
186 #[test]
187 fn deserialize_from_f64_whole() {
188 let e: Event = serde_json::from_str(r#"{"ts":0.0}"#).unwrap();
189 assert_eq!(e.ts, epoch());
190 }
191
192 #[test]
195 fn deserialize_from_rfc3339_utc() {
196 let e: Event = serde_json::from_str(r#"{"ts":"1970-01-01T00:00:00+00:00"}"#).unwrap();
197 assert_eq!(e.ts, epoch());
198 }
199
200 #[test]
201 fn deserialize_from_rfc3339_z_suffix() {
202 let e: Event = serde_json::from_str(r#"{"ts":"1970-01-01T00:00:00Z"}"#).unwrap();
203 assert_eq!(e.ts, epoch());
204 }
205
206 #[test]
207 fn deserialize_from_rfc3339_non_epoch() {
208 let e: Event = serde_json::from_str(r#"{"ts":"2023-11-14T22:13:20+00:00"}"#).unwrap();
209 assert_eq!(e.ts, Utc.timestamp_opt(1_700_000_000, 0).unwrap());
210 }
211
212 #[test]
215 fn deserialize_invalid_string() {
216 let result: Result<Event, _> = serde_json::from_str(r#"{"ts":"not-a-date"}"#);
217 assert!(result.is_err());
218 }
219
220 #[test]
221 fn deserialize_u64_out_of_i64_range() {
222 let val = serde_json::json!({"ts": u64::MAX});
224 let result: Result<Event, _> = serde_json::from_value(val);
225 assert!(result.is_err());
226 }
227
228 #[test]
231 fn roundtrip() {
232 let original = Event {
233 ts: Utc.timestamp_opt(1_234_567_890, 0).unwrap(),
234 };
235 let json = serde_json::to_string(&original).unwrap();
236 let back: Event = serde_json::from_str(&json).unwrap();
237 assert_eq!(back, original);
238 }
239
240 #[test]
243 fn deserialize_invalid_type_triggers_expecting() {
244 let result: Result<Event, _> = serde_json::from_str(r#"{"ts":true}"#);
246 assert!(result.is_err());
247 }
248
249 #[test]
252 fn deserialize_i64_out_of_range() {
253 let val = serde_json::json!({"ts": i64::MAX});
256 let result: Result<Event, _> = serde_json::from_value(val);
257 assert!(result.is_err());
258 }
259
260 #[test]
263 fn deserialize_f64_out_of_range() {
264 let val = serde_json::json!({"ts": 1.0e18_f64});
266 let result: Result<Event, _> = serde_json::from_value(val);
267 assert!(result.is_err());
268 }
269}