Skip to main content

tea_protocol/
timestamp.rs

1use std::fmt;
2use std::str::FromStr;
3
4use chrono::{DateTime, SecondsFormat, Utc};
5use serde::{Deserialize, Deserializer, Serialize, Serializer};
6use thiserror::Error;
7
8/// An RFC 3339 timestamp normalized to UTC with millisecond precision.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
10pub struct ProtocolTimestamp(DateTime<Utc>);
11
12impl ProtocolTimestamp {
13    /// Returns the normalized UTC timestamp.
14    #[must_use]
15    pub const fn as_utc(self) -> DateTime<Utc> {
16        self.0
17    }
18}
19
20/// Error returned when parsing a protocol timestamp.
21#[derive(Debug, Error)]
22pub enum ProtocolTimestampParseError {
23    /// The timestamp is not valid RFC 3339 text.
24    #[error("timestamp is not valid RFC 3339: {0}")]
25    InvalidRfc3339(#[from] chrono::ParseError),
26    /// The timestamp does not have exactly three fractional second digits.
27    #[error("timestamp must have exactly millisecond precision")]
28    InvalidPrecision,
29}
30
31impl fmt::Display for ProtocolTimestamp {
32    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
33        formatter.write_str(&self.0.to_rfc3339_opts(SecondsFormat::Millis, true))
34    }
35}
36
37impl FromStr for ProtocolTimestamp {
38    type Err = ProtocolTimestampParseError;
39
40    fn from_str(value: &str) -> Result<Self, Self::Err> {
41        validate_millisecond_precision(value)?;
42        Ok(Self(
43            DateTime::parse_from_rfc3339(value)?.with_timezone(&Utc),
44        ))
45    }
46}
47
48impl Serialize for ProtocolTimestamp {
49    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
50    where
51        S: Serializer,
52    {
53        serializer.collect_str(self)
54    }
55}
56
57impl<'de> Deserialize<'de> for ProtocolTimestamp {
58    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
59    where
60        D: Deserializer<'de>,
61    {
62        let value = String::deserialize(deserializer)?;
63        value.parse().map_err(serde::de::Error::custom)
64    }
65}
66
67fn validate_millisecond_precision(value: &str) -> Result<(), ProtocolTimestampParseError> {
68    let time_start = value
69        .find('T')
70        .ok_or(ProtocolTimestampParseError::InvalidPrecision)?;
71    let timezone_start = if value.ends_with('Z') {
72        value.len() - 1
73    } else {
74        value[time_start + 1..]
75            .rfind(['+', '-'])
76            .map(|index| time_start + 1 + index)
77            .ok_or(ProtocolTimestampParseError::InvalidPrecision)?
78    };
79    let fraction_start = value[time_start + 1..timezone_start]
80        .find('.')
81        .map(|index| time_start + 1 + index + 1)
82        .ok_or(ProtocolTimestampParseError::InvalidPrecision)?;
83    let fraction = &value[fraction_start..timezone_start];
84    if fraction.len() != 3 || !fraction.bytes().all(|byte| byte.is_ascii_digit()) {
85        return Err(ProtocolTimestampParseError::InvalidPrecision);
86    }
87    Ok(())
88}