t2t-core 0.1.1

time values: points on the wall and monotonic clocks, counter readings, and the spans between them.
Documentation
//! Each value on the wire: the string its `Display` writes, where a person reads the format, and
//! its count, where none does.

use core::fmt;

use serde_core::de::{self, Unexpected, Visitor};
use serde_core::{Deserialize, Deserializer, Serialize, Serializer};

use crate::{
    BootUptime, RawUptime, TaiTimestamp, TickRate, Tickdelta, Tickstamp, Timedelta, Timestamp,
    Uptime,
};

/// Serializes `$type` as the string its `Display` and `FromStr` agree on where a person reads the
/// format, and as its count where none does.
///
/// The count is an `i64`, or `$as_count`'s `$count`, read back through `$from_count`.
macro_rules! spelled {
    ($type:ident, $expecting:literal $(,)?) => {
        spelled!($type, $expecting, i64, |value: $type| value.0, |count| Ok($type(count)));
    };
    ($type:ident, $expecting:literal, $count:ty, $as_count:expr, $from_count:expr $(,)?) => {
        impl<'de> Deserialize<'de> for $type {
            fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
                /// Reads the value from its spelling.
                struct Spelling;

                impl Visitor<'_> for Spelling {
                    type Value = $type;

                    fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
                        formatter.write_str($expecting)
                    }

                    fn visit_str<E: de::Error>(self, text: &str) -> Result<$type, E> {
                        text.parse().map_err(E::custom)
                    }
                }

                if deserializer.is_human_readable() {
                    deserializer.deserialize_str(Spelling)
                } else {
                    ($from_count)(<$count>::deserialize(deserializer)?)
                }
            }
        }

        impl Serialize for $type {
            fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
                if serializer.is_human_readable() {
                    serializer.collect_str(self)
                } else {
                    ($as_count)(*self).serialize(serializer)
                }
            }
        }
    };
}

spelled!(Timestamp, "an RFC 3339 instant in UTC, as \"2026-09-16T07:45:35Z\"");
spelled!(TaiTimestamp, "an RFC 3339 date and time in TAI, as \"2026-09-16T07:46:12 TAI\"");
spelled!(Uptime, "the span since the origin, as \"1m30s\"");
spelled!(RawUptime, "the span since the origin, as \"1m30s\"");
spelled!(BootUptime, "the span since boot, as \"1m30s\"");
spelled!(Tickstamp, "a count of ticks, as \"24 ticks\"");
spelled!(Timedelta, "a span, as \"1m30s\"");
spelled!(Tickdelta, "a count of ticks, as \"24 ticks\"");
// A zero count is refused as serde refuses one for a `NonZeroU64`.
spelled!(
    TickRate,
    "a count of hertz above zero, as \"24000000 Hz\"",
    u64,
    TickRate::as_hertz,
    |hertz| {
        TickRate::from_hertz(hertz).ok_or_else(|| {
            de::Error::invalid_value(Unexpected::Unsigned(hertz), &"a count of hertz above zero")
        })
    },
);

#[cfg(test)]
mod tests {
    use serde_test::{
        Compact, Configure as _, Readable, Token, assert_de_tokens_error, assert_tokens,
    };

    use crate::{
        BootUptime, RawUptime, TaiTimestamp, TickRate, Tickdelta, Tickstamp, Timedelta, Timestamp,
        Uptime,
    };

    #[test]
    fn each_value_round_trips_as_its_spelling() {
        let instant = Timestamp::from_nanos(1_789_544_735_123_456_789);
        assert_tokens(&instant.readable(), &[Token::Str("2026-09-16T07:45:35.123456789Z")]);
        let tai = TaiTimestamp::from_secs(1_789_544_772);
        assert_tokens(&tai.readable(), &[Token::Str("2026-09-16T07:46:12.000000000 TAI")]);
        assert_tokens(&Uptime::from_secs(90).readable(), &[Token::Str("1m30s")]);
        assert_tokens(&RawUptime::from_secs(90).readable(), &[Token::Str("1m30s")]);
        assert_tokens(&BootUptime::from_secs(90).readable(), &[Token::Str("1m30s")]);
        assert_tokens(&Tickstamp::from_ticks(24).readable(), &[Token::Str("24 ticks")]);
        assert_tokens(&Timedelta::from_mins(5).readable(), &[Token::Str("5m")]);
        assert_tokens(&Tickdelta::from_ticks(-24).readable(), &[Token::Str("-24 ticks")]);
        let rate = TickRate::from_hertz(24_000_000).expect("a nonzero rate");
        assert_tokens(&rate.readable(), &[Token::Str("24000000 Hz")]);
    }

    #[test]
    fn a_format_no_person_reads_takes_each_value_as_its_count() {
        let instant = Timestamp::from_nanos(1_789_544_735_123_456_789);
        assert_tokens(&instant.compact(), &[Token::I64(1_789_544_735_123_456_789)]);
        assert_tokens(&TaiTimestamp::from_nanos(7).compact(), &[Token::I64(7)]);
        assert_tokens(&Uptime::from_nanos(7).compact(), &[Token::I64(7)]);
        assert_tokens(&RawUptime::from_nanos(7).compact(), &[Token::I64(7)]);
        assert_tokens(&BootUptime::from_nanos(7).compact(), &[Token::I64(7)]);
        assert_tokens(&Tickstamp::from_ticks(7).compact(), &[Token::I64(7)]);
        assert_tokens(&Timedelta::from_nanos(-7).compact(), &[Token::I64(-7)]);
        assert_tokens(&Tickdelta::from_ticks(-7).compact(), &[Token::I64(-7)]);
        let rate = TickRate::from_hertz(24_000_000).expect("a nonzero rate");
        assert_tokens(&rate.compact(), &[Token::U64(24_000_000)]);
    }

    #[test]
    fn a_spelling_that_does_not_read_is_refused_with_its_reason() {
        assert_de_tokens_error::<Readable<Timedelta>>(
            &[Token::Str("5 minutes")],
            "parse timedelta error: expected `0`, or counts with units from coarsest to finest, \
             as `1m30s`",
        );
        assert_de_tokens_error::<Compact<TickRate>>(
            &[Token::U64(0)],
            "invalid value: integer `0`, expected a count of hertz above zero",
        );
    }
}