#[cfg(feature = "system-clock")]
use chrono::{DateTime, FixedOffset, Local, NaiveDate, NaiveDateTime, Utc};
#[cfg(not(feature = "system-clock"))]
use chrono::{DateTime, FixedOffset, NaiveDate, NaiveDateTime, Utc};
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub enum TimeZoneSpec {
#[default]
Local,
Utc,
FixedOffsetSeconds(i32),
}
impl TimeZoneSpec {
pub fn fixed_offset(&self) -> Option<FixedOffset> {
match self {
TimeZoneSpec::Utc => FixedOffset::east_opt(0),
TimeZoneSpec::FixedOffsetSeconds(secs) => FixedOffset::east_opt(*secs),
TimeZoneSpec::Local => None,
}
}
pub fn validate_for_determinism(&self) -> Result<(), String> {
match self {
TimeZoneSpec::Local => Err(
"Deterministic mode forbids `Local` timezone (use UTC or a fixed offset)"
.to_string(),
),
TimeZoneSpec::Utc => Ok(()),
TimeZoneSpec::FixedOffsetSeconds(secs) => {
FixedOffset::east_opt(*secs).ok_or_else(|| {
format!("Invalid fixed offset: {secs} seconds (must be within +/-24h)")
})?;
Ok(())
}
}
}
}
pub trait ClockProvider: std::fmt::Debug + Send + Sync {
fn timezone(&self) -> &TimeZoneSpec;
fn now(&self) -> NaiveDateTime;
fn today(&self) -> NaiveDate {
self.now().date()
}
}
#[cfg(feature = "system-clock")]
#[derive(Clone, Debug)]
pub struct SystemClock {
timezone: TimeZoneSpec,
}
#[cfg(feature = "system-clock")]
impl SystemClock {
pub fn new(timezone: TimeZoneSpec) -> Self {
Self { timezone }
}
}
#[cfg(feature = "system-clock")]
impl ClockProvider for SystemClock {
fn timezone(&self) -> &TimeZoneSpec {
&self.timezone
}
fn now(&self) -> NaiveDateTime {
match &self.timezone {
TimeZoneSpec::Local => Local::now().naive_local(),
TimeZoneSpec::Utc => Utc::now().naive_utc(),
TimeZoneSpec::FixedOffsetSeconds(secs) => {
let off = FixedOffset::east_opt(*secs)
.unwrap_or_else(|| FixedOffset::east_opt(0).unwrap());
let utc_now: DateTime<Utc> = Utc::now();
utc_now.with_timezone(&off).naive_local()
}
}
}
}
#[derive(Clone, Debug)]
pub struct FixedClock {
timestamp_utc: DateTime<Utc>,
timezone: TimeZoneSpec,
}
impl FixedClock {
pub fn new(timestamp_utc: DateTime<Utc>, timezone: TimeZoneSpec) -> Self {
Self {
timestamp_utc,
timezone,
}
}
pub fn new_deterministic(
timestamp_utc: DateTime<Utc>,
timezone: TimeZoneSpec,
) -> Result<Self, String> {
timezone.validate_for_determinism()?;
Ok(Self::new(timestamp_utc, timezone))
}
fn now_in_timezone(&self) -> NaiveDateTime {
match &self.timezone {
TimeZoneSpec::Utc => self.timestamp_utc.naive_utc(),
TimeZoneSpec::FixedOffsetSeconds(secs) => {
let off = FixedOffset::east_opt(*secs).expect("validated fixed offset");
self.timestamp_utc.with_timezone(&off).naive_local()
}
TimeZoneSpec::Local => {
#[cfg(feature = "system-clock")]
{
self.timestamp_utc.with_timezone(&Local).naive_local()
}
#[cfg(not(feature = "system-clock"))]
{
self.timestamp_utc.naive_utc()
}
}
}
}
}
impl ClockProvider for FixedClock {
fn timezone(&self) -> &TimeZoneSpec {
&self.timezone
}
fn now(&self) -> NaiveDateTime {
self.now_in_timezone()
}
}
#[derive(Debug)]
pub struct SnapshotClock {
inner: std::sync::Arc<dyn ClockProvider>,
timezone: TimeZoneSpec,
sample: std::sync::RwLock<NaiveDateTime>,
}
impl SnapshotClock {
pub fn new(inner: std::sync::Arc<dyn ClockProvider>) -> Self {
let timezone = inner.timezone().clone();
let sample = inner.now();
Self {
inner,
timezone,
sample: std::sync::RwLock::new(sample),
}
}
pub fn refresh(&self) {
let now = self.inner.now();
*self.sample.write().expect("clock sample lock poisoned") = now;
}
}
impl ClockProvider for SnapshotClock {
fn timezone(&self) -> &TimeZoneSpec {
&self.timezone
}
fn now(&self) -> NaiveDateTime {
*self.sample.read().expect("clock sample lock poisoned")
}
}