printobserver_types/
timestamp.rs1use 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#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct TimestampError {
20 detail: String,
22}
23
24impl TimestampError {
25 #[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#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
42pub struct Timestamp(DateTime<Utc>);
43
44impl Timestamp {
45 #[must_use]
47 pub fn now() -> Self {
48 Self(Utc::now())
49 }
50
51 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 pub fn plus_seconds(self, seconds: i64) -> Result<Self, TimestampError> {
77 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 #[must_use]
100 pub fn latest() -> Self {
101 let last_day = chrono::NaiveDate::from_ymd_opt(9999, 12, 31)
102 .and_then(|day| day.and_hms_nano_opt(23, 59, 59, 999_999_999))
103 .expect("the last moment of the year 9999 is a date-time");
104 Self(last_day.and_utc())
105 }
106
107 #[must_use]
109 pub const fn as_utc(&self) -> &DateTime<Utc> {
110 &self.0
111 }
112}
113
114impl From<DateTime<Utc>> for Timestamp {
115 fn from(instant: DateTime<Utc>) -> Self {
116 Self(instant)
117 }
118}
119
120impl fmt::Display for Timestamp {
121 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
122 write!(
126 formatter,
127 "{}",
128 self.0.to_rfc3339_opts(SecondsFormat::AutoSi, true)
129 )
130 }
131}
132
133impl FromStr for Timestamp {
134 type Err = TimestampError;
135
136 fn from_str(text: &str) -> Result<Self, Self::Err> {
137 DateTime::parse_from_rfc3339(text)
138 .map(|offset| Self(offset.with_timezone(&Utc)))
139 .map_err(|error| TimestampError {
140 detail: format!("{text:?}: {error}"),
141 })
142 }
143}
144
145impl Serialize for Timestamp {
146 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
147 serializer.collect_str(self)
148 }
149}
150
151impl<'de> Deserialize<'de> for Timestamp {
152 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
153 let text = String::deserialize(deserializer)?;
154 text.parse().map_err(D::Error::custom)
155 }
156}
157
158impl JsonSchema for Timestamp {
159 fn schema_name() -> Cow<'static, str> {
160 Cow::Borrowed("Timestamp")
161 }
162
163 fn json_schema(_generator: &mut SchemaGenerator) -> Schema {
164 json_schema!({
165 "type": "string",
166 "format": "date-time",
167 "title": "Timestamp",
168 "description": "An instant in UTC, as an RFC 3339 string with a zero offset."
169 })
170 }
171}