#![forbid(unsafe_code)]
#![deny(missing_docs)]
use chrono::{DateTime, TimeZone, Utc};
#[cfg(any(test, feature = "testing"))]
#[cfg_attr(docsrs, doc(cfg(feature = "testing")))]
pub mod testing;
#[cfg(any(test, feature = "testing"))]
#[cfg_attr(docsrs, doc(cfg(feature = "testing")))]
pub use testing::MockClock as DeterministicClock;
pub trait Clock: Send + Sync + 'static {
fn now(&self) -> DateTime<Utc>;
}
#[derive(Debug, Default)]
pub struct SystemClock;
impl Clock for SystemClock {
fn now(&self) -> DateTime<Utc> {
Utc::now()
}
}
pub fn now_rfc3339<C: Clock>(clock: &C) -> String {
clock.now().to_rfc3339()
}
pub fn now_epoch<C: Clock>(clock: &C) -> i64 {
clock.now().timestamp()
}
pub fn parse_datetime_flexible(txt: Option<&str>, secs: Option<i64>) -> Option<DateTime<Utc>> {
if let Some(s) = txt
&& let Ok(dt) = DateTime::parse_from_rfc3339(s)
{
return Some(dt.with_timezone(&Utc));
}
secs.and_then(|ts| Utc.timestamp_opt(ts, 0).single())
}
pub fn epoch_to_rfc3339(secs: i64) -> Option<String> {
Utc.timestamp_opt(secs, 0)
.single()
.map(|dt| dt.to_rfc3339())
}
#[cfg(test)]
mod time_tests {
use super::*;
use chrono::Datelike;
#[test]
fn system_clock_now_returns_recent_wall_clock_time() {
let clock = SystemClock;
let now = clock.now();
let floor = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).single().unwrap();
assert!(
now > floor,
"SystemClock::now() must return wall-clock time, not Default (got {now})"
);
}
#[test]
fn now_rfc3339_returns_parseable_recent_timestamp() {
let s = now_rfc3339(&SystemClock);
assert!(!s.is_empty(), "now_rfc3339 must not be empty");
let parsed = DateTime::parse_from_rfc3339(&s)
.expect("now_rfc3339 must produce a valid RFC3339 string (got: {s})");
let floor = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).single().unwrap();
assert!(
parsed.with_timezone(&Utc) > floor,
"now_rfc3339 must reflect wall-clock time, not Default"
);
}
#[test]
fn now_epoch_returns_recent_timestamp() {
let t = now_epoch(&SystemClock);
assert!(
t > 1_700_000_000,
"now_epoch must return wall-clock seconds (got {t})"
);
}
#[test]
fn parse_datetime_flexible_branches() {
let valid_rfc3339 = "2026-04-01T12:00:00Z";
let parsed = parse_datetime_flexible(Some(valid_rfc3339), None)
.expect("valid RFC3339 must parse to Some");
assert_eq!(parsed.year(), 2026);
assert_eq!(parsed.month(), 4);
let from_secs = parse_datetime_flexible(Some("not-a-date"), Some(1_700_000_000))
.expect("invalid txt with valid secs must fall through to secs");
assert_eq!(from_secs.timestamp(), 1_700_000_000);
let txt_wins = parse_datetime_flexible(Some(valid_rfc3339), Some(1))
.expect("valid txt + secs must prefer txt");
assert_eq!(txt_wins.year(), 2026);
assert_ne!(
txt_wins.timestamp(),
1,
"&& → || would have used the secs fallback"
);
let only_secs = parse_datetime_flexible(None, Some(0))
.expect("txt None with valid secs must parse from secs");
assert_eq!(only_secs.timestamp(), 0);
assert!(
parse_datetime_flexible(None, None).is_none(),
"both None must return None"
);
}
#[test]
fn epoch_to_rfc3339_returns_parseable_iso_string() {
let s = epoch_to_rfc3339(1_700_000_000).expect("valid epoch must produce Some(rfc3339)");
let parsed = DateTime::parse_from_rfc3339(&s)
.expect("epoch_to_rfc3339 must produce a valid RFC3339 string");
assert_eq!(parsed.timestamp(), 1_700_000_000);
assert!(s.starts_with("2023-"), "1.7e9 maps to 2023 (got: {s})");
}
}