1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
use serde::{de::Visitor, ser::Error, Deserialize, Deserializer, Serialize, Serializer};
use std::{fmt::Formatter, num::NonZeroU8};
use time::{
    format_description::well_known::{
        iso8601::{Config, EncodedConfig, FormattedComponents, TimePrecision},
        Iso8601,
    },
    OffsetDateTime, PrimitiveDateTime,
};

#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub enum Timestamp {
    /// Full offset information
    Offset(OffsetDateTime),
    /// No offset information
    Primitive(PrimitiveDateTime),
}

impl Timestamp {
    pub fn assume_utc(self) -> OffsetDateTime {
        match self {
            Self::Offset(value) => value,
            Self::Primitive(value) => value.assume_utc(),
        }
    }
}

impl From<OffsetDateTime> for Timestamp {
    fn from(value: OffsetDateTime) -> Self {
        Self::Offset(value)
    }
}

impl From<PrimitiveDateTime> for Timestamp {
    fn from(value: PrimitiveDateTime) -> Self {
        Self::Primitive(value)
    }
}

impl Serialize for Timestamp {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        const OFFSET_FORMAT: EncodedConfig = Config::DEFAULT
            .set_time_precision(TimePrecision::Second {
                decimal_digits: Some(unsafe { NonZeroU8::new_unchecked(3) }),
            })
            .encode();

        const PRIMITIVE_FORMAT: EncodedConfig = Config::DEFAULT
            .set_formatted_components(FormattedComponents::DateTime)
            .set_time_precision(TimePrecision::Second {
                decimal_digits: Some(unsafe { NonZeroU8::new_unchecked(3) }),
            })
            .encode();

        match self {
            Self::Offset(value) => {
                let value = value
                    .format(&Iso8601::<OFFSET_FORMAT>)
                    .map_err(|err| Error::custom(format!("Failed to encode timestamp: {err}")))?;
                serializer.serialize_str(&value)
            }
            Self::Primitive(value) => {
                let value = value
                    .format(&Iso8601::<PRIMITIVE_FORMAT>)
                    .map_err(|err| Error::custom(format!("Failed to encode timestamp: {err}")))?;
                serializer.serialize_str(&value)
            }
        }
    }
}

impl<'de> Deserialize<'de> for Timestamp {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        struct TimestampVisitor;

        impl<'de> Visitor<'de> for TimestampVisitor {
            type Value = Timestamp;

            fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
                formatter.write_str("an ISO 8601 timestamp with our without timezone")
            }

            fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
            where
                E: serde::de::Error,
            {
                if let Ok(result) = OffsetDateTime::parse(v, &Iso8601::PARSING) {
                    return Ok(result.into());
                }
                if let Ok(result) = PrimitiveDateTime::parse(v, &Iso8601::PARSING) {
                    return Ok(result.into());
                }

                Err(E::custom(format!("unable to parse '{v}' as ISO 8601 timestamp")))
            }
        }

        deserializer.deserialize_str(TimestampVisitor)
    }
}

#[cfg(test)]
mod test {
    use super::*;
    use time::macros::datetime;

    #[test]
    pub fn serialize_timestamp_offset() {
        assert_eq!(
            &serde_json::to_string(&Timestamp::from(datetime!(2020-01-02 12:34 ).assume_utc())).unwrap(),
            r#""2020-01-02T12:34:00.000Z""#
        );

        assert_eq!(
            &serde_json::to_string(&Timestamp::from(datetime!(2020-01-02 12:34 +01:00))).unwrap(),
            r#""2020-01-02T12:34:00.000+01:00""#
        );
    }

    #[test]
    pub fn serialize_timestamp_primitive() {
        assert_eq!(
            &serde_json::to_string(&Timestamp::from(datetime!(2020-01-02 12:34))).unwrap(),
            r#""2020-01-02T12:34:00.000""#
        );
    }
}