nt_time/serde_with/iso_8601/
option.rs

1// SPDX-FileCopyrightText: 2023 Shun Sakai
2//
3// SPDX-License-Identifier: Apache-2.0 OR MIT
4
5//! Use the well-known [ISO 8601 format] when serializing and deserializing an
6//! [`Option<FileTime>`].
7//!
8//! Use this module in combination with Serde's [`with`] attribute.
9//!
10//! <div class="warning">
11//!
12//! If the `large-dates` feature is not enabled, the largest date and time is
13//! "9999-12-31 23:59:59.999999999 UTC".
14//!
15//! </div>
16//!
17//! # Examples
18//!
19//! ```
20//! use nt_time::{
21//!     FileTime,
22//!     serde::{Deserialize, Serialize},
23//!     serde_with::iso_8601,
24//! };
25//!
26//! #[derive(Deserialize, Serialize)]
27//! struct Time {
28//!     #[serde(with = "iso_8601::option")]
29//!     time: Option<FileTime>,
30//! }
31//!
32//! let ft = Time {
33//!     time: Some(FileTime::UNIX_EPOCH),
34//! };
35//! let json = serde_json::to_string(&ft).unwrap();
36//! assert_eq!(json, r#"{"time":"+001970-01-01T00:00:00.000000000Z"}"#);
37//!
38//! let ft: Time = serde_json::from_str(&json).unwrap();
39//! assert_eq!(ft.time, Some(FileTime::UNIX_EPOCH));
40//!
41//! let ft = Time { time: None };
42//! let json = serde_json::to_string(&ft).unwrap();
43//! assert_eq!(json, r#"{"time":null}"#);
44//!
45//! let ft: Time = serde_json::from_str(&json).unwrap();
46//! assert_eq!(ft.time, None);
47//! ```
48//!
49//! [ISO 8601 format]: https://www.iso.org/iso-8601-date-and-time-format.html
50//! [`with`]: https://serde.rs/field-attrs.html#with
51
52use serde::{Deserializer, Serializer, de::Error as _, ser::Error as _};
53use time::{OffsetDateTime, serde::iso8601};
54
55use crate::FileTime;
56
57#[allow(clippy::missing_errors_doc)]
58/// Serializes an [`Option<FileTime>`] into the given Serde serializer.
59///
60/// This serializes using the well-known [ISO 8601 format].
61///
62/// [ISO 8601 format]: https://www.iso.org/iso-8601-date-and-time-format.html
63#[inline]
64pub fn serialize<S: Serializer>(ft: &Option<FileTime>, serializer: S) -> Result<S::Ok, S::Error> {
65    iso8601::option::serialize(
66        &(*ft)
67            .map(OffsetDateTime::try_from)
68            .transpose()
69            .map_err(S::Error::custom)?,
70        serializer,
71    )
72}
73
74#[allow(clippy::missing_errors_doc)]
75/// Deserializes an [`Option<FileTime>`] from the given Serde deserializer.
76///
77/// This deserializes from its [ISO 8601 representation].
78///
79/// [ISO 8601 representation]: https://www.iso.org/iso-8601-date-and-time-format.html
80#[inline]
81pub fn deserialize<'de, D: Deserializer<'de>>(
82    deserializer: D,
83) -> Result<Option<FileTime>, D::Error> {
84    iso8601::option::deserialize(deserializer)?
85        .map(FileTime::try_from)
86        .transpose()
87        .map_err(D::Error::custom)
88}
89
90#[cfg(test)]
91mod tests {
92    use serde::{Deserialize, Serialize};
93    use serde_test::{Token, assert_de_tokens_error, assert_tokens};
94
95    use super::*;
96
97    #[derive(Debug, Deserialize, Eq, PartialEq, Serialize)]
98    struct Test {
99        #[serde(with = "crate::serde_with::iso_8601::option")]
100        time: Option<FileTime>,
101    }
102
103    #[test]
104    fn serde() {
105        assert_tokens(
106            &Test {
107                time: Some(FileTime::NT_TIME_EPOCH),
108            },
109            &[
110                Token::Struct {
111                    name: "Test",
112                    len: 1,
113                },
114                Token::Str("time"),
115                Token::Some,
116                Token::BorrowedStr("+001601-01-01T00:00:00.000000000Z"),
117                Token::StructEnd,
118            ],
119        );
120        assert_tokens(
121            &Test {
122                time: Some(FileTime::UNIX_EPOCH),
123            },
124            &[
125                Token::Struct {
126                    name: "Test",
127                    len: 1,
128                },
129                Token::Str("time"),
130                Token::Some,
131                Token::BorrowedStr("+001970-01-01T00:00:00.000000000Z"),
132                Token::StructEnd,
133            ],
134        );
135        assert_tokens(
136            &Test { time: None },
137            &[
138                Token::Struct {
139                    name: "Test",
140                    len: 1,
141                },
142                Token::Str("time"),
143                Token::None,
144                Token::StructEnd,
145            ],
146        );
147    }
148
149    #[cfg(feature = "large-dates")]
150    #[test]
151    fn serde_with_large_dates() {
152        assert_tokens(
153            &Test {
154                time: Some(FileTime::MAX),
155            },
156            &[
157                Token::Struct {
158                    name: "Test",
159                    len: 1,
160                },
161                Token::Str("time"),
162                Token::Some,
163                Token::BorrowedStr("+060056-05-28T05:36:10.955161500Z"),
164                Token::StructEnd,
165            ],
166        );
167    }
168
169    #[test]
170    fn deserialize_error() {
171        assert_de_tokens_error::<Test>(
172            &[
173                Token::Struct {
174                    name: "Test",
175                    len: 1,
176                },
177                Token::Str("time"),
178                Token::Some,
179                Token::BorrowedStr("+001600-12-31T23:59:59.999999900Z"),
180                Token::StructEnd,
181            ],
182            "date and time is before `1601-01-01 00:00:00 UTC`",
183        );
184    }
185
186    #[cfg(not(feature = "large-dates"))]
187    #[test]
188    fn deserialize_error_without_large_dates() {
189        assert_de_tokens_error::<Test>(
190            &[
191                Token::Struct {
192                    name: "Test",
193                    len: 1,
194                },
195                Token::Str("time"),
196                Token::Some,
197                Token::BorrowedStr("+010000-01-01T00:00:00.000000000Z"),
198                Token::StructEnd,
199            ],
200            "unexpected trailing characters; the end of input was expected",
201        );
202    }
203
204    #[cfg(feature = "large-dates")]
205    #[test]
206    fn deserialize_error_with_large_dates() {
207        assert_de_tokens_error::<Test>(
208            &[
209                Token::Struct {
210                    name: "Test",
211                    len: 1,
212                },
213                Token::Str("time"),
214                Token::Some,
215                Token::BorrowedStr("+060056-05-28T05:36:10.955161600Z"),
216                Token::StructEnd,
217            ],
218            "date and time is after `+60056-05-28 05:36:10.955161500 UTC`",
219        );
220    }
221
222    #[test]
223    fn serialize_json() {
224        assert_eq!(
225            serde_json::to_string(&Test {
226                time: Some(FileTime::NT_TIME_EPOCH)
227            })
228            .unwrap(),
229            r#"{"time":"+001601-01-01T00:00:00.000000000Z"}"#
230        );
231        assert_eq!(
232            serde_json::to_string(&Test {
233                time: Some(FileTime::UNIX_EPOCH)
234            })
235            .unwrap(),
236            r#"{"time":"+001970-01-01T00:00:00.000000000Z"}"#
237        );
238        assert_eq!(
239            serde_json::to_string(&Test { time: None }).unwrap(),
240            r#"{"time":null}"#
241        );
242    }
243
244    #[cfg(feature = "large-dates")]
245    #[test]
246    fn serialize_json_with_large_dates() {
247        assert_eq!(
248            serde_json::to_string(&Test {
249                time: Some(FileTime::MAX)
250            })
251            .unwrap(),
252            r#"{"time":"+060056-05-28T05:36:10.955161500Z"}"#
253        );
254    }
255
256    #[test]
257    fn deserialize_json() {
258        assert_eq!(
259            serde_json::from_str::<Test>(r#"{"time":"1601-01-01T00:00:00Z"}"#).unwrap(),
260            Test {
261                time: Some(FileTime::NT_TIME_EPOCH)
262            }
263        );
264        assert_eq!(
265            serde_json::from_str::<Test>(r#"{"time":"1970-01-01T00:00:00Z"}"#).unwrap(),
266            Test {
267                time: Some(FileTime::UNIX_EPOCH)
268            }
269        );
270        assert_eq!(
271            serde_json::from_str::<Test>(r#"{"time":null}"#).unwrap(),
272            Test { time: None }
273        );
274    }
275
276    #[cfg(feature = "large-dates")]
277    #[test]
278    fn deserialize_json_with_large_dates() {
279        assert_eq!(
280            serde_json::from_str::<Test>(r#"{"time":"+060056-05-28T05:36:10.955161500Z"}"#)
281                .unwrap(),
282            Test {
283                time: Some(FileTime::MAX)
284            }
285        );
286    }
287}