use std::sync::{Arc, Mutex};
use chrono::{DateTime, Utc};
pub trait ClockSource: Send + Sync + 'static {
fn now(&self) -> DateTime<Utc>;
}
#[derive(Debug, Clone, Copy)]
pub struct Clock(DateTime<Utc>);
impl Clock {
#[must_use]
pub const fn now(&self) -> DateTime<Utc> {
self.0
}
}
impl std::ops::Deref for Clock {
type Target = DateTime<Utc>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl axum::extract::FromRequestParts<crate::state::AppState> for Clock {
type Rejection = std::convert::Infallible;
async fn from_request_parts(
_parts: &mut axum::http::request::Parts,
state: &crate::state::AppState,
) -> Result<Self, Self::Rejection> {
Ok(Self(state.clock().now()))
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct SystemClock;
impl ClockSource for SystemClock {
fn now(&self) -> DateTime<Utc> {
Utc::now()
}
}
#[derive(Debug, Clone, Copy)]
pub struct FixedClock(DateTime<Utc>);
impl FixedClock {
#[must_use]
pub const fn at(dt: DateTime<Utc>) -> Self {
Self(dt)
}
}
impl ClockSource for FixedClock {
fn now(&self) -> DateTime<Utc> {
self.0
}
}
#[derive(Clone, Debug)]
pub struct TickingClock(Arc<Mutex<DateTime<Utc>>>);
impl TickingClock {
#[must_use]
pub fn starting_at(dt: DateTime<Utc>) -> Self {
Self(Arc::new(Mutex::new(dt)))
}
pub fn advance(&self, duration: std::time::Duration) {
let mut guard = self
.0
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if let Ok(delta) = chrono::Duration::from_std(duration) {
*guard += delta;
}
}
}
impl ClockSource for TickingClock {
fn now(&self) -> DateTime<Utc> {
*self
.0
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
}
#[must_use]
pub fn clock_unix_secs(clock: &dyn ClockSource) -> u64 {
clock_unix_duration(clock).as_secs()
}
#[must_use]
pub fn clock_unix_duration(clock: &dyn ClockSource) -> std::time::Duration {
let now = clock.now();
let ts = now.timestamp();
if ts >= 0 {
std::time::Duration::new(ts.cast_unsigned(), now.timestamp_subsec_nanos())
} else {
std::time::Duration::ZERO
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::TimeZone;
#[test]
fn system_clock_returns_time_close_to_utc_now() {
let clock = SystemClock;
let a = clock.now();
let b = Utc::now();
assert!(
(b - a).num_seconds().abs() < 1,
"SystemClock should be within 1s of Utc::now()"
);
}
#[test]
fn fixed_clock_always_returns_same_time() {
let pinned = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
let clock = FixedClock::at(pinned);
assert_eq!(clock.now(), pinned);
assert_eq!(clock.now(), pinned);
}
#[test]
fn ticking_clock_starts_at_given_time() {
let start = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
let clock = TickingClock::starting_at(start);
assert_eq!(clock.now(), start);
}
#[test]
fn ticking_clock_advances_correctly() {
let start = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
let clock = TickingClock::starting_at(start);
clock.advance(std::time::Duration::from_secs(3600));
assert_eq!(clock.now(), start + chrono::Duration::hours(1));
}
#[test]
fn ticking_clock_clone_shares_state() {
let start = Utc.with_ymd_and_hms(2025, 6, 1, 12, 0, 0).unwrap();
let clock = TickingClock::starting_at(start);
let clone = clock.clone();
clock.advance(std::time::Duration::from_secs(86400));
assert_eq!(clone.now(), start + chrono::Duration::days(1));
}
#[test]
fn clock_unix_secs_uses_clock_timestamp() {
let pinned = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
let clock = FixedClock::at(pinned);
let secs = clock_unix_secs(&clock);
assert_eq!(secs, pinned.timestamp().cast_unsigned());
}
#[test]
fn clock_unix_duration_zero_for_pre_epoch() {
let pre_epoch = Utc.with_ymd_and_hms(1969, 12, 31, 23, 59, 59).unwrap();
let clock = FixedClock::at(pre_epoch);
assert_eq!(clock_unix_duration(&clock), std::time::Duration::ZERO);
}
}