nt_time/serde_with/rfc_3339/
option.rs

1// SPDX-FileCopyrightText: 2023 Shun Sakai
2//
3// SPDX-License-Identifier: Apache-2.0 OR MIT
4
5//! Use the well-known [RFC 3339 format] when serializing and deserializing an
6//! [`Option<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::rfc_3339,
17//! };
18//!
19//! #[derive(Deserialize, Serialize)]
20//! struct Time {
21//!     #[serde(with = "rfc_3339::option")]
22//!     time: Option<FileTime>,
23//! }
24//!
25//! let ft = Time {
26//!     time: Some(FileTime::UNIX_EPOCH),
27//! };
28//! let json = serde_json::to_string(&ft).unwrap();
29//! assert_eq!(json, r#"{"time":"1970-01-01T00:00:00Z"}"#);
30//!
31//! let ft: Time = serde_json::from_str(&json).unwrap();
32//! assert_eq!(ft.time, Some(FileTime::UNIX_EPOCH));
33//!
34//! let ft = Time { time: None };
35//! let json = serde_json::to_string(&ft).unwrap();
36//! assert_eq!(json, r#"{"time":null}"#);
37//!
38//! let ft: Time = serde_json::from_str(&json).unwrap();
39//! assert_eq!(ft.time, None);
40//! ```
41//!
42//! [RFC 3339 format]: https://datatracker.ietf.org/doc/html/rfc3339#section-5.6
43//! [`with`]: https://serde.rs/field-attrs.html#with
44
45use serde::{Deserializer, Serializer, de::Error as _, ser::Error as _};
46use time::{OffsetDateTime, serde::rfc3339};
47
48use crate::FileTime;
49
50#[allow(clippy::missing_errors_doc)]
51/// Serializes an [`Option<FileTime>`] into the given Serde serializer.
52///
53/// This serializes using the well-known [RFC 3339 format].
54///
55/// [RFC 3339 format]: https://datatracker.ietf.org/doc/html/rfc3339#section-5.6
56#[inline]
57pub fn serialize<S: Serializer>(ft: &Option<FileTime>, serializer: S) -> Result<S::Ok, S::Error> {
58    rfc3339::option::serialize(
59        &(*ft)
60            .map(OffsetDateTime::try_from)
61            .transpose()
62            .map_err(S::Error::custom)?,
63        serializer,
64    )
65}
66
67#[allow(clippy::missing_errors_doc)]
68/// Deserializes an [`Option<FileTime>`] from the given Serde deserializer.
69///
70/// This deserializes from its [RFC 3339 representation].
71///
72/// [RFC 3339 representation]: https://datatracker.ietf.org/doc/html/rfc3339#section-5.6
73#[inline]
74pub fn deserialize<'de, D: Deserializer<'de>>(
75    deserializer: D,
76) -> Result<Option<FileTime>, D::Error> {
77    rfc3339::option::deserialize(deserializer)?
78        .map(FileTime::try_from)
79        .transpose()
80        .map_err(D::Error::custom)
81}
82
83#[cfg(test)]
84mod tests {
85    use serde::{Deserialize, Serialize};
86    use serde_test::{Token, assert_de_tokens_error, assert_ser_tokens_error, assert_tokens};
87
88    use super::*;
89
90    #[derive(Debug, Deserialize, Eq, PartialEq, Serialize)]
91    struct Test {
92        #[serde(with = "crate::serde_with::rfc_3339::option")]
93        time: Option<FileTime>,
94    }
95
96    #[test]
97    fn serde() {
98        assert_tokens(
99            &Test {
100                time: Some(FileTime::NT_TIME_EPOCH),
101            },
102            &[
103                Token::Struct {
104                    name: "Test",
105                    len: 1,
106                },
107                Token::Str("time"),
108                Token::Some,
109                Token::BorrowedStr("1601-01-01T00:00:00Z"),
110                Token::StructEnd,
111            ],
112        );
113        assert_tokens(
114            &Test {
115                time: Some(FileTime::UNIX_EPOCH),
116            },
117            &[
118                Token::Struct {
119                    name: "Test",
120                    len: 1,
121                },
122                Token::Str("time"),
123                Token::Some,
124                Token::BorrowedStr("1970-01-01T00:00:00Z"),
125                Token::StructEnd,
126            ],
127        );
128        assert_tokens(
129            &Test { time: None },
130            &[
131                Token::Struct {
132                    name: "Test",
133                    len: 1,
134                },
135                Token::Str("time"),
136                Token::None,
137                Token::StructEnd,
138            ],
139        );
140    }
141
142    #[cfg(not(feature = "large-dates"))]
143    #[test]
144    fn serialize_error_without_large_dates() {
145        assert_ser_tokens_error::<Test>(
146            &Test {
147                time: Some(FileTime::MAX),
148            },
149            &[
150                Token::Struct {
151                    name: "Test",
152                    len: 1,
153                },
154                Token::Str("time"),
155            ],
156            "timestamp must be in the range -377705116800..=253402300799",
157        );
158    }
159
160    #[cfg(feature = "large-dates")]
161    #[test]
162    fn serialize_error_with_large_dates() {
163        assert_ser_tokens_error::<Test>(
164            &Test {
165                time: Some(FileTime::MAX),
166            },
167            &[
168                Token::Struct {
169                    name: "Test",
170                    len: 1,
171                },
172                Token::Str("time"),
173            ],
174            "The year component cannot be formatted into the requested format.",
175        );
176    }
177
178    #[test]
179    fn deserialize_error() {
180        assert_de_tokens_error::<Test>(
181            &[
182                Token::Struct {
183                    name: "Test",
184                    len: 1,
185                },
186                Token::Str("time"),
187                Token::Some,
188                Token::BorrowedStr("1600-12-31T23:59:59.999999900Z"),
189                Token::StructEnd,
190            ],
191            "date and time is before `1601-01-01 00:00:00 UTC`",
192        );
193    }
194
195    #[test]
196    fn serialize_json() {
197        assert_eq!(
198            serde_json::to_string(&Test {
199                time: Some(FileTime::NT_TIME_EPOCH)
200            })
201            .unwrap(),
202            r#"{"time":"1601-01-01T00:00:00Z"}"#
203        );
204        assert_eq!(
205            serde_json::to_string(&Test {
206                time: Some(FileTime::UNIX_EPOCH)
207            })
208            .unwrap(),
209            r#"{"time":"1970-01-01T00:00:00Z"}"#
210        );
211        assert_eq!(
212            serde_json::to_string(&Test { time: None }).unwrap(),
213            r#"{"time":null}"#
214        );
215    }
216
217    #[test]
218    fn deserialize_json() {
219        assert_eq!(
220            serde_json::from_str::<Test>(r#"{"time":"1601-01-01T00:00:00Z"}"#).unwrap(),
221            Test {
222                time: Some(FileTime::NT_TIME_EPOCH)
223            }
224        );
225        assert_eq!(
226            serde_json::from_str::<Test>(r#"{"time":"1970-01-01T00:00:00Z"}"#).unwrap(),
227            Test {
228                time: Some(FileTime::UNIX_EPOCH)
229            }
230        );
231        assert_eq!(
232            serde_json::from_str::<Test>(r#"{"time":null}"#).unwrap(),
233            Test { time: None }
234        );
235    }
236}