#[cfg(not(feature = "chrono"))]
pub type DateTime = time::OffsetDateTime;
#[cfg(feature = "chrono")]
pub type DateTime = chrono::DateTime<chrono::Utc>;
#[cfg(not(feature = "chrono"))]
pub fn parse_rfc3339(s: &str) -> Result<DateTime, crate::Error> {
time::OffsetDateTime::parse(s, &time::format_description::well_known::Rfc3339)
.map(|dt| dt.to_offset(time::UtcOffset::UTC))
.map_err(|e| e.into())
}
#[cfg(feature = "chrono")]
pub fn parse_rfc3339(s: &str) -> Result<DateTime, crate::Error> {
chrono::DateTime::parse_from_rfc3339(s)
.map(|dt| dt.with_timezone(&chrono::Utc))
.map_err(|e| e.into())
}
#[cfg(not(feature = "chrono"))]
pub fn from_nanos(nanos: i128) -> Result<DateTime, crate::Error> {
time::OffsetDateTime::from_unix_timestamp_nanos(nanos).map_err(|e| e.into())
}
#[cfg(feature = "chrono")]
pub fn from_nanos(nanos: i128) -> Result<DateTime, crate::Error> {
let nanos_i64: i64 = nanos.try_into().map_err(|_| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"nanoseconds value out of range for the chrono backend (supports ~1678 to 2262)",
)
})?;
let secs = nanos_i64.div_euclid(1_000_000_000);
let nsecs = nanos_i64.rem_euclid(1_000_000_000) as u32;
chrono::DateTime::from_timestamp(secs, nsecs).ok_or_else(|| {
Box::new(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"invalid timestamp",
)) as crate::Error
})
}
#[cfg(not(feature = "chrono"))]
pub fn now() -> DateTime {
time::OffsetDateTime::now_utc()
}
#[cfg(feature = "chrono")]
pub fn now() -> DateTime {
chrono::Utc::now()
}
#[cfg(not(feature = "chrono"))]
pub(crate) use time::serde::rfc3339;
#[cfg(feature = "chrono")]
#[allow(dead_code)]
pub(crate) mod rfc3339 {
use serde::{Deserialize, Deserializer, Serialize, Serializer};
pub(crate) fn serialize<S>(
dt: &chrono::DateTime<chrono::Utc>,
serializer: S,
) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
Serialize::serialize(dt, serializer)
}
pub(crate) fn deserialize<'de, D>(
deserializer: D,
) -> Result<chrono::DateTime<chrono::Utc>, D::Error>
where
D: Deserializer<'de>,
{
Deserialize::deserialize(deserializer)
}
pub(crate) mod option {
use serde::{Deserialize, Deserializer, Serialize, Serializer};
pub(crate) fn serialize<S>(
dt: &Option<chrono::DateTime<chrono::Utc>>,
serializer: S,
) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
Serialize::serialize(dt, serializer)
}
pub(crate) fn deserialize<'de, D>(
deserializer: D,
) -> Result<Option<chrono::DateTime<chrono::Utc>>, D::Error>
where
D: Deserializer<'de>,
{
Deserialize::deserialize(deserializer)
}
}
}
#[cfg(not(feature = "chrono"))]
pub fn add_std_duration(
dt: DateTime,
duration: std::time::Duration,
) -> Result<DateTime, crate::Error> {
let time_duration = time::Duration::try_from(duration)?;
dt.checked_add(time_duration).ok_or_else(|| {
Box::new(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"datetime overflow",
)) as crate::Error
})
}
#[cfg(feature = "chrono")]
pub fn add_std_duration(
dt: DateTime,
duration: std::time::Duration,
) -> Result<DateTime, crate::Error> {
let chrono_duration = chrono::Duration::from_std(duration)?;
dt.checked_add_signed(chrono_duration).ok_or_else(|| {
Box::new(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"datetime overflow",
)) as crate::Error
})
}
#[cfg(test)]
mod tests {
use super::*;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
struct Wrap(#[serde(with = "rfc3339")] DateTime);
#[derive(Serialize, Deserialize, Default)]
struct WrapOpt(
#[serde(
default,
with = "rfc3339::option",
skip_serializing_if = "Option::is_none"
)]
Option<DateTime>,
);
const FIXED_NANOS: i128 = 1_672_628_645_123_456_789;
const FIXED_RFC3339: &str = "2023-01-02T03:04:05.123456789Z";
#[test]
fn rfc3339_full_precision_is_byte_identical() {
let dt = from_nanos(FIXED_NANOS).unwrap();
let json = serde_json::to_string(&Wrap(dt)).unwrap();
assert_eq!(json, format!("\"{FIXED_RFC3339}\""));
}
#[test]
fn rfc3339_roundtrips() {
let dt = from_nanos(FIXED_NANOS).unwrap();
let json = serde_json::to_string(&Wrap(dt)).unwrap();
let back: Wrap = serde_json::from_str(&json).unwrap();
assert_eq!(serde_json::to_string(&back).unwrap(), json);
}
#[test]
fn parse_rfc3339_matches_from_nanos() {
let parsed = parse_rfc3339(FIXED_RFC3339).unwrap();
let from_ts = from_nanos(FIXED_NANOS).unwrap();
assert_eq!(parsed, from_ts);
}
#[test]
fn parse_rfc3339_server_format_variants() {
let cases: &[(&str, i128)] = &[
("2023-01-02T03:04:05Z", 1_672_628_645_000_000_000),
("2023-01-02T03:04:05.123Z", 1_672_628_645_123_000_000),
("2023-01-02T03:04:05.123456Z", 1_672_628_645_123_456_000),
("2023-01-02T03:04:05.123456789Z", 1_672_628_645_123_456_789),
];
for (s, nanos) in cases {
let parsed = parse_rfc3339(s).unwrap_or_else(|e| panic!("parse {s:?}: {e}"));
assert_eq!(
parsed,
from_nanos(*nanos).unwrap(),
"instant mismatch for {s:?}"
);
let de: Wrap = serde_json::from_str(&format!("\"{s}\""))
.unwrap_or_else(|e| panic!("serde deserialize {s:?}: {e}"));
assert_eq!(
de.0,
from_nanos(*nanos).unwrap(),
"serde mismatch for {s:?}"
);
}
}
#[test]
fn rfc3339_trailing_zero_fractions_are_instant_equal() {
for nanos in [
1_672_628_645_120_000_000, 1_672_628_645_100_000_000, 1_672_628_645_000_000_000, ] {
let dt = from_nanos(nanos).unwrap();
let json = serde_json::to_string(&Wrap(dt)).unwrap();
let back: Wrap = serde_json::from_str(&json).unwrap();
assert_eq!(
back.0,
from_nanos(nanos).unwrap(),
"instant changed for {nanos}"
);
let s = json.trim_matches('"');
assert_eq!(parse_rfc3339(s).unwrap(), from_nanos(nanos).unwrap());
}
}
#[test]
fn from_nanos_handles_negative_sub_second() {
let cases: &[(i128, &str)] = &[
(-1, "1969-12-31T23:59:59.999999999Z"),
(-1_500_000_000, "1969-12-31T23:59:58.500000000Z"),
(-1_000_000_000, "1969-12-31T23:59:59Z"),
];
for (nanos, expected) in cases {
let dt = from_nanos(*nanos).unwrap_or_else(|e| panic!("from_nanos({nanos}): {e}"));
assert_eq!(dt, parse_rfc3339(expected).unwrap(), "mismatch for {nanos}");
}
}
#[test]
fn rfc3339_option_serde() {
let some = WrapOpt(Some(from_nanos(FIXED_NANOS).unwrap()));
let json = serde_json::to_string(&some).unwrap();
assert_eq!(json, format!("\"{FIXED_RFC3339}\""));
let back: WrapOpt = serde_json::from_str(&json).unwrap();
assert_eq!(back.0, Some(from_nanos(FIXED_NANOS).unwrap()));
let null: WrapOpt = serde_json::from_str("null").unwrap();
assert_eq!(null.0, None);
#[derive(Deserialize)]
struct Outer {
#[serde(default, with = "rfc3339::option")]
ts: Option<DateTime>,
}
let outer: Outer = serde_json::from_str("{}").unwrap();
assert_eq!(outer.ts, None);
}
#[test]
fn parse_rfc3339_normalizes_offset() {
let with_offset = parse_rfc3339("2023-01-02T03:04:05+02:00").unwrap();
let utc = parse_rfc3339("2023-01-02T01:04:05Z").unwrap();
assert_eq!(with_offset, utc);
}
#[cfg(not(feature = "chrono"))]
#[test]
fn datetime_is_time_by_default() {
fn assert_offset_date_time(_: time::OffsetDateTime) {}
assert_offset_date_time(now());
}
#[cfg(feature = "chrono")]
#[test]
fn datetime_is_chrono_when_enabled() {
fn assert_chrono(_: chrono::DateTime<chrono::Utc>) {}
assert_chrono(now());
}
}