Skip to main content

printobserver_types/
timestamp.rs

1//! The one instant representation this vocabulary has.
2//!
3//! Every timestamp is an instant in UTC, serialized as an RFC 3339 string with
4//! a zero offset. There is no local-time representation anywhere in this crate:
5//! a string carrying a non-zero offset parses to the instant it denotes and
6//! re-emits at a zero offset, and a string carrying no offset at all is refused
7//! rather than guessed at.
8
9use core::fmt;
10use core::str::FromStr;
11use std::borrow::Cow;
12
13use chrono::{DateTime, SecondsFormat, TimeDelta, Utc};
14use schemars::{JsonSchema, Schema, SchemaGenerator, json_schema};
15use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error as _};
16
17/// Why a string is not an instant this vocabulary can hold.
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct TimestampError {
20    /// What is wrong with the string.
21    detail: String,
22}
23
24impl TimestampError {
25    /// What is wrong with the string.
26    #[must_use]
27    pub fn detail(&self) -> &str {
28        &self.detail
29    }
30}
31
32impl fmt::Display for TimestampError {
33    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
34        write!(formatter, "not an RFC 3339 instant: {}", self.detail)
35    }
36}
37
38impl core::error::Error for TimestampError {}
39
40/// An instant in UTC, serialized as an RFC 3339 string with a zero offset.
41#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
42pub struct Timestamp(DateTime<Utc>);
43
44impl Timestamp {
45    /// The instant now.
46    #[must_use]
47    pub fn now() -> Self {
48        Self(Utc::now())
49    }
50
51    /// The instant this many seconds after the Unix epoch.
52    ///
53    /// # Errors
54    ///
55    /// Returns an error when the count of seconds denotes no representable
56    /// instant.
57    pub fn from_unix_seconds(seconds: i64) -> Result<Self, TimestampError> {
58        DateTime::from_timestamp(seconds, 0)
59            .map(Self)
60            .ok_or_else(|| TimestampError {
61                detail: format!(
62                    "{seconds} seconds from the Unix epoch is not a representable instant"
63                ),
64            })
65    }
66
67    /// The instant this many seconds after this one.
68    ///
69    /// Sub-second precision is kept rather than dropped: an adjustment asked to
70    /// stand for a minute stands for a minute, rather than for a minute less
71    /// however far into a second it happened to be made.
72    ///
73    /// # Errors
74    ///
75    /// Returns an error when the result is not a representable instant.
76    pub fn plus_seconds(self, seconds: i64) -> Result<Self, TimestampError> {
77        // `try_seconds` rather than `seconds`: a count of seconds that is not a
78        // representable span is answered here rather than raised, because the
79        // caller is a duration somebody asked for.
80        TimeDelta::try_seconds(seconds)
81            .and_then(|span| self.0.checked_add_signed(span))
82            .map(Self)
83            .ok_or_else(|| TimestampError {
84                detail: format!("{seconds} seconds after {self} is not a representable instant"),
85            })
86    }
87
88    /// The instant as the UTC date-time it is.
89    #[must_use]
90    pub const fn as_utc(&self) -> &DateTime<Utc> {
91        &self.0
92    }
93}
94
95impl From<DateTime<Utc>> for Timestamp {
96    fn from(instant: DateTime<Utc>) -> Self {
97        Self(instant)
98    }
99}
100
101impl fmt::Display for Timestamp {
102    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
103        // `use_z` is what makes the offset a literal zero rather than `+00:00`,
104        // and `AutoSi` keeps whatever sub-second precision the instant carries
105        // so that an instant survives a round trip unchanged.
106        write!(
107            formatter,
108            "{}",
109            self.0.to_rfc3339_opts(SecondsFormat::AutoSi, true)
110        )
111    }
112}
113
114impl FromStr for Timestamp {
115    type Err = TimestampError;
116
117    fn from_str(text: &str) -> Result<Self, Self::Err> {
118        DateTime::parse_from_rfc3339(text)
119            .map(|offset| Self(offset.with_timezone(&Utc)))
120            .map_err(|error| TimestampError {
121                detail: format!("{text:?}: {error}"),
122            })
123    }
124}
125
126impl Serialize for Timestamp {
127    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
128        serializer.collect_str(self)
129    }
130}
131
132impl<'de> Deserialize<'de> for Timestamp {
133    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
134        let text = String::deserialize(deserializer)?;
135        text.parse().map_err(D::Error::custom)
136    }
137}
138
139impl JsonSchema for Timestamp {
140    fn schema_name() -> Cow<'static, str> {
141        Cow::Borrowed("Timestamp")
142    }
143
144    fn json_schema(_generator: &mut SchemaGenerator) -> Schema {
145        json_schema!({
146            "type": "string",
147            "format": "date-time",
148            "title": "Timestamp",
149            "description": "An instant in UTC, as an RFC 3339 string with a zero offset."
150        })
151    }
152}