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