tea_protocol/
timestamp.rs1use std::fmt;
2use std::str::FromStr;
3
4use chrono::{DateTime, SecondsFormat, Utc};
5use serde::{Deserialize, Deserializer, Serialize, Serializer};
6use thiserror::Error;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
10pub struct ProtocolTimestamp(DateTime<Utc>);
11
12impl ProtocolTimestamp {
13 #[must_use]
15 pub const fn as_utc(self) -> DateTime<Utc> {
16 self.0
17 }
18}
19
20#[derive(Debug, Error)]
22pub enum ProtocolTimestampParseError {
23 #[error("timestamp is not valid RFC 3339: {0}")]
25 InvalidRfc3339(#[from] chrono::ParseError),
26 #[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}