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