use core::fmt;
use core::str::FromStr;
use std::borrow::Cow;
use chrono::{DateTime, SecondsFormat, TimeDelta, Utc};
use schemars::{JsonSchema, Schema, SchemaGenerator, json_schema};
use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error as _};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TimestampError {
detail: String,
}
impl TimestampError {
#[must_use]
pub fn detail(&self) -> &str {
&self.detail
}
}
impl fmt::Display for TimestampError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "not an RFC 3339 instant: {}", self.detail)
}
}
impl core::error::Error for TimestampError {}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Timestamp(DateTime<Utc>);
impl Timestamp {
#[must_use]
pub fn now() -> Self {
Self(Utc::now())
}
pub fn from_unix_seconds(seconds: i64) -> Result<Self, TimestampError> {
DateTime::from_timestamp(seconds, 0)
.map(Self)
.ok_or_else(|| TimestampError {
detail: format!(
"{seconds} seconds from the Unix epoch is not a representable instant"
),
})
}
pub fn plus_seconds(self, seconds: i64) -> Result<Self, TimestampError> {
TimeDelta::try_seconds(seconds)
.and_then(|span| self.0.checked_add_signed(span))
.map(Self)
.ok_or_else(|| TimestampError {
detail: format!("{seconds} seconds after {self} is not a representable instant"),
})
}
#[must_use]
pub fn latest() -> Self {
let last_day = chrono::NaiveDate::from_ymd_opt(9999, 12, 31)
.and_then(|day| day.and_hms_nano_opt(23, 59, 59, 999_999_999))
.expect("the last moment of the year 9999 is a date-time");
Self(last_day.and_utc())
}
#[must_use]
pub const fn as_utc(&self) -> &DateTime<Utc> {
&self.0
}
}
impl From<DateTime<Utc>> for Timestamp {
fn from(instant: DateTime<Utc>) -> Self {
Self(instant)
}
}
impl fmt::Display for Timestamp {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
formatter,
"{}",
self.0.to_rfc3339_opts(SecondsFormat::AutoSi, true)
)
}
}
impl FromStr for Timestamp {
type Err = TimestampError;
fn from_str(text: &str) -> Result<Self, Self::Err> {
DateTime::parse_from_rfc3339(text)
.map(|offset| Self(offset.with_timezone(&Utc)))
.map_err(|error| TimestampError {
detail: format!("{text:?}: {error}"),
})
}
}
impl Serialize for Timestamp {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.collect_str(self)
}
}
impl<'de> Deserialize<'de> for Timestamp {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let text = String::deserialize(deserializer)?;
text.parse().map_err(D::Error::custom)
}
}
impl JsonSchema for Timestamp {
fn schema_name() -> Cow<'static, str> {
Cow::Borrowed("Timestamp")
}
fn json_schema(_generator: &mut SchemaGenerator) -> Schema {
json_schema!({
"type": "string",
"format": "date-time",
"title": "Timestamp",
"description": "An instant in UTC, as an RFC 3339 string with a zero offset."
})
}
}