1use axum::extract::Json as AxumJson;
30use axum::http::StatusCode;
31use axum::response::{IntoResponse, Response};
32use axum::routing::{get, post};
33use axum::{Json, Router};
34use chrono::{DateTime, Duration, Utc};
35use mockforge_core::TimeTravelManager;
36use serde::{Deserialize, Serialize};
37use std::sync::Arc;
38use std::sync::OnceLock;
39
40static MANAGER: OnceLock<Arc<TimeTravelManager>> = OnceLock::new();
44
45pub fn init_time_travel_manager(manager: Arc<TimeTravelManager>) {
48 let _ = MANAGER.set(manager);
49}
50
51fn manager() -> Option<Arc<TimeTravelManager>> {
52 MANAGER.get().cloned()
53}
54
55#[derive(Debug, Deserialize)]
56struct EnableRequest {
57 #[serde(default)]
59 time: Option<DateTime<Utc>>,
60 #[serde(default)]
62 scale: Option<f64>,
63}
64
65#[derive(Debug, Deserialize)]
66struct AdvanceRequest {
67 duration: String,
69}
70
71#[derive(Debug, Deserialize)]
72struct SetTimeRequest {
73 time: DateTime<Utc>,
74}
75
76#[derive(Debug, Deserialize)]
77struct ScaleRequest {
78 scale: f64,
79}
80
81fn not_initialised() -> Response {
82 (
83 StatusCode::NOT_FOUND,
84 Json(serde_json::json!({
85 "error": "time_travel_not_initialised",
86 "message": "TimeTravelManager hasn't been registered on this server",
87 })),
88 )
89 .into_response()
90}
91
92#[derive(Debug, Serialize)]
93struct OkResponse<S> {
94 success: bool,
95 status: S,
96}
97
98async fn status_handler() -> Response {
99 let Some(m) = manager() else {
100 return not_initialised();
101 };
102 Json(m.clock().status()).into_response()
103}
104
105async fn enable_handler(AxumJson(req): AxumJson<EnableRequest>) -> Response {
106 let Some(m) = manager() else {
107 return not_initialised();
108 };
109 let time = req.time.unwrap_or_else(Utc::now);
110 m.enable_and_set(time);
111 if let Some(scale) = req.scale {
112 m.set_scale(scale);
113 }
114 Json(OkResponse {
115 success: true,
116 status: m.clock().status(),
117 })
118 .into_response()
119}
120
121async fn disable_handler() -> Response {
122 let Some(m) = manager() else {
123 return not_initialised();
124 };
125 m.disable();
126 Json(OkResponse {
127 success: true,
128 status: m.clock().status(),
129 })
130 .into_response()
131}
132
133async fn advance_handler(AxumJson(req): AxumJson<AdvanceRequest>) -> Response {
134 let Some(m) = manager() else {
135 return not_initialised();
136 };
137 match parse_duration(&req.duration) {
138 Ok(dur) => {
139 m.advance(dur);
140 Json(OkResponse {
141 success: true,
142 status: m.clock().status(),
143 })
144 .into_response()
145 }
146 Err(e) => (
147 StatusCode::BAD_REQUEST,
148 Json(serde_json::json!({
149 "error": "invalid_duration",
150 "message": e,
151 })),
152 )
153 .into_response(),
154 }
155}
156
157async fn set_handler(AxumJson(req): AxumJson<SetTimeRequest>) -> Response {
158 let Some(m) = manager() else {
159 return not_initialised();
160 };
161 m.clock().set_time(req.time);
162 Json(OkResponse {
163 success: true,
164 status: m.clock().status(),
165 })
166 .into_response()
167}
168
169async fn scale_handler(AxumJson(req): AxumJson<ScaleRequest>) -> Response {
170 let Some(m) = manager() else {
171 return not_initialised();
172 };
173 m.set_scale(req.scale);
174 Json(OkResponse {
175 success: true,
176 status: m.clock().status(),
177 })
178 .into_response()
179}
180
181async fn reset_handler() -> Response {
182 let Some(m) = manager() else {
183 return not_initialised();
184 };
185 m.clock().reset();
186 Json(OkResponse {
187 success: true,
188 status: m.clock().status(),
189 })
190 .into_response()
191}
192
193fn parse_duration(s: &str) -> Result<Duration, String> {
197 let s = s.trim().trim_start_matches('+').trim_start_matches('-');
198 if s.is_empty() {
199 return Err("empty duration".to_string());
200 }
201
202 type DurationCtor = fn(i64) -> Duration;
205 let units: &[(&str, DurationCtor)] = &[
206 ("weeks", |n| Duration::days(n * 7)),
207 ("week", |n| Duration::days(n * 7)),
208 ("days", Duration::days),
209 ("day", Duration::days),
210 ("hours", Duration::hours),
211 ("hour", Duration::hours),
212 ("minutes", Duration::minutes),
213 ("minute", Duration::minutes),
214 ("seconds", Duration::seconds),
215 ("second", Duration::seconds),
216 ("ms", Duration::milliseconds),
217 ("d", Duration::days),
218 ("h", Duration::hours),
219 ("m", Duration::minutes),
220 ("s", Duration::seconds),
221 ];
222
223 for (suffix, ctor) in units {
224 if let Some(num_str) = s.strip_suffix(suffix) {
225 let num_str = num_str.trim();
226 let n: i64 =
227 num_str.parse().map_err(|e| format!("invalid number '{}': {}", num_str, e))?;
228 return Ok(ctor(n));
229 }
230 }
231
232 Err(format!("unknown duration suffix in '{}'; expected w/d/h/m/s/ms", s))
233}
234
235pub fn time_travel_router() -> Router {
237 Router::new()
238 .route("/status", get(status_handler))
239 .route("/enable", post(enable_handler))
240 .route("/disable", post(disable_handler))
241 .route("/advance", post(advance_handler))
242 .route("/set", post(set_handler))
243 .route("/scale", post(scale_handler))
244 .route("/reset", post(reset_handler))
245}
246
247#[cfg(test)]
248mod tests {
249 use super::*;
250
251 #[test]
252 fn parse_duration_basic_units() {
253 assert_eq!(parse_duration("30s").unwrap(), Duration::seconds(30));
254 assert_eq!(parse_duration("5m").unwrap(), Duration::minutes(5));
255 assert_eq!(parse_duration("2h").unwrap(), Duration::hours(2));
256 assert_eq!(parse_duration("3d").unwrap(), Duration::days(3));
257 }
258
259 #[test]
260 fn parse_duration_weeks() {
261 assert_eq!(parse_duration("1week").unwrap(), Duration::days(7));
262 assert_eq!(parse_duration("2weeks").unwrap(), Duration::days(14));
263 }
264
265 #[test]
266 fn parse_duration_milliseconds() {
267 assert_eq!(parse_duration("250ms").unwrap(), Duration::milliseconds(250));
268 }
269
270 #[test]
271 fn parse_duration_relative_prefix() {
272 assert_eq!(parse_duration("+1h").unwrap(), Duration::hours(1));
273 assert_eq!(parse_duration("-30m").unwrap(), Duration::minutes(30));
274 }
275
276 #[test]
277 fn parse_duration_rejects_empty() {
278 assert!(parse_duration("").is_err());
279 assert!(parse_duration(" ").is_err());
280 }
281
282 #[test]
283 fn parse_duration_rejects_unknown_suffix() {
284 assert!(parse_duration("5fortnights").is_err());
285 assert!(parse_duration("12").is_err());
286 }
287}