1use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Visitor, ser::Error};
2use std::{fmt::Formatter, num::NonZeroU8};
3use time::{
4 OffsetDateTime, PrimitiveDateTime,
5 format_description::well_known::{
6 Iso8601,
7 iso8601::{Config, EncodedConfig, FormattedComponents, TimePrecision},
8 },
9};
10
11#[derive(Copy, Clone, PartialEq, Eq, Debug)]
12pub enum Timestamp {
13 Offset(OffsetDateTime),
15 Primitive(PrimitiveDateTime),
17}
18
19impl Timestamp {
20 pub fn assume_utc(self) -> OffsetDateTime {
21 match self {
22 Self::Offset(value) => value,
23 Self::Primitive(value) => value.assume_utc(),
24 }
25 }
26}
27
28impl From<OffsetDateTime> for Timestamp {
29 fn from(value: OffsetDateTime) -> Self {
30 Self::Offset(value)
31 }
32}
33
34impl From<PrimitiveDateTime> for Timestamp {
35 fn from(value: PrimitiveDateTime) -> Self {
36 Self::Primitive(value)
37 }
38}
39
40impl Serialize for Timestamp {
41 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
42 where
43 S: Serializer,
44 {
45 const OFFSET_FORMAT: EncodedConfig = Config::DEFAULT
46 .set_time_precision(TimePrecision::Second {
47 decimal_digits: Some(NonZeroU8::new(3).unwrap()),
48 })
49 .encode();
50
51 const PRIMITIVE_FORMAT: EncodedConfig = Config::DEFAULT
52 .set_formatted_components(FormattedComponents::DateTime)
53 .set_time_precision(TimePrecision::Second {
54 decimal_digits: Some(NonZeroU8::new(3).unwrap()),
55 })
56 .encode();
57
58 match self {
59 Self::Offset(value) => {
60 let value = value
61 .format(&Iso8601::<OFFSET_FORMAT>)
62 .map_err(|err| Error::custom(format!("Failed to encode timestamp: {err}")))?;
63 serializer.serialize_str(&value)
64 }
65 Self::Primitive(value) => {
66 let value = value
67 .format(&Iso8601::<PRIMITIVE_FORMAT>)
68 .map_err(|err| Error::custom(format!("Failed to encode timestamp: {err}")))?;
69 serializer.serialize_str(&value)
70 }
71 }
72 }
73}
74
75impl<'de> Deserialize<'de> for Timestamp {
76 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
77 where
78 D: Deserializer<'de>,
79 {
80 struct TimestampVisitor;
81
82 impl Visitor<'_> for TimestampVisitor {
83 type Value = Timestamp;
84
85 fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
86 formatter.write_str("an ISO 8601 timestamp with our without timezone")
87 }
88
89 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
90 where
91 E: serde::de::Error,
92 {
93 if let Ok(result) = OffsetDateTime::parse(v, &Iso8601::PARSING) {
94 return Ok(result.into());
95 }
96 if let Ok(result) = PrimitiveDateTime::parse(v, &Iso8601::PARSING) {
97 return Ok(result.into());
98 }
99
100 Err(E::custom(format!(
101 "unable to parse '{v}' as ISO 8601 timestamp"
102 )))
103 }
104 }
105
106 deserializer.deserialize_str(TimestampVisitor)
107 }
108}
109
110#[cfg(test)]
111mod test {
112 use super::*;
113 use time::macros::datetime;
114
115 #[test]
116 pub fn serialize_timestamp_offset() {
117 assert_eq!(
118 &serde_json::to_string(&Timestamp::from(datetime!(2020-01-02 12:34 ).assume_utc()))
119 .unwrap(),
120 r#""2020-01-02T12:34:00.000Z""#
121 );
122
123 assert_eq!(
124 &serde_json::to_string(&Timestamp::from(datetime!(2020-01-02 12:34 +01:00))).unwrap(),
125 r#""2020-01-02T12:34:00.000+01:00""#
126 );
127 }
128
129 #[test]
130 pub fn serialize_timestamp_primitive() {
131 assert_eq!(
132 &serde_json::to_string(&Timestamp::from(datetime!(2020-01-02 12:34))).unwrap(),
133 r#""2020-01-02T12:34:00.000""#
134 );
135 }
136
137 #[test]
138 pub fn deserialize_invalid_timestamp() {
139 let invalid = r#""invalid-timestamp-foo""#;
140 let err = serde_json::from_str::<Timestamp>(invalid).unwrap_err();
141 assert!(err.to_string().contains("unable to parse"));
142 }
143
144 #[test]
145 pub fn deserialize_timestamp_primitive() {
146 let s = r#""2020-01-02T12:34:00.000""#;
147 let ts = serde_json::from_str(s).unwrap();
148 assert!(matches!(ts, Timestamp::Primitive(_)));
149 }
150
151 #[test]
152 pub fn deserialize_timestamp_offset() {
153 let s = r#""2020-01-02T12:34:00.000Z""#;
154 let ts = serde_json::from_str(s).unwrap();
155 assert!(matches!(ts, Timestamp::Offset(_)));
156 }
157
158 #[test]
159 pub fn assume_utc_for_offset_and_primitive() {
160 let offset_datetime = datetime!(2020-01-02 12:34 +01:00);
161 let primitive_datetime = datetime!(2020-01-02 12:34);
162
163 let offset_ts = Timestamp::from(offset_datetime);
164 let primitive_ts = Timestamp::from(primitive_datetime);
165
166 assert_eq!(offset_ts.assume_utc(), offset_datetime);
167 assert_eq!(primitive_ts.assume_utc(), primitive_datetime.assume_utc());
168 }
169}