use serde::{Deserialize, Deserializer, Serialize, Serializer};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
pub struct Nanos(i64);
impl Nanos {
pub const fn from_i64(n: i64) -> Self {
Self(n)
}
pub const fn as_i64(self) -> i64 {
self.0
}
#[cfg(feature = "chrono")]
#[cfg_attr(docsrs, doc(cfg(feature = "chrono")))]
pub fn to_chrono(self) -> chrono::DateTime<chrono::Utc> {
let secs = self.0.div_euclid(1_000_000_000);
let nsec = self.0.rem_euclid(1_000_000_000) as u32;
chrono::DateTime::from_timestamp(secs, nsec).expect("nanoseconds in range")
}
#[cfg(feature = "time")]
#[cfg_attr(docsrs, doc(cfg(feature = "time")))]
pub fn to_time(self) -> time::OffsetDateTime {
time::OffsetDateTime::from_unix_timestamp_nanos(self.0 as i128)
.expect("nanoseconds in range")
}
}
impl Serialize for Nanos {
fn serialize<S: Serializer>(&self, s: S) -> std::result::Result<S::Ok, S::Error> {
s.serialize_str(&self.0.to_string())
}
}
impl<'de> Deserialize<'de> for Nanos {
fn deserialize<D: Deserializer<'de>>(d: D) -> std::result::Result<Self, D::Error> {
use serde::de::Error;
#[derive(Deserialize)]
#[serde(untagged)]
enum Wire<'a> {
Str(&'a str),
Num(i64),
}
match Wire::deserialize(d)? {
Wire::Str(s) => s.parse::<i64>().map(Nanos).map_err(D::Error::custom),
Wire::Num(n) => Ok(Nanos(n)),
}
}
}
pub trait IntoTimestamp {
fn into_timestamp_string(self) -> String;
}
impl IntoTimestamp for i64 {
fn into_timestamp_string(self) -> String {
self.to_string()
}
}
impl IntoTimestamp for Nanos {
fn into_timestamp_string(self) -> String {
self.0.to_string()
}
}
impl IntoTimestamp for &str {
fn into_timestamp_string(self) -> String {
self.to_string()
}
}
impl IntoTimestamp for String {
fn into_timestamp_string(self) -> String {
self
}
}
#[cfg(feature = "chrono")]
#[cfg_attr(docsrs, doc(cfg(feature = "chrono")))]
impl IntoTimestamp for chrono::DateTime<chrono::Utc> {
fn into_timestamp_string(self) -> String {
self.timestamp_nanos_opt()
.expect("datetime in representable range")
.to_string()
}
}
#[cfg(feature = "time")]
#[cfg_attr(docsrs, doc(cfg(feature = "time")))]
impl IntoTimestamp for time::OffsetDateTime {
fn into_timestamp_string(self) -> String {
self.unix_timestamp_nanos().to_string()
}
}
#[derive(Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct Timestamp(String);
impl Timestamp {
pub fn new(s: impl Into<String>) -> Self {
Self(s.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn into_string(self) -> String {
self.0
}
#[cfg(feature = "chrono")]
#[cfg_attr(docsrs, doc(cfg(feature = "chrono")))]
pub fn to_chrono(
&self,
) -> std::result::Result<chrono::DateTime<chrono::Utc>, chrono::ParseError> {
chrono::DateTime::parse_from_rfc3339(&self.0).map(|dt| dt.with_timezone(&chrono::Utc))
}
#[cfg(feature = "time")]
#[cfg_attr(docsrs, doc(cfg(feature = "time")))]
pub fn to_time(&self) -> std::result::Result<time::OffsetDateTime, time::error::Parse> {
time::OffsetDateTime::parse(&self.0, &time::format_description::well_known::Rfc3339)
}
}
impl std::fmt::Debug for Timestamp {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Debug::fmt(&self.0, f)
}
}
impl std::fmt::Display for Timestamp {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl AsRef<str> for Timestamp {
fn as_ref(&self) -> &str {
&self.0
}
}
impl From<String> for Timestamp {
fn from(s: String) -> Self {
Self(s)
}
}
impl From<&str> for Timestamp {
fn from(s: &str) -> Self {
Self(s.to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn nanos_roundtrip_json() {
let n = Nanos::from_i64(1_704_067_200_123_456_789);
let s = serde_json::to_string(&n).unwrap();
assert_eq!(s, "\"1704067200123456789\"");
let back: Nanos = serde_json::from_str(&s).unwrap();
assert_eq!(back, n);
}
#[test]
fn nanos_deserializes_number_too() {
let back: Nanos = serde_json::from_str("1704067200000000000").unwrap();
assert_eq!(back.as_i64(), 1_704_067_200_000_000_000);
}
#[test]
fn into_timestamp_i64() {
assert_eq!(123_i64.into_timestamp_string(), "123");
}
#[test]
fn into_timestamp_str_passthrough() {
assert_eq!(
"2025-01-01T00:00:00Z".into_timestamp_string(),
"2025-01-01T00:00:00Z"
);
}
#[cfg(feature = "chrono")]
#[test]
fn into_timestamp_chrono() {
use chrono::{TimeZone, Utc};
let dt = Utc.timestamp_opt(1_704_067_200, 0).single().unwrap();
assert_eq!(dt.into_timestamp_string(), "1704067200000000000");
}
#[test]
fn timestamp_roundtrip_json() {
let t = Timestamp::from("2024-01-01T00:00:00Z");
let s = serde_json::to_string(&t).unwrap();
assert_eq!(s, "\"2024-01-01T00:00:00Z\"");
let back: Timestamp = serde_json::from_str(&s).unwrap();
assert_eq!(back, t);
}
#[test]
fn timestamp_deserializes_preserves_format() {
let t: Timestamp = serde_json::from_str("\"2024-01-01T00:00:00.123+02:00\"").unwrap();
assert_eq!(t.as_str(), "2024-01-01T00:00:00.123+02:00");
}
#[test]
fn timestamp_debug_prints_as_string() {
let t = Timestamp::from("2024-01-01T00:00:00Z");
assert_eq!(format!("{t:?}"), "\"2024-01-01T00:00:00Z\"");
assert_eq!(format!("{t}"), "2024-01-01T00:00:00Z");
}
#[cfg(feature = "chrono")]
#[test]
fn timestamp_to_chrono_normalizes_offset_to_utc() {
let t = Timestamp::from("2024-01-01T02:00:00+02:00");
let dt = t.to_chrono().unwrap();
assert_eq!(dt.to_rfc3339(), "2024-01-01T00:00:00+00:00");
}
#[cfg(feature = "chrono")]
#[test]
fn timestamp_to_chrono_surfaces_parse_error() {
let t = Timestamp::from("not a timestamp");
assert!(t.to_chrono().is_err());
}
#[cfg(feature = "time")]
#[test]
fn timestamp_to_time_parses_rfc3339() {
let t = Timestamp::from("2024-01-01T00:00:00Z");
let dt = t.to_time().unwrap();
assert_eq!(dt.unix_timestamp(), 1_704_067_200);
}
#[cfg(feature = "time")]
#[test]
fn timestamp_to_time_surfaces_parse_error() {
let t = Timestamp::from("not a timestamp");
assert!(t.to_time().is_err());
}
}