nt_time/serde_with/rfc_2822/
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 2822 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_2822,
17//! };
18//!
19//! #[derive(Deserialize, Serialize)]
20//! struct Time {
21//!     #[serde(with = "rfc_2822::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":"Thu, 01 Jan 1970 00:00:00 +0000"}"#);
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 2822 format]: https://datatracker.ietf.org/doc/html/rfc2822#section-3.3
43//! [`with`]: https://serde.rs/field-attrs.html#with
44
45use serde::{Deserializer, Serializer, de::Error as _, ser::Error as _};
46use time::{OffsetDateTime, serde::rfc2822};
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 2822 format].
54///
55/// [RFC 2822 format]: https://datatracker.ietf.org/doc/html/rfc2822#section-3.3
56#[inline]
57pub fn serialize<S: Serializer>(ft: &Option<FileTime>, serializer: S) -> Result<S::Ok, S::Error> {
58    rfc2822::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 2822 representation].
71///
72/// [RFC 2822 representation]: https://datatracker.ietf.org/doc/html/rfc2822#section-3.3
73#[inline]
74pub fn deserialize<'de, D: Deserializer<'de>>(
75    deserializer: D,
76) -> Result<Option<FileTime>, D::Error> {
77    rfc2822::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_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_2822::option")]
93        time: Option<FileTime>,
94    }
95
96    #[test]
97    fn serde() {
98        assert_tokens(
99            &Test {
100                time: Some(FileTime::UNIX_EPOCH),
101            },
102            &[
103                Token::Struct {
104                    name: "Test",
105                    len: 1,
106                },
107                Token::Str("time"),
108                Token::Some,
109                Token::BorrowedStr("Thu, 01 Jan 1970 00:00:00 +0000"),
110                Token::StructEnd,
111            ],
112        );
113        assert_tokens(
114            &Test { time: None },
115            &[
116                Token::Struct {
117                    name: "Test",
118                    len: 1,
119                },
120                Token::Str("time"),
121                Token::None,
122                Token::StructEnd,
123            ],
124        );
125    }
126
127    #[test]
128    fn serialize_error() {
129        assert_ser_tokens_error::<Test>(
130            &Test {
131                time: Some(FileTime::NT_TIME_EPOCH),
132            },
133            &[
134                Token::Struct {
135                    name: "Test",
136                    len: 1,
137                },
138                Token::Str("time"),
139            ],
140            "The year component cannot be formatted into the requested format.",
141        );
142    }
143
144    #[test]
145    fn serialize_json() {
146        assert_eq!(
147            serde_json::to_string(&Test {
148                time: Some(FileTime::UNIX_EPOCH)
149            })
150            .unwrap(),
151            r#"{"time":"Thu, 01 Jan 1970 00:00:00 +0000"}"#
152        );
153        assert_eq!(
154            serde_json::to_string(&Test { time: None }).unwrap(),
155            r#"{"time":null}"#
156        );
157    }
158
159    #[test]
160    fn deserialize_json() {
161        assert_eq!(
162            serde_json::from_str::<Test>(r#"{"time":"Thu, 01 Jan 1970 00:00:00 +0000"}"#).unwrap(),
163            Test {
164                time: Some(FileTime::UNIX_EPOCH)
165            }
166        );
167        assert_eq!(
168            serde_json::from_str::<Test>(r#"{"time":null}"#).unwrap(),
169            Test { time: None }
170        );
171    }
172}