autumn_web/time.rs
1//! Deterministic, injectable wall-clock time.
2//!
3//! Autumn exposes a [`Clock`] extractor so handlers can read the current time
4//! through the framework's injected clock instead of calling
5//! [`chrono::Utc::now`] directly. In tests, replace the clock with
6//! [`FixedClock`] or [`TickingClock`] via [`crate::test::TestApp::with_clock`]
7//! to control time without sleeping.
8//!
9//! # Quick example
10//!
11//! ```rust,no_run
12//! use autumn_web::prelude::*;
13//! use autumn_web::time::Clock;
14//!
15//! #[get("/token-age")]
16//! async fn token_age(clock: Clock) -> String {
17//! format!("now is {}", clock.now())
18//! }
19//! ```
20//!
21//! # Testing time-sensitive logic
22//!
23//! ```rust,no_run
24//! use autumn_web::prelude::*;
25//! use autumn_web::test::TestApp;
26//! use autumn_web::time::{Clock, TickingClock};
27//! use chrono::{TimeZone, Utc};
28//! use std::time::Duration;
29//!
30//! #[get("/token")]
31//! async fn check_token(clock: Clock) -> axum::http::StatusCode {
32//! let issued = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
33//! if clock.now() < issued + chrono::Duration::days(30) {
34//! axum::http::StatusCode::OK
35//! } else {
36//! axum::http::StatusCode::UNAUTHORIZED
37//! }
38//! }
39//!
40//! # #[tokio::main]
41//! # async fn main() {
42//! let issued = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
43//! let client = TestApp::new()
44//! .routes(routes![check_token])
45//! .with_clock(TickingClock::starting_at(issued))
46//! .build();
47//!
48//! client.get("/token").send().await.assert_status(200); // valid
49//! client.advance_clock(Duration::from_secs(30 * 24 * 3600)); // advance 30 days
50//! client.get("/token").send().await.assert_status(401); // expired
51//! # }
52//! ```
53
54use std::sync::{Arc, Mutex};
55
56use chrono::{DateTime, Utc};
57
58// ── Clock source trait ────────────────────────────────────────────────────────
59
60/// Source of the current wall-clock time used internally by the framework.
61///
62/// Production apps see [`SystemClock`] (the silent default). Tests swap it out
63/// via [`crate::test::TestApp::with_clock`].
64///
65/// Implement this trait to supply a custom clock (e.g. from an NTP client or a
66/// property-testing generator).
67pub trait ClockSource: Send + Sync + 'static {
68 /// Returns the current UTC instant.
69 fn now(&self) -> DateTime<Utc>;
70}
71
72// ── Extractor ─────────────────────────────────────────────────────────────────
73
74/// Axum extractor that resolves the current framework time.
75///
76/// Use as a handler argument to get the current time through the injected clock
77/// instead of calling [`chrono::Utc::now`] directly. This lets tests control
78/// time via [`crate::test::TestApp::with_clock`] and
79/// [`crate::test::TestClient::advance_clock`].
80///
81/// ```rust,ignore
82/// use autumn_web::time::Clock;
83///
84/// async fn handler(clock: Clock) -> String {
85/// format!("Current time: {}", clock.now())
86/// }
87/// ```
88#[derive(Debug, Clone, Copy)]
89pub struct Clock(DateTime<Utc>);
90
91impl Clock {
92 /// Returns the UTC instant captured when this extractor was resolved.
93 #[must_use]
94 pub const fn now(&self) -> DateTime<Utc> {
95 self.0
96 }
97}
98
99impl std::ops::Deref for Clock {
100 type Target = DateTime<Utc>;
101
102 fn deref(&self) -> &Self::Target {
103 &self.0
104 }
105}
106
107impl axum::extract::FromRequestParts<crate::state::AppState> for Clock {
108 type Rejection = std::convert::Infallible;
109
110 async fn from_request_parts(
111 _parts: &mut axum::http::request::Parts,
112 state: &crate::state::AppState,
113 ) -> Result<Self, Self::Rejection> {
114 Ok(Self(state.clock().now()))
115 }
116}
117
118// ── System (real) clock ───────────────────────────────────────────────────────
119
120/// Real wall-clock implementation of [`ClockSource`].
121///
122/// This is the default when no custom clock is configured. It delegates to
123/// [`chrono::Utc::now`] and carries zero overhead compared to calling
124/// `Utc::now()` directly.
125#[derive(Debug, Clone, Copy, Default)]
126pub struct SystemClock;
127
128impl ClockSource for SystemClock {
129 fn now(&self) -> DateTime<Utc> {
130 Utc::now()
131 }
132}
133
134// ── Fixed clock ───────────────────────────────────────────────────────────────
135
136/// A test clock that stays pinned to a fixed point in time.
137///
138/// Every call to [`ClockSource::now`] returns the same instant. Use when you
139/// need a stable reference time but don't need [`crate::test::TestClient::advance_clock`].
140///
141/// Calling `advance_clock` when this clock is active is a safe no-op.
142///
143/// ```rust,ignore
144/// use autumn_web::time::FixedClock;
145/// use chrono::{TimeZone, Utc};
146///
147/// let clock = FixedClock::at(Utc.with_ymd_and_hms(2025, 6, 1, 0, 0, 0).unwrap());
148/// ```
149#[derive(Debug, Clone, Copy)]
150pub struct FixedClock(DateTime<Utc>);
151
152impl FixedClock {
153 /// Create a clock pinned to `dt`.
154 #[must_use]
155 pub const fn at(dt: DateTime<Utc>) -> Self {
156 Self(dt)
157 }
158}
159
160impl ClockSource for FixedClock {
161 fn now(&self) -> DateTime<Utc> {
162 self.0
163 }
164}
165
166// ── Ticking clock ─────────────────────────────────────────────────────────────
167
168/// A test clock that starts at a given time and can be stepped forward.
169///
170/// Cloning produces a handle that shares the same internal instant — a clone
171/// passed to [`crate::test::TestApp::with_clock`] and a clone kept by the test
172/// both observe the same time.
173///
174/// Advance the clock between requests via
175/// [`crate::test::TestClient::advance_clock`]:
176///
177/// ```rust,ignore
178/// use autumn_web::time::TickingClock;
179/// use chrono::{TimeZone, Utc};
180/// use std::time::Duration;
181///
182/// let clock = TickingClock::starting_at(Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap());
183/// let client = TestApp::new().with_clock(clock.clone()).build();
184///
185/// client.advance_clock(Duration::from_secs(3600)); // advance 1 hour
186/// ```
187#[derive(Clone, Debug)]
188pub struct TickingClock(Arc<Mutex<DateTime<Utc>>>);
189
190impl TickingClock {
191 /// Create a ticking clock starting at `dt`.
192 #[must_use]
193 pub fn starting_at(dt: DateTime<Utc>) -> Self {
194 Self(Arc::new(Mutex::new(dt)))
195 }
196
197 /// Step this clock forward by `duration`.
198 ///
199 /// Sub-millisecond durations are truncated to zero (chrono's minimum resolution
200 /// is microseconds). This method never panics.
201 pub fn advance(&self, duration: std::time::Duration) {
202 let mut guard = self
203 .0
204 .lock()
205 .unwrap_or_else(std::sync::PoisonError::into_inner);
206 if let Ok(delta) = chrono::Duration::from_std(duration) {
207 *guard += delta;
208 }
209 }
210}
211
212impl ClockSource for TickingClock {
213 fn now(&self) -> DateTime<Utc> {
214 *self
215 .0
216 .lock()
217 .unwrap_or_else(std::sync::PoisonError::into_inner)
218 }
219}
220
221// ── Helpers for internal framework code ──────────────────────────────────────
222
223/// Compute the current Unix timestamp in seconds from the given clock.
224///
225/// Used by scheduler and storage internals instead of
226/// `SystemTime::now().duration_since(UNIX_EPOCH)`.
227#[must_use]
228pub fn clock_unix_secs(clock: &dyn ClockSource) -> u64 {
229 clock_unix_duration(clock).as_secs()
230}
231
232/// Compute the elapsed duration since the Unix epoch from the given clock.
233#[must_use]
234pub fn clock_unix_duration(clock: &dyn ClockSource) -> std::time::Duration {
235 let now = clock.now();
236 let ts = now.timestamp();
237 if ts >= 0 {
238 std::time::Duration::new(ts.cast_unsigned(), now.timestamp_subsec_nanos())
239 } else {
240 std::time::Duration::ZERO
241 }
242}
243
244// ── Module-level unit tests ───────────────────────────────────────────────────
245
246#[cfg(test)]
247mod tests {
248 use super::*;
249 use chrono::TimeZone;
250
251 #[test]
252 fn system_clock_returns_time_close_to_utc_now() {
253 let clock = SystemClock;
254 let a = clock.now();
255 let b = Utc::now();
256 assert!(
257 (b - a).num_seconds().abs() < 1,
258 "SystemClock should be within 1s of Utc::now()"
259 );
260 }
261
262 #[test]
263 fn fixed_clock_always_returns_same_time() {
264 let pinned = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
265 let clock = FixedClock::at(pinned);
266 assert_eq!(clock.now(), pinned);
267 assert_eq!(clock.now(), pinned);
268 }
269
270 #[test]
271 fn ticking_clock_starts_at_given_time() {
272 let start = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
273 let clock = TickingClock::starting_at(start);
274 assert_eq!(clock.now(), start);
275 }
276
277 #[test]
278 fn ticking_clock_advances_correctly() {
279 let start = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
280 let clock = TickingClock::starting_at(start);
281 clock.advance(std::time::Duration::from_secs(3600));
282 assert_eq!(clock.now(), start + chrono::Duration::hours(1));
283 }
284
285 #[test]
286 fn ticking_clock_clone_shares_state() {
287 let start = Utc.with_ymd_and_hms(2025, 6, 1, 12, 0, 0).unwrap();
288 let clock = TickingClock::starting_at(start);
289 let clone = clock.clone();
290
291 clock.advance(std::time::Duration::from_secs(86400));
292 assert_eq!(clone.now(), start + chrono::Duration::days(1));
293 }
294
295 #[test]
296 fn clock_unix_secs_uses_clock_timestamp() {
297 let pinned = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
298 let clock = FixedClock::at(pinned);
299 let secs = clock_unix_secs(&clock);
300 assert_eq!(secs, pinned.timestamp().cast_unsigned());
301 }
302
303 #[test]
304 fn clock_unix_duration_zero_for_pre_epoch() {
305 // Chrono timestamps before the epoch should not underflow.
306 let pre_epoch = Utc.with_ymd_and_hms(1969, 12, 31, 23, 59, 59).unwrap();
307 let clock = FixedClock::at(pre_epoch);
308 assert_eq!(clock_unix_duration(&clock), std::time::Duration::ZERO);
309 }
310}