nt_time/serde_with/unix_time/
option.rs

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