1use std::sync::Arc;
4use std::time::{Duration, SystemTime, UNIX_EPOCH};
5
6use chrono::{DateTime, Utc};
7
8pub type UtcInstant = DateTime<Utc>;
10
11#[derive(Debug, thiserror::Error)]
13#[non_exhaustive]
14pub enum ClockError {
15 #[error("clock unavailable: {reason}")]
17 Unavailable {
18 reason: String,
20 },
21 #[error("clock unavailable: instant is outside the supported time range")]
23 InstantOutOfRange {
24 #[source]
26 source: Option<std::num::TryFromIntError>,
27 },
28}
29
30pub trait Clock: std::fmt::Debug + Send + Sync {
32 fn now(&self) -> Result<DateTime<Utc>, ClockError>;
38
39 fn unix_seconds(&self) -> Result<u64, ClockError> {
45 unix_seconds(self.now()?)
46 }
47}
48
49#[derive(Debug, Default)]
54pub enum ClockSource {
55 #[default]
57 System,
58 Supplied(Arc<dyn Clock>),
60}
61
62impl Clock for ClockSource {
63 fn now(&self) -> Result<DateTime<Utc>, ClockError> {
64 match self {
65 Self::System => SystemClock.now(),
66 Self::Supplied(clock) => clock.now(),
67 }
68 }
69}
70
71#[derive(Debug, Clone, Copy, Default)]
73pub struct SystemClock;
74
75impl Clock for SystemClock {
76 fn now(&self) -> Result<DateTime<Utc>, ClockError> {
77 match SystemTime::now().duration_since(UNIX_EPOCH) {
78 Ok(duration) => utc_from_duration(duration, false),
79 Err(error) => utc_from_duration(error.duration(), true),
80 }
81 }
82}
83
84pub fn unix_seconds(instant: DateTime<Utc>) -> Result<u64, ClockError> {
90 u64::try_from(instant.timestamp()).map_err(|source| ClockError::InstantOutOfRange {
91 source: Some(source),
92 })
93}
94
95fn utc_from_duration(duration: Duration, before_epoch: bool) -> Result<DateTime<Utc>, ClockError> {
96 let seconds =
97 i64::try_from(duration.as_secs()).map_err(|source| ClockError::InstantOutOfRange {
98 source: Some(source),
99 })?;
100 let nanos = duration.subsec_nanos();
101 let (seconds, nanos) = if before_epoch {
102 let seconds = seconds
103 .checked_neg()
104 .ok_or(ClockError::InstantOutOfRange { source: None })?;
105 if nanos == 0 {
106 (seconds, 0)
107 } else {
108 (
109 seconds
110 .checked_sub(1)
111 .ok_or(ClockError::InstantOutOfRange { source: None })?,
112 1_000_000_000 - nanos,
113 )
114 }
115 } else {
116 (seconds, nanos)
117 };
118 DateTime::from_timestamp(seconds, nanos).ok_or(ClockError::InstantOutOfRange { source: None })
119}
120
121#[cfg(test)]
122mod tests {
123 use super::*;
124
125 #[test]
126 fn system_conversion_preserves_epoch_and_fractional_seconds() {
127 assert_eq!(
128 utc_from_duration(Duration::ZERO, false)
129 .unwrap()
130 .timestamp(),
131 0
132 );
133 let instant = utc_from_duration(Duration::new(7, 123_456_789), false).unwrap();
134 assert_eq!(instant.timestamp(), 7);
135 assert_eq!(instant.timestamp_subsec_nanos(), 123_456_789);
136 }
137
138 #[test]
139 fn system_conversion_preserves_negative_fractional_seconds() {
140 let instant = utc_from_duration(Duration::new(7, 123_456_789), true).unwrap();
141 assert_eq!(instant.timestamp(), -8);
142 assert_eq!(instant.timestamp_subsec_nanos(), 876_543_211);
143 assert_eq!(
144 utc_from_duration(Duration::from_secs(7), true)
145 .unwrap()
146 .timestamp(),
147 -7
148 );
149 assert!(matches!(
150 unix_seconds(instant),
151 Err(ClockError::InstantOutOfRange { .. })
152 ));
153 let error = unix_seconds(instant).unwrap_err();
154 assert!(
155 std::error::Error::source(&error)
156 .is_some_and(<dyn std::error::Error + 'static>::is::<std::num::TryFromIntError>)
157 );
158 }
159
160 #[test]
161 fn system_conversion_refuses_unrepresentable_seconds_without_zero() {
162 for before_epoch in [false, true] {
163 assert!(matches!(
164 utc_from_duration(Duration::MAX, before_epoch),
165 Err(ClockError::InstantOutOfRange { .. })
166 ));
167 let error = utc_from_duration(Duration::MAX, before_epoch).unwrap_err();
168 assert!(
169 std::error::Error::source(&error).is_some_and(
170 <dyn std::error::Error + 'static>::is::<std::num::TryFromIntError>
171 )
172 );
173 assert!(matches!(
174 utc_from_duration(Duration::from_secs(i64::MAX.unsigned_abs()), before_epoch),
175 Err(ClockError::InstantOutOfRange { .. })
176 ));
177 }
178 }
179}