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