nt_time/serde_with/unix_time/milliseconds/
option.rs

1// SPDX-FileCopyrightText: 2024 Shun Sakai
2//
3// SPDX-License-Identifier: Apache-2.0 OR MIT
4
5//! Use [Unix time] in milliseconds when serializing and deserializing an
6//! [`Option<FileTime>`].
7//!
8//! Use this module in combination with Serde's [`with`] attribute.
9//!
10//! # Examples
11//!
12//! ```
13//! use nt_time::{
14//!     FileTime,
15//!     serde::{Deserialize, Serialize},
16//!     serde_with::unix_time,
17//! };
18//!
19//! #[derive(Deserialize, Serialize)]
20//! struct Time {
21//!     #[serde(with = "unix_time::milliseconds::option")]
22//!     time: Option<FileTime>,
23//! }
24//!
25//! let ft = Time {
26//!     time: Some(FileTime::NT_TIME_EPOCH),
27//! };
28//! let json = serde_json::to_string(&ft).unwrap();
29//! assert_eq!(json, r#"{"time":-11644473600000}"#);
30//!
31//! let ft: Time = serde_json::from_str(&json).unwrap();
32//! assert_eq!(ft.time, Some(FileTime::NT_TIME_EPOCH));
33//!
34//! let ft = Time { time: None };
35//! let json = serde_json::to_string(&ft).unwrap();
36//! assert_eq!(json, r#"{"time":null}"#);
37//!
38//! let ft: Time = serde_json::from_str(&json).unwrap();
39//! assert_eq!(ft.time, None);
40//! ```
41//!
42//! [Unix time]: https://en.wikipedia.org/wiki/Unix_time
43//! [`with`]: https://serde.rs/field-attrs.html#with
44
45use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error};
46
47use crate::FileTime;
48
49#[allow(clippy::missing_errors_doc)]
50/// Serializes an [`Option<FileTime>`] into the given Serde serializer.
51///
52/// This serializes using [Unix time] in milliseconds.
53///
54/// [Unix time]: https://en.wikipedia.org/wiki/Unix_time
55#[inline]
56pub fn serialize<S: Serializer>(ft: &Option<FileTime>, serializer: S) -> Result<S::Ok, S::Error> {
57    ft.map(FileTime::to_unix_time_millis).serialize(serializer)
58}
59
60#[allow(clippy::missing_errors_doc)]
61/// Deserializes an [`Option<FileTime>`] from the given Serde deserializer.
62///
63/// This deserializes from its [Unix time] in milliseconds.
64///
65/// [Unix time]: https://en.wikipedia.org/wiki/Unix_time
66#[inline]
67pub fn deserialize<'de, D: Deserializer<'de>>(
68    deserializer: D,
69) -> Result<Option<FileTime>, D::Error> {
70    Option::deserialize(deserializer)?
71        .map(FileTime::from_unix_time_millis)
72        .transpose()
73        .map_err(D::Error::custom)
74}
75
76#[cfg(test)]
77mod tests {
78    use core::time::Duration;
79
80    use serde_test::{
81        Token, assert_de_tokens, assert_de_tokens_error, assert_ser_tokens, assert_tokens,
82    };
83
84    use super::*;
85
86    #[derive(Debug, Deserialize, Eq, PartialEq, Serialize)]
87    struct Test {
88        #[serde(with = "crate::serde_with::unix_time::milliseconds::option")]
89        time: Option<FileTime>,
90    }
91
92    #[test]
93    fn serde() {
94        assert_tokens(
95            &Test {
96                time: Some(FileTime::NT_TIME_EPOCH),
97            },
98            &[
99                Token::Struct {
100                    name: "Test",
101                    len: 1,
102                },
103                Token::Str("time"),
104                Token::Some,
105                Token::I64(-11_644_473_600_000),
106                Token::StructEnd,
107            ],
108        );
109        assert_tokens(
110            &Test {
111                time: Some(FileTime::UNIX_EPOCH),
112            },
113            &[
114                Token::Struct {
115                    name: "Test",
116                    len: 1,
117                },
118                Token::Str("time"),
119                Token::Some,
120                Token::I64(i64::default()),
121                Token::StructEnd,
122            ],
123        );
124        assert_tokens(
125            &Test { time: None },
126            &[
127                Token::Struct {
128                    name: "Test",
129                    len: 1,
130                },
131                Token::Str("time"),
132                Token::None,
133                Token::StructEnd,
134            ],
135        );
136    }
137
138    #[test]
139    fn serialize() {
140        assert_ser_tokens(
141            &Test {
142                time: Some(FileTime::MAX),
143            },
144            &[
145                Token::Struct {
146                    name: "Test",
147                    len: 1,
148                },
149                Token::Str("time"),
150                Token::Some,
151                Token::I64(1_833_029_933_770_955),
152                Token::StructEnd,
153            ],
154        );
155    }
156
157    #[test]
158    fn deserialize() {
159        assert_de_tokens(
160            &Test {
161                time: Some(FileTime::MAX - Duration::from_nanos(161_500)),
162            },
163            &[
164                Token::Struct {
165                    name: "Test",
166                    len: 1,
167                },
168                Token::Str("time"),
169                Token::Some,
170                Token::I64(1_833_029_933_770_955),
171                Token::StructEnd,
172            ],
173        );
174    }
175
176    #[test]
177    fn deserialize_error() {
178        assert_de_tokens_error::<Test>(
179            &[
180                Token::Struct {
181                    name: "Test",
182                    len: 1,
183                },
184                Token::Str("time"),
185                Token::Some,
186                Token::I64(-11_644_473_600_001),
187                Token::StructEnd,
188            ],
189            "date and time is before `1601-01-01 00:00:00 UTC`",
190        );
191        assert_de_tokens_error::<Test>(
192            &[
193                Token::Struct {
194                    name: "Test",
195                    len: 1,
196                },
197                Token::Str("time"),
198                Token::Some,
199                Token::I64(1_833_029_933_770_956),
200                Token::StructEnd,
201            ],
202            "date and time is after `+60056-05-28 05:36:10.955161500 UTC`",
203        );
204    }
205
206    #[test]
207    fn serialize_json() {
208        assert_eq!(
209            serde_json::to_string(&Test {
210                time: Some(FileTime::NT_TIME_EPOCH)
211            })
212            .unwrap(),
213            r#"{"time":-11644473600000}"#
214        );
215        assert_eq!(
216            serde_json::to_string(&Test {
217                time: Some(FileTime::UNIX_EPOCH)
218            })
219            .unwrap(),
220            r#"{"time":0}"#
221        );
222        assert_eq!(
223            serde_json::to_string(&Test {
224                time: Some(FileTime::MAX)
225            })
226            .unwrap(),
227            r#"{"time":1833029933770955}"#
228        );
229        assert_eq!(
230            serde_json::to_string(&Test { time: None }).unwrap(),
231            r#"{"time":null}"#
232        );
233    }
234
235    #[cfg(feature = "std")]
236    #[test_strategy::proptest]
237    fn serialize_json_roundtrip(timestamp: Option<i64>) {
238        use proptest::{prop_assert_eq, prop_assume};
239
240        if let Some(ts) = timestamp {
241            prop_assume!((-11_644_473_600_000..=1_833_029_933_770_955).contains(&ts));
242        }
243
244        let ft = Test {
245            time: timestamp
246                .map(FileTime::from_unix_time_millis)
247                .transpose()
248                .unwrap(),
249        };
250        let json = serde_json::to_string(&ft).unwrap();
251        if let Some(ts) = timestamp {
252            prop_assert_eq!(json, format!(r#"{{"time":{ts}}}"#));
253        } else {
254            prop_assert_eq!(json, r#"{"time":null}"#);
255        }
256    }
257
258    #[test]
259    fn deserialize_json() {
260        assert_eq!(
261            serde_json::from_str::<Test>(r#"{"time":-11644473600000}"#).unwrap(),
262            Test {
263                time: Some(FileTime::NT_TIME_EPOCH)
264            }
265        );
266        assert_eq!(
267            serde_json::from_str::<Test>(r#"{"time":0}"#).unwrap(),
268            Test {
269                time: Some(FileTime::UNIX_EPOCH)
270            }
271        );
272        assert_eq!(
273            serde_json::from_str::<Test>(r#"{"time":1833029933770955}"#).unwrap(),
274            Test {
275                time: Some(FileTime::MAX - Duration::from_nanos(161_500))
276            }
277        );
278        assert_eq!(
279            serde_json::from_str::<Test>(r#"{"time":null}"#).unwrap(),
280            Test { time: None }
281        );
282    }
283
284    #[cfg(feature = "std")]
285    #[test_strategy::proptest]
286    fn deserialize_json_roundtrip(timestamp: Option<i64>) {
287        use std::string::String;
288
289        use proptest::{prop_assert_eq, prop_assume};
290
291        if let Some(ts) = timestamp {
292            prop_assume!((-11_644_473_600_000..=1_833_029_933_770_955).contains(&ts));
293        }
294
295        let json = if let Some(ts) = timestamp {
296            format!(r#"{{"time":{ts}}}"#)
297        } else {
298            String::from(r#"{"time":null}"#)
299        };
300        let ft = serde_json::from_str::<Test>(&json).unwrap();
301        prop_assert_eq!(
302            ft.time,
303            timestamp
304                .map(FileTime::from_unix_time_millis)
305                .transpose()
306                .unwrap()
307        );
308    }
309}