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
54#[inline]
55pub fn serialize<S: Serializer>(ft: &Option<FileTime>, serializer: S) -> Result<S::Ok, S::Error> {
56    ft.map(FileTime::to_unix_time_secs).serialize(serializer)
57}
58
59#[allow(clippy::missing_errors_doc)]
60/// Deserializes an [`Option<FileTime>`] from the given Serde deserializer.
61///
62/// This deserializes from its [Unix time] in seconds.
63///
64/// [Unix time]: https://en.wikipedia.org/wiki/Unix_time
65#[inline]
66pub fn deserialize<'de, D: Deserializer<'de>>(
67    deserializer: D,
68) -> Result<Option<FileTime>, D::Error> {
69    Option::deserialize(deserializer)?
70        .map(FileTime::from_unix_time_secs)
71        .transpose()
72        .map_err(D::Error::custom)
73}
74
75#[cfg(test)]
76mod tests {
77    use core::time::Duration;
78
79    use serde_test::{
80        Token, assert_de_tokens, assert_de_tokens_error, assert_ser_tokens, assert_tokens,
81    };
82
83    use super::*;
84
85    #[derive(Debug, Deserialize, Eq, PartialEq, Serialize)]
86    struct Test {
87        #[serde(with = "crate::serde_with::unix_time::option")]
88        time: Option<FileTime>,
89    }
90
91    #[test]
92    fn serde() {
93        assert_tokens(
94            &Test {
95                time: Some(FileTime::NT_TIME_EPOCH),
96            },
97            &[
98                Token::Struct {
99                    name: "Test",
100                    len: 1,
101                },
102                Token::Str("time"),
103                Token::Some,
104                Token::I64(-11_644_473_600),
105                Token::StructEnd,
106            ],
107        );
108        assert_tokens(
109            &Test {
110                time: Some(FileTime::UNIX_EPOCH),
111            },
112            &[
113                Token::Struct {
114                    name: "Test",
115                    len: 1,
116                },
117                Token::Str("time"),
118                Token::Some,
119                Token::I64(i64::default()),
120                Token::StructEnd,
121            ],
122        );
123        assert_tokens(
124            &Test { time: None },
125            &[
126                Token::Struct {
127                    name: "Test",
128                    len: 1,
129                },
130                Token::Str("time"),
131                Token::None,
132                Token::StructEnd,
133            ],
134        );
135    }
136
137    #[test]
138    fn serialize() {
139        assert_ser_tokens(
140            &Test {
141                time: Some(FileTime::MAX),
142            },
143            &[
144                Token::Struct {
145                    name: "Test",
146                    len: 1,
147                },
148                Token::Str("time"),
149                Token::Some,
150                Token::I64(1_833_029_933_770),
151                Token::StructEnd,
152            ],
153        );
154    }
155
156    #[test]
157    fn deserialize() {
158        assert_de_tokens(
159            &Test {
160                time: Some(FileTime::MAX - Duration::from_nanos(955_161_500)),
161            },
162            &[
163                Token::Struct {
164                    name: "Test",
165                    len: 1,
166                },
167                Token::Str("time"),
168                Token::Some,
169                Token::I64(1_833_029_933_770),
170                Token::StructEnd,
171            ],
172        );
173    }
174
175    #[test]
176    fn deserialize_error() {
177        assert_de_tokens_error::<Test>(
178            &[
179                Token::Struct {
180                    name: "Test",
181                    len: 1,
182                },
183                Token::Str("time"),
184                Token::Some,
185                Token::I64(-11_644_473_601),
186                Token::StructEnd,
187            ],
188            "date and time is before `1601-01-01 00:00:00 UTC`",
189        );
190        assert_de_tokens_error::<Test>(
191            &[
192                Token::Struct {
193                    name: "Test",
194                    len: 1,
195                },
196                Token::Str("time"),
197                Token::Some,
198                Token::I64(1_833_029_933_771),
199                Token::StructEnd,
200            ],
201            "date and time is after `+60056-05-28 05:36:10.955161500 UTC`",
202        );
203    }
204
205    #[test]
206    fn serialize_json() {
207        assert_eq!(
208            serde_json::to_string(&Test {
209                time: Some(FileTime::NT_TIME_EPOCH)
210            })
211            .unwrap(),
212            r#"{"time":-11644473600}"#
213        );
214        assert_eq!(
215            serde_json::to_string(&Test {
216                time: Some(FileTime::UNIX_EPOCH)
217            })
218            .unwrap(),
219            r#"{"time":0}"#
220        );
221        assert_eq!(
222            serde_json::to_string(&Test {
223                time: Some(FileTime::MAX)
224            })
225            .unwrap(),
226            r#"{"time":1833029933770}"#
227        );
228        assert_eq!(
229            serde_json::to_string(&Test { time: None }).unwrap(),
230            r#"{"time":null}"#
231        );
232    }
233
234    #[cfg(feature = "std")]
235    #[test_strategy::proptest]
236    fn serialize_json_roundtrip(timestamp: Option<i64>) {
237        use proptest::{prop_assert_eq, prop_assume};
238
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    #[test_strategy::proptest]
285    fn deserialize_json_roundtrip(timestamp: Option<i64>) {
286        use std::string::String;
287
288        use proptest::{prop_assert_eq, prop_assume};
289
290        if let Some(ts) = timestamp {
291            prop_assume!((-11_644_473_600..=1_833_029_933_770).contains(&ts));
292        }
293
294        let json = if let Some(ts) = timestamp {
295            format!(r#"{{"time":{ts}}}"#)
296        } else {
297            String::from(r#"{"time":null}"#)
298        };
299        let ft = serde_json::from_str::<Test>(&json).unwrap();
300        prop_assert_eq!(
301            ft.time,
302            timestamp
303                .map(FileTime::from_unix_time_secs)
304                .transpose()
305                .unwrap()
306        );
307    }
308}