nt_time/serde_with/unix_time/milliseconds/
option.rs1use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error};
46
47use crate::FileTime;
48
49#[allow(clippy::missing_errors_doc)]
50#[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#[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}