Skip to main content

t2t_core/serde/
mod.rs

1//! How a time crosses to a config file, a log or a peer.
2//!
3//! Every value serializes as the string its `Display` writes, where a person reads the format:
4//! RFC 3339 for an instant, `"1m30s"` for a span, `"24 ticks"` for a count of ticks. A format no
5//! person reads, such as bincode or postcard, takes each as its count. A peer that sends a count
6//! in a format a person reads names the unit at the field, and the counted type picks the module.
7//!
8//! Each unit has an `option` module for a field that may be absent. Every module reads a count as
9//! a number or a decimal string, refusing one past what the type holds, and writes a number, but
10//! for nanoseconds since the epoch: those pass what an `f64` holds exactly, so a JSON reader backed
11//! by one would round them, and they are written as a string.
12//!
13//! # Examples
14//! ```
15//! use serde::{Deserialize, Serialize};
16//! use serde_json::from_str;
17//! use t2t_core::serde::{timedelta, timestamp};
18//! use t2t_core::{Timedelta, Timestamp};
19//!
20//! #[derive(Serialize, Deserialize)]
21//! struct Listing {
22//!     #[serde(with = "timestamp::secs")]
23//!     expiry: Timestamp,
24//!     #[serde(with = "timedelta::millis")]
25//!     poll: Timedelta,
26//! }
27//!
28//! let listing: Listing = from_str(r#"{"expiry":1700000300,"poll":250}"#)?;
29//! assert_eq!(listing.expiry, Timestamp::from_secs(1_700_000_300), "whole seconds");
30//! assert_eq!(listing.poll, Timedelta::from_millis(250), "whole milliseconds");
31//! # Ok::<(), serde_json::Error>(())
32//! ```
33
34#[cfg(feature = "schemars")]
35mod schema;
36mod spelling;
37
38use core::fmt;
39
40use serde_core::de::{self, Visitor};
41use serde_core::{Deserializer, Serializer};
42
43use crate::OutOfRangeError;
44use crate::errors::narrow;
45
46/// Reads a count written as a number or as a decimal string.
47struct CountVisitor;
48
49impl Visitor<'_> for CountVisitor {
50    type Value = i64;
51
52    fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
53        formatter.write_str("a count, as a number or a decimal string")
54    }
55
56    fn visit_i64<E: de::Error>(self, count: i64) -> Result<i64, E> {
57        Ok(count)
58    }
59
60    fn visit_u64<E: de::Error>(self, count: u64) -> Result<i64, E> {
61        narrow(count).map_err(E::custom)
62    }
63
64    fn visit_str<E: de::Error>(self, count: &str) -> Result<i64, E> {
65        count.parse().map_err(E::custom)
66    }
67}
68
69/// Reads a count of units `nanos_per_unit` nanoseconds long, as nanoseconds: whichever way it was
70/// written, where a person reads the format, and as the `i64` both writers write where none does,
71/// since a format that does not describe itself, as bincode or postcard, reads only the type it is
72/// asked for.
73fn read<'de, D: Deserializer<'de>>(deserializer: D, nanos_per_unit: i64) -> Result<i64, D::Error> {
74    let count = if deserializer.is_human_readable() {
75        deserializer.deserialize_any(CountVisitor)?
76    } else {
77        deserializer.deserialize_i64(CountVisitor)?
78    };
79    count.checked_mul(nanos_per_unit).ok_or_else(|| de::Error::custom(OutOfRangeError))
80}
81
82/// Writes a count as a number.
83fn write_number<S: Serializer>(count: i64, serializer: S) -> Result<S::Ok, S::Error> {
84    serializer.serialize_i64(count)
85}
86
87/// Writes a count as a decimal string, or as a number where no person reads the format.
88fn write_decimal<S: Serializer>(count: i64, serializer: S) -> Result<S::Ok, S::Error> {
89    if serializer.is_human_readable() {
90        serializer.collect_str(&count)
91    } else {
92        serializer.serialize_i64(count)
93    }
94}
95
96/// A `#[serde(with = …)]` module, and its `option` twin, for one type counted in one unit.
97macro_rules! count_module {
98    (
99        $module:ident,
100        $type:ident,
101        $as_unit:ident,
102        $nanos_per_unit:ident,
103        $write:ident,
104        $doc:literal $(,)?
105    ) => {
106        #[doc = concat!("A [`", stringify!($type), "`](crate::", stringify!($type), ") as ", $doc, ".")]
107        pub mod $module {
108            use serde_core::{Deserializer, Serializer};
109
110            use crate::consts::$nanos_per_unit;
111            use crate::serde::{read, $write};
112            use crate::$type;
113
114            /// Writes the count.
115            ///
116            /// # Errors
117            /// Whatever the serializer reports.
118            pub fn serialize<S: Serializer>(
119                value: &$type, serializer: S,
120            ) -> Result<S::Ok, S::Error> {
121                $write(value.$as_unit(), serializer)
122            }
123
124            /// Reads the count.
125            ///
126            /// # Errors
127            /// Whatever the deserializer reports, a string that is not a count, or a count past
128            /// what the type holds.
129            pub fn deserialize<'de, D: Deserializer<'de>>(
130                deserializer: D,
131            ) -> Result<$type, D::Error> {
132                read(deserializer, $nanos_per_unit).map($type::from_nanos)
133            }
134
135            /// The same, for a field that may be absent.
136            pub mod option {
137                use serde_core::{Deserialize, Deserializer, Serialize, Serializer};
138
139                use crate::consts::$nanos_per_unit;
140                use crate::serde::{read, $write};
141                use crate::$type;
142
143                /// A count, in a shape `Option` writes and reads, with the presence a format no
144                /// person reads marks.
145                struct Counted($type);
146
147                impl Serialize for Counted {
148                    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
149                        $write(self.0.$as_unit(), serializer)
150                    }
151                }
152
153                impl<'de> Deserialize<'de> for Counted {
154                    fn deserialize<D: Deserializer<'de>>(
155                        deserializer: D,
156                    ) -> Result<Self, D::Error> {
157                        read(deserializer, $nanos_per_unit).map($type::from_nanos).map(Self)
158                    }
159                }
160
161                /// Writes the count, or nothing.
162                ///
163                /// # Errors
164                /// Whatever the serializer reports.
165                pub fn serialize<S: Serializer>(
166                    value: &Option<$type>, serializer: S,
167                ) -> Result<S::Ok, S::Error> {
168                    value.map(Counted).serialize(serializer)
169                }
170
171                /// Reads the count, or nothing.
172                ///
173                /// # Errors
174                /// Whatever the deserializer reports, a string that is not a count, or a count
175                /// past what the type holds.
176                pub fn deserialize<'de, D: Deserializer<'de>>(
177                    deserializer: D,
178                ) -> Result<Option<$type>, D::Error> {
179                    Ok(Option::<Counted>::deserialize(deserializer)?.map(|counted| counted.0))
180                }
181            }
182        }
183    };
184}
185
186/// A [`Timestamp`](crate::Timestamp) as a count since the Unix epoch, in the unit the module
187/// names.
188pub mod timestamp {
189    count_module!(
190        secs,
191        Timestamp,
192        as_secs,
193        NANOS_PER_SECOND,
194        write_number,
195        "whole seconds since the Unix epoch",
196    );
197    count_module!(
198        millis,
199        Timestamp,
200        as_millis,
201        NANOS_PER_MILLISECOND,
202        write_number,
203        "whole milliseconds since the Unix epoch",
204    );
205    count_module!(
206        micros,
207        Timestamp,
208        as_micros,
209        NANOS_PER_MICROSECOND,
210        write_number,
211        "whole microseconds since the Unix epoch",
212    );
213    count_module!(
214        nanos,
215        Timestamp,
216        as_nanos,
217        NANOS_PER_NANOSECOND,
218        write_decimal,
219        "nanoseconds since the Unix epoch, in a string",
220    );
221}
222
223/// A [`Timedelta`](crate::Timedelta) as a count, in the unit the module names.
224pub mod timedelta {
225    count_module!(
226        secs,
227        Timedelta,
228        as_secs,
229        NANOS_PER_SECOND,
230        write_number,
231        "a count of whole seconds",
232    );
233    count_module!(
234        millis,
235        Timedelta,
236        as_millis,
237        NANOS_PER_MILLISECOND,
238        write_number,
239        "a count of whole milliseconds",
240    );
241    count_module!(
242        micros,
243        Timedelta,
244        as_micros,
245        NANOS_PER_MICROSECOND,
246        write_number,
247        "a count of whole microseconds",
248    );
249    count_module!(
250        nanos,
251        Timedelta,
252        as_nanos,
253        NANOS_PER_NANOSECOND,
254        write_number,
255        "a count of nanoseconds",
256    );
257}
258
259#[cfg(test)]
260mod tests {
261    use alloc::format;
262    use alloc::string::ToString as _;
263
264    use rstest::rstest;
265    use serde::{Deserialize, Serialize};
266    use serde_json::{from_str, to_string};
267    use serde_test::{Configure as _, Token, assert_tokens};
268
269    use super::{timedelta, timestamp};
270    use crate::{Timedelta, Timestamp};
271
272    /// One field for each kind of module.
273    #[derive(Debug, PartialEq, Serialize, Deserialize)]
274    struct Fields {
275        /// Whole seconds.
276        #[serde(with = "timestamp::secs")]
277        expiry: Timestamp,
278        /// Nanoseconds, written as a string.
279        #[serde(with = "timestamp::nanos")]
280        exact: Timestamp,
281        /// A span counted in milliseconds.
282        #[serde(with = "timedelta::millis")]
283        poll: Timedelta,
284        /// A span that may be absent.
285        #[serde(with = "timedelta::nanos::option")]
286        budget: Option<Timedelta>,
287    }
288
289    /// The fields every test reads and writes.
290    const FIELDS: Fields = Fields {
291        expiry: Timestamp::from_secs(1_700_000_300),
292        exact: Timestamp::from_nanos(1_789_544_735_123_456_789),
293        poll: Timedelta::from_millis(250),
294        budget: Some(Timedelta::from_nanos(500)),
295    };
296
297    /// The reason a count past what its type holds is refused for.
298    const OUT_OF_RANGE: &str = "out of range error: the time is outside what the target type holds";
299
300    #[test]
301    fn each_unit_round_trips_as_its_count() {
302        let written = to_string(&FIELDS).expect("a document");
303        assert_eq!(
304            written,
305            r#"{"expiry":1700000300,"exact":"1789544735123456789","poll":250,"budget":500}"#,
306            "each field as a count in its unit"
307        );
308        assert_eq!(from_str::<Fields>(&written).expect("the fields"), FIELDS, "and back");
309    }
310
311    #[test]
312    fn a_format_no_person_reads_takes_each_count_as_an_integer() {
313        assert_tokens(
314            &FIELDS.compact(),
315            &[
316                Token::Struct { name: "Fields", len: 4 },
317                Token::Str("expiry"),
318                Token::I64(1_700_000_300),
319                Token::Str("exact"),
320                Token::I64(1_789_544_735_123_456_789),
321                Token::Str("poll"),
322                Token::I64(250),
323                Token::Str("budget"),
324                Token::Some,
325                Token::I64(500),
326                Token::StructEnd,
327            ],
328        );
329    }
330
331    #[test]
332    fn a_count_is_read_quoted_or_bare() {
333        let read: Fields = from_str(
334            r#"{"expiry":"1700000300","exact":1789544735123456789,"poll":"250","budget":null}"#,
335        )
336        .expect("the fields");
337        assert_eq!(read, Fields { budget: None, ..FIELDS }, "each count, however it came");
338    }
339
340    #[rstest]
341    #[case::the_year_9999("253402300799", OUT_OF_RANGE)]
342    #[case::not_a_count(r#""soon""#, "invalid digit found in string")]
343    #[case::past_an_i64("18446744073709551615", OUT_OF_RANGE)]
344    fn a_count_past_the_range_or_not_a_count_is_refused(
345        #[case] expiry: &str, #[case] reason: &str,
346    ) {
347        let document = format!(r#"{{"expiry":{expiry},"exact":"0","poll":0,"budget":null}}"#);
348        let error = from_str::<Fields>(&document).expect_err("a count the field refuses");
349        let written = error.to_string();
350        assert_eq!(written.split(" at line ").next(), Some(reason), "refused for its reason");
351    }
352}