nt_time/serde_with/
unix_time.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 a [`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")]
21//!     time: FileTime,
22//! }
23//!
24//! let ft = Time {
25//!     time: 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, FileTime::NT_TIME_EPOCH);
32//! ```
33//!
34//! [Unix time]: https://en.wikipedia.org/wiki/Unix_time
35//! [`with`]: https://serde.rs/field-attrs.html#with
36
37pub mod microseconds;
38pub mod milliseconds;
39pub mod nanoseconds;
40pub mod option;
41
42use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error};
43
44use crate::FileTime;
45
46#[allow(clippy::missing_errors_doc)]
47/// Serializes a [`FileTime`] into the given Serde serializer.
48///
49/// This serializes using [Unix time] in seconds.
50///
51/// [Unix time]: https://en.wikipedia.org/wiki/Unix_time
52#[inline]
53pub fn serialize<S: Serializer>(ft: &FileTime, serializer: S) -> Result<S::Ok, S::Error> {
54    ft.to_unix_time_secs().serialize(serializer)
55}
56
57#[allow(clippy::missing_errors_doc)]
58/// Deserializes a [`FileTime`] from the given Serde deserializer.
59///
60/// This deserializes from its [Unix time] in seconds.
61///
62/// [Unix time]: https://en.wikipedia.org/wiki/Unix_time
63#[inline]
64pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<FileTime, D::Error> {
65    FileTime::from_unix_time_secs(<_>::deserialize(deserializer)?).map_err(D::Error::custom)
66}
67
68#[cfg(test)]
69mod tests {
70    use core::time::Duration;
71
72    use serde_test::{
73        Token, assert_de_tokens, assert_de_tokens_error, assert_ser_tokens, assert_tokens,
74    };
75
76    use super::*;
77
78    #[derive(Debug, Deserialize, Eq, PartialEq, Serialize)]
79    struct Test {
80        #[serde(with = "crate::serde_with::unix_time")]
81        time: FileTime,
82    }
83
84    #[test]
85    fn serde() {
86        assert_tokens(
87            &Test {
88                time: FileTime::NT_TIME_EPOCH,
89            },
90            &[
91                Token::Struct {
92                    name: "Test",
93                    len: 1,
94                },
95                Token::Str("time"),
96                Token::I64(-11_644_473_600),
97                Token::StructEnd,
98            ],
99        );
100        assert_tokens(
101            &Test {
102                time: FileTime::UNIX_EPOCH,
103            },
104            &[
105                Token::Struct {
106                    name: "Test",
107                    len: 1,
108                },
109                Token::Str("time"),
110                Token::I64(i64::default()),
111                Token::StructEnd,
112            ],
113        );
114    }
115
116    #[test]
117    fn serialize() {
118        assert_ser_tokens(
119            &Test {
120                time: FileTime::MAX,
121            },
122            &[
123                Token::Struct {
124                    name: "Test",
125                    len: 1,
126                },
127                Token::Str("time"),
128                Token::I64(1_833_029_933_770),
129                Token::StructEnd,
130            ],
131        );
132    }
133
134    #[test]
135    fn deserialize() {
136        assert_de_tokens(
137            &Test {
138                time: FileTime::MAX - Duration::from_nanos(955_161_500),
139            },
140            &[
141                Token::Struct {
142                    name: "Test",
143                    len: 1,
144                },
145                Token::Str("time"),
146                Token::I64(1_833_029_933_770),
147                Token::StructEnd,
148            ],
149        );
150    }
151
152    #[test]
153    fn deserialize_error() {
154        assert_de_tokens_error::<Test>(
155            &[
156                Token::Struct {
157                    name: "Test",
158                    len: 1,
159                },
160                Token::Str("time"),
161                Token::I64(-11_644_473_601),
162                Token::StructEnd,
163            ],
164            "date and time is before `1601-01-01 00:00:00 UTC`",
165        );
166        assert_de_tokens_error::<Test>(
167            &[
168                Token::Struct {
169                    name: "Test",
170                    len: 1,
171                },
172                Token::Str("time"),
173                Token::I64(1_833_029_933_771),
174                Token::StructEnd,
175            ],
176            "date and time is after `+60056-05-28 05:36:10.955161500 UTC`",
177        );
178    }
179
180    #[test]
181    fn serialize_json() {
182        assert_eq!(
183            serde_json::to_string(&Test {
184                time: FileTime::NT_TIME_EPOCH
185            })
186            .unwrap(),
187            r#"{"time":-11644473600}"#
188        );
189        assert_eq!(
190            serde_json::to_string(&Test {
191                time: FileTime::UNIX_EPOCH
192            })
193            .unwrap(),
194            r#"{"time":0}"#
195        );
196        assert_eq!(
197            serde_json::to_string(&Test {
198                time: FileTime::MAX
199            })
200            .unwrap(),
201            r#"{"time":1833029933770}"#
202        );
203    }
204
205    #[cfg(feature = "std")]
206    #[test_strategy::proptest]
207    fn serialize_json_roundtrip(#[strategy(-11_644_473_600..=1_833_029_933_770_i64)] ts: i64) {
208        use proptest::prop_assert_eq;
209
210        let ft = Test {
211            time: FileTime::from_unix_time_secs(ts).unwrap(),
212        };
213        let json = serde_json::to_string(&ft).unwrap();
214        prop_assert_eq!(json, format!(r#"{{"time":{ts}}}"#));
215    }
216
217    #[test]
218    fn deserialize_json() {
219        assert_eq!(
220            serde_json::from_str::<Test>(r#"{"time":-11644473600}"#).unwrap(),
221            Test {
222                time: FileTime::NT_TIME_EPOCH
223            }
224        );
225        assert_eq!(
226            serde_json::from_str::<Test>(r#"{"time":0}"#).unwrap(),
227            Test {
228                time: FileTime::UNIX_EPOCH
229            }
230        );
231        assert_eq!(
232            serde_json::from_str::<Test>(r#"{"time":1833029933770}"#).unwrap(),
233            Test {
234                time: FileTime::MAX - Duration::from_nanos(955_161_500)
235            }
236        );
237    }
238
239    #[cfg(feature = "std")]
240    #[test_strategy::proptest]
241    fn deserialize_json_roundtrip(#[strategy(-11_644_473_600..=1_833_029_933_770_i64)] ts: i64) {
242        use proptest::prop_assert_eq;
243
244        let json = format!(r#"{{"time":{ts}}}"#);
245        let ft = serde_json::from_str::<Test>(&json).unwrap();
246        prop_assert_eq!(ft.time, FileTime::from_unix_time_secs(ts).unwrap());
247    }
248}