cve/
timestamp.rs

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    /// Full offset information
14    Offset(OffsetDateTime),
15    /// No offset information
16    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}