Skip to main content

t2t_core/serde/
spelling.rs

1//! Each value on the wire: the string its `Display` writes, where a person reads the format, and
2//! its count, where none does.
3
4use core::fmt;
5
6use serde_core::de::{self, Unexpected, Visitor};
7use serde_core::{Deserialize, Deserializer, Serialize, Serializer};
8
9use crate::{
10    BootUptime, RawUptime, TaiTimestamp, TickRate, Tickdelta, Tickstamp, Timedelta, Timestamp,
11    Uptime,
12};
13
14/// Serializes `$type` as the string its `Display` and `FromStr` agree on where a person reads the
15/// format, and as its count where none does.
16///
17/// The count is an `i64`, or `$as_count`'s `$count`, read back through `$from_count`.
18macro_rules! spelled {
19    ($type:ident, $expecting:literal $(,)?) => {
20        spelled!($type, $expecting, i64, |value: $type| value.0, |count| Ok($type(count)));
21    };
22    ($type:ident, $expecting:literal, $count:ty, $as_count:expr, $from_count:expr $(,)?) => {
23        impl<'de> Deserialize<'de> for $type {
24            fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
25                /// Reads the value from its spelling.
26                struct Spelling;
27
28                impl Visitor<'_> for Spelling {
29                    type Value = $type;
30
31                    fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
32                        formatter.write_str($expecting)
33                    }
34
35                    fn visit_str<E: de::Error>(self, text: &str) -> Result<$type, E> {
36                        text.parse().map_err(E::custom)
37                    }
38                }
39
40                if deserializer.is_human_readable() {
41                    deserializer.deserialize_str(Spelling)
42                } else {
43                    ($from_count)(<$count>::deserialize(deserializer)?)
44                }
45            }
46        }
47
48        impl Serialize for $type {
49            fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
50                if serializer.is_human_readable() {
51                    serializer.collect_str(self)
52                } else {
53                    ($as_count)(*self).serialize(serializer)
54                }
55            }
56        }
57    };
58}
59
60spelled!(Timestamp, "an RFC 3339 instant in UTC, as \"2026-09-16T07:45:35Z\"");
61spelled!(TaiTimestamp, "an RFC 3339 date and time in TAI, as \"2026-09-16T07:46:12 TAI\"");
62spelled!(Uptime, "the span since the origin, as \"1m30s\"");
63spelled!(RawUptime, "the span since the origin, as \"1m30s\"");
64spelled!(BootUptime, "the span since boot, as \"1m30s\"");
65spelled!(Tickstamp, "a count of ticks, as \"24 ticks\"");
66spelled!(Timedelta, "a span, as \"1m30s\"");
67spelled!(Tickdelta, "a count of ticks, as \"24 ticks\"");
68// A zero count is refused as serde refuses one for a `NonZeroU64`.
69spelled!(
70    TickRate,
71    "a count of hertz above zero, as \"24000000 Hz\"",
72    u64,
73    TickRate::as_hertz,
74    |hertz| {
75        TickRate::from_hertz(hertz).ok_or_else(|| {
76            de::Error::invalid_value(Unexpected::Unsigned(hertz), &"a count of hertz above zero")
77        })
78    },
79);
80
81#[cfg(test)]
82mod tests {
83    use serde_test::{
84        Compact, Configure as _, Readable, Token, assert_de_tokens_error, assert_tokens,
85    };
86
87    use crate::{
88        BootUptime, RawUptime, TaiTimestamp, TickRate, Tickdelta, Tickstamp, Timedelta, Timestamp,
89        Uptime,
90    };
91
92    #[test]
93    fn each_value_round_trips_as_its_spelling() {
94        let instant = Timestamp::from_nanos(1_789_544_735_123_456_789);
95        assert_tokens(&instant.readable(), &[Token::Str("2026-09-16T07:45:35.123456789Z")]);
96        let tai = TaiTimestamp::from_secs(1_789_544_772);
97        assert_tokens(&tai.readable(), &[Token::Str("2026-09-16T07:46:12.000000000 TAI")]);
98        assert_tokens(&Uptime::from_secs(90).readable(), &[Token::Str("1m30s")]);
99        assert_tokens(&RawUptime::from_secs(90).readable(), &[Token::Str("1m30s")]);
100        assert_tokens(&BootUptime::from_secs(90).readable(), &[Token::Str("1m30s")]);
101        assert_tokens(&Tickstamp::from_ticks(24).readable(), &[Token::Str("24 ticks")]);
102        assert_tokens(&Timedelta::from_mins(5).readable(), &[Token::Str("5m")]);
103        assert_tokens(&Tickdelta::from_ticks(-24).readable(), &[Token::Str("-24 ticks")]);
104        let rate = TickRate::from_hertz(24_000_000).expect("a nonzero rate");
105        assert_tokens(&rate.readable(), &[Token::Str("24000000 Hz")]);
106    }
107
108    #[test]
109    fn a_format_no_person_reads_takes_each_value_as_its_count() {
110        let instant = Timestamp::from_nanos(1_789_544_735_123_456_789);
111        assert_tokens(&instant.compact(), &[Token::I64(1_789_544_735_123_456_789)]);
112        assert_tokens(&TaiTimestamp::from_nanos(7).compact(), &[Token::I64(7)]);
113        assert_tokens(&Uptime::from_nanos(7).compact(), &[Token::I64(7)]);
114        assert_tokens(&RawUptime::from_nanos(7).compact(), &[Token::I64(7)]);
115        assert_tokens(&BootUptime::from_nanos(7).compact(), &[Token::I64(7)]);
116        assert_tokens(&Tickstamp::from_ticks(7).compact(), &[Token::I64(7)]);
117        assert_tokens(&Timedelta::from_nanos(-7).compact(), &[Token::I64(-7)]);
118        assert_tokens(&Tickdelta::from_ticks(-7).compact(), &[Token::I64(-7)]);
119        let rate = TickRate::from_hertz(24_000_000).expect("a nonzero rate");
120        assert_tokens(&rate.compact(), &[Token::U64(24_000_000)]);
121    }
122
123    #[test]
124    fn a_spelling_that_does_not_read_is_refused_with_its_reason() {
125        assert_de_tokens_error::<Readable<Timedelta>>(
126            &[Token::Str("5 minutes")],
127            "parse timedelta error: expected `0`, or counts with units from coarsest to finest, \
128             as `1m30s`",
129        );
130        assert_de_tokens_error::<Compact<TickRate>>(
131            &[Token::U64(0)],
132            "invalid value: integer `0`, expected a count of hertz above zero",
133        );
134    }
135}