Skip to main content

defuse_time/
serde.rs

1use core::marker::PhantomData;
2
3use serde::{Deserializer, Serializer, de};
4use serde_with::{DeserializeAs, Same, SerializeAs};
5
6use crate::{Overflow, Timestamp};
7
8macro_rules! serde_as {
9    ($($vis:vis struct $name:ident: $int:ty {
10        $as:ident,
11        $from:ident,
12    })*) => {$(
13        $vis struct $name<F: ?Sized = Same>(PhantomData<F>);
14
15        impl<F> SerializeAs<Timestamp> for $name<F>
16        where
17            F: SerializeAs<$int> + ?Sized,
18        {
19            #[inline]
20            fn serialize_as<S>(source: &Timestamp, serializer: S) -> Result<S::Ok, S::Error>
21            where
22                S: Serializer,
23            {
24                let timestamp: $int = source.$as();
25                <F as SerializeAs<$int>>::serialize_as(&timestamp, serializer)
26            }
27        }
28
29        impl<'de, F> DeserializeAs<'de, Timestamp> for $name<F>
30        where
31            F: DeserializeAs<'de, $int> + ?Sized,
32        {
33            #[inline]
34            fn deserialize_as<D>(deserializer: D) -> Result<Timestamp, D::Error>
35            where
36                D: Deserializer<'de>,
37            {
38                let timestamp = <F as DeserializeAs<'de, $int>>::deserialize_as(deserializer)?;
39                Timestamp::$from(timestamp).ok_or(Overflow).map_err( de::Error::custom)
40            }
41        }
42
43        #[cfg(feature = "schemars-v0_8")]
44        const _: () = {
45            use schemars::{SchemaGenerator, schema::Schema};
46            use serde_with::schemars_0_8::JsonSchemaAs;
47
48            impl<F> JsonSchemaAs<Timestamp> for $name<F>
49            where
50                F: JsonSchemaAs<$int> + ?Sized,
51            {
52                #[inline]
53                fn is_referenceable() -> bool {
54                    false
55                }
56
57                #[inline]
58                fn schema_name() -> String {
59                    stringify!($name<F>).into()
60                }
61
62                #[inline]
63                fn json_schema(generator: &mut SchemaGenerator) -> Schema {
64                    <F as JsonSchemaAs<$int>>::json_schema(generator)
65                }
66            }
67        };
68    )*};
69}
70
71serde_as! {
72    pub struct TimestampSeconds: i64 {
73        as_secs,
74        from_secs,
75    }
76
77    pub struct TimestampMilliSeconds: i64 {
78        as_millis,
79        from_millis,
80    }
81
82    pub struct TimestampMicroSeconds: i128 {
83        as_micros,
84        from_micros,
85    }
86
87    pub struct TimestampNanoSeconds: i128 {
88        as_nanos,
89        from_nanos,
90    }
91}
92
93#[cfg(test)]
94#[allow(clippy::inconsistent_digit_grouping)]
95mod tests {
96    use std::fmt::Debug;
97
98    use rstest::rstest;
99
100    use serde_json::json;
101    use serde_with::{DisplayFromStr, de::DeserializeAsWrap, ser::SerializeAsWrap};
102
103    use super::*;
104
105    #[rstest]
106    fn timestamp_secs_roundtrip(#[values(0, 1782395622, -1782395622)] secs: i64) {
107        let ts = Timestamp::from_secs(secs).unwrap();
108        roundtrip_as::<_, TimestampSeconds>(&ts);
109        roundtrip_as::<_, TimestampSeconds<DisplayFromStr>>(&ts);
110    }
111
112    #[rstest]
113    fn timestamp_millis_roundtrip(#[values(0, 1782395622_123, -1782395622_123)] millis: i64) {
114        let ts = Timestamp::from_millis(millis).unwrap();
115        roundtrip_as::<_, TimestampMilliSeconds>(&ts);
116        roundtrip_as::<_, TimestampMilliSeconds<DisplayFromStr>>(&ts);
117    }
118
119    #[rstest]
120    fn timestamp_micros_roundtrip(
121        #[values(0, 1782395622_123456, -1782395622_123456)] micros: i128,
122    ) {
123        let ts = Timestamp::from_micros(micros).unwrap();
124        roundtrip_as::<_, TimestampMicroSeconds>(&ts);
125        roundtrip_as::<_, TimestampMicroSeconds<DisplayFromStr>>(&ts);
126    }
127
128    #[rstest]
129    fn timestamp_nanos_roundtrip(
130        #[values(0, 1782395622_123456789, -1782395622_123456789)] nanos: i128,
131    ) {
132        let ts = Timestamp::from_nanos(nanos).unwrap();
133        roundtrip_as::<_, TimestampNanoSeconds>(&ts);
134        roundtrip_as::<_, TimestampNanoSeconds<DisplayFromStr>>(&ts);
135    }
136
137    // Helper roundtrip
138    #[track_caller]
139    fn roundtrip_as<T, As>(orig: &T)
140    where
141        for<'de> As: SerializeAs<T> + DeserializeAs<'de, T>,
142        T: PartialEq + Debug,
143    {
144        let serialized =
145            serde_json::to_string(&SerializeAsWrap::<T, As>::new(orig)).expect("JSON: serialize");
146        let deserialized: T = serde_json::from_str::<DeserializeAsWrap<T, As>>(&serialized)
147            .expect("JSON: deserialize")
148            .into_inner();
149        assert_eq!(
150            &deserialized, orig,
151            "deserialized value differs from the original one"
152        );
153    }
154
155    #[rstest]
156    #[case(json!("1970-01-01T00:00:00Z"), None)]
157    #[case(json!("1970-01-01T00:00:00+00:00"), json!("1970-01-01T00:00:00Z"))]
158    #[case(json!("1970-01-01T10:00:00+04:00"), json!("1970-01-01T06:00:00Z"))]
159    #[case(json!("1970-01-01T10:00:00-02:30"), json!("1970-01-01T12:30:00Z"))]
160    #[case(json!("2026-06-25T13:53:42.123456789Z"), None)]
161    #[case(
162        json!("2026-06-25T13:53:42.123456789+00:00"),
163        json!("2026-06-25T13:53:42.123456789Z"),
164    )]
165    #[case(
166        json!("2026-06-25T13:53:42.123456789+02:30"),
167        json!("2026-06-25T11:23:42.123456789Z"),
168    )]
169    #[case(
170        json!("2026-06-25T13:53:42.123456789-05:00"),
171        json!("2026-06-25T18:53:42.123456789Z"),
172    )]
173    fn semi_roundtrip(
174        #[case] orig: serde_json::Value,
175        #[case] expected: impl Into<Option<serde_json::Value>>,
176    ) {
177        let ts: Timestamp = serde_json::from_value(orig.clone()).expect("JSON: deserialize");
178        let got = serde_json::to_value(ts).expect("JSON: serialize");
179        assert_eq!(got, expected.into().unwrap_or(orig));
180    }
181}