1use axum::{
4 extract::Path,
5 http::StatusCode,
6 response::{IntoResponse, Json},
7};
8use chrono::{DateTime, Duration, Utc};
9use mockforge_core::{RepeatConfig, ScheduledResponse, TimeTravelManager, VirtualClock};
10use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12use std::sync::{Arc, RwLock};
13use tracing::info;
14
15static TIME_TRAVEL_MANAGER: once_cell::sync::OnceCell<Arc<RwLock<Option<Arc<TimeTravelManager>>>>> =
17 once_cell::sync::OnceCell::new();
18
19pub fn init_time_travel_manager(manager: Arc<TimeTravelManager>) {
21 let cell = TIME_TRAVEL_MANAGER.get_or_init(|| Arc::new(RwLock::new(None)));
22 let mut guard = cell.write().unwrap();
23 *guard = Some(manager);
24}
25
26fn get_time_travel_manager() -> Option<Arc<TimeTravelManager>> {
28 TIME_TRAVEL_MANAGER.get().and_then(|cell| cell.read().unwrap().clone())
29}
30
31#[derive(Debug, Serialize, Deserialize)]
33pub struct EnableTimeTravelRequest {
34 pub time: Option<DateTime<Utc>>,
36 pub scale: Option<f64>,
38}
39
40#[derive(Debug, Serialize, Deserialize)]
42pub struct AdvanceTimeRequest {
43 pub duration: String,
45}
46
47#[derive(Debug, Serialize, Deserialize)]
49pub struct SetScaleRequest {
50 pub scale: f64,
52}
53
54#[derive(Debug, Serialize, Deserialize)]
56pub struct ScheduleResponseRequest {
57 pub trigger_time: String,
59 pub body: serde_json::Value,
61 #[serde(default = "default_status")]
63 pub status: u16,
64 #[serde(default)]
66 pub headers: HashMap<String, String>,
67 pub name: Option<String>,
69 pub repeat: Option<RepeatConfig>,
71}
72
73fn default_status() -> u16 {
74 200
75}
76
77#[derive(Debug, Serialize, Deserialize)]
79pub struct ScheduleResponseResponse {
80 pub id: String,
81 pub trigger_time: DateTime<Utc>,
82}
83
84pub async fn get_time_travel_status() -> impl IntoResponse {
86 match get_time_travel_manager() {
87 Some(manager) => {
88 let status = manager.clock().status();
89 Json(status).into_response()
90 }
91 None => (
92 StatusCode::NOT_FOUND,
93 Json(serde_json::json!({
94 "error": "Time travel not initialized"
95 })),
96 )
97 .into_response(),
98 }
99}
100
101pub async fn enable_time_travel(Json(req): Json<EnableTimeTravelRequest>) -> impl IntoResponse {
103 match get_time_travel_manager() {
104 Some(manager) => {
105 let time = req.time.unwrap_or_else(Utc::now);
106 manager.clock().enable_and_set(time);
107
108 if let Some(scale) = req.scale {
109 manager.clock().set_scale(scale);
110 }
111
112 info!("Time travel enabled at {}", time);
113
114 Json(serde_json::json!({
115 "success": true,
116 "status": manager.clock().status()
117 }))
118 .into_response()
119 }
120 None => (
121 StatusCode::NOT_FOUND,
122 Json(serde_json::json!({
123 "error": "Time travel not initialized"
124 })),
125 )
126 .into_response(),
127 }
128}
129
130pub async fn disable_time_travel() -> impl IntoResponse {
132 match get_time_travel_manager() {
133 Some(manager) => {
134 manager.clock().disable();
135 info!("Time travel disabled");
136
137 Json(serde_json::json!({
138 "success": true,
139 "status": manager.clock().status()
140 }))
141 .into_response()
142 }
143 None => (
144 StatusCode::NOT_FOUND,
145 Json(serde_json::json!({
146 "error": "Time travel not initialized"
147 })),
148 )
149 .into_response(),
150 }
151}
152
153pub async fn advance_time(Json(req): Json<AdvanceTimeRequest>) -> impl IntoResponse {
155 match get_time_travel_manager() {
156 Some(manager) => {
157 let duration = parse_duration(&req.duration);
159
160 match duration {
161 Ok(dur) => {
162 manager.clock().advance(dur);
163 info!("Time advanced by {}", req.duration);
164
165 Json(serde_json::json!({
166 "success": true,
167 "status": manager.clock().status()
168 }))
169 .into_response()
170 }
171 Err(e) => (
172 StatusCode::BAD_REQUEST,
173 Json(serde_json::json!({
174 "error": format!("Invalid duration format: {}", e)
175 })),
176 )
177 .into_response(),
178 }
179 }
180 None => (
181 StatusCode::NOT_FOUND,
182 Json(serde_json::json!({
183 "error": "Time travel not initialized"
184 })),
185 )
186 .into_response(),
187 }
188}
189
190pub async fn set_time_scale(Json(req): Json<SetScaleRequest>) -> impl IntoResponse {
192 match get_time_travel_manager() {
193 Some(manager) => {
194 manager.clock().set_scale(req.scale);
195 info!("Time scale set to {}x", req.scale);
196
197 Json(serde_json::json!({
198 "success": true,
199 "status": manager.clock().status()
200 }))
201 .into_response()
202 }
203 None => (
204 StatusCode::NOT_FOUND,
205 Json(serde_json::json!({
206 "error": "Time travel not initialized"
207 })),
208 )
209 .into_response(),
210 }
211}
212
213pub async fn reset_time_travel() -> impl IntoResponse {
215 match get_time_travel_manager() {
216 Some(manager) => {
217 manager.clock().reset();
218 info!("Time travel reset");
219
220 Json(serde_json::json!({
221 "success": true,
222 "status": manager.clock().status()
223 }))
224 .into_response()
225 }
226 None => (
227 StatusCode::NOT_FOUND,
228 Json(serde_json::json!({
229 "error": "Time travel not initialized"
230 })),
231 )
232 .into_response(),
233 }
234}
235
236pub async fn schedule_response(Json(req): Json<ScheduleResponseRequest>) -> impl IntoResponse {
238 match get_time_travel_manager() {
239 Some(manager) => {
240 let trigger_time = parse_trigger_time(&req.trigger_time, manager.clock());
242
243 match trigger_time {
244 Ok(time) => {
245 let scheduled_response = ScheduledResponse {
246 id: uuid::Uuid::new_v4().to_string(),
247 trigger_time: time,
248 body: req.body,
249 status: req.status,
250 headers: req.headers,
251 name: req.name,
252 repeat: req.repeat,
253 };
254
255 match manager.scheduler().schedule(scheduled_response.clone()) {
256 Ok(id) => {
257 info!("Scheduled response {} for {}", id, time);
258
259 Json(ScheduleResponseResponse {
260 id,
261 trigger_time: time,
262 })
263 .into_response()
264 }
265 Err(e) => (
266 StatusCode::INTERNAL_SERVER_ERROR,
267 Json(serde_json::json!({
268 "error": format!("Failed to schedule response: {}", e)
269 })),
270 )
271 .into_response(),
272 }
273 }
274 Err(e) => (
275 StatusCode::BAD_REQUEST,
276 Json(serde_json::json!({
277 "error": format!("Invalid trigger time: {}", e)
278 })),
279 )
280 .into_response(),
281 }
282 }
283 None => (
284 StatusCode::NOT_FOUND,
285 Json(serde_json::json!({
286 "error": "Time travel not initialized"
287 })),
288 )
289 .into_response(),
290 }
291}
292
293pub async fn list_scheduled_responses() -> impl IntoResponse {
295 match get_time_travel_manager() {
296 Some(manager) => {
297 let scheduled = manager.scheduler().list_scheduled();
298 Json(scheduled).into_response()
299 }
300 None => (
301 StatusCode::NOT_FOUND,
302 Json(serde_json::json!({
303 "error": "Time travel not initialized"
304 })),
305 )
306 .into_response(),
307 }
308}
309
310pub async fn cancel_scheduled_response(Path(id): Path<String>) -> impl IntoResponse {
312 match get_time_travel_manager() {
313 Some(manager) => {
314 let cancelled = manager.scheduler().cancel(&id);
315
316 if cancelled {
317 info!("Cancelled scheduled response {}", id);
318 Json(serde_json::json!({
319 "success": true
320 }))
321 .into_response()
322 } else {
323 (
324 StatusCode::NOT_FOUND,
325 Json(serde_json::json!({
326 "error": "Scheduled response not found"
327 })),
328 )
329 .into_response()
330 }
331 }
332 None => (
333 StatusCode::NOT_FOUND,
334 Json(serde_json::json!({
335 "error": "Time travel not initialized"
336 })),
337 )
338 .into_response(),
339 }
340}
341
342pub async fn clear_scheduled_responses() -> impl IntoResponse {
344 match get_time_travel_manager() {
345 Some(manager) => {
346 manager.scheduler().clear_all();
347 info!("Cleared all scheduled responses");
348
349 Json(serde_json::json!({
350 "success": true
351 }))
352 .into_response()
353 }
354 None => (
355 StatusCode::NOT_FOUND,
356 Json(serde_json::json!({
357 "error": "Time travel not initialized"
358 })),
359 )
360 .into_response(),
361 }
362}
363
364fn parse_duration(s: &str) -> Result<Duration, String> {
366 let s = s.trim();
367 if s.is_empty() {
368 return Err("Empty duration string".to_string());
369 }
370
371 let (num_str, unit) = if let Some(pos) = s.chars().position(|c| !c.is_numeric()) {
373 (&s[..pos], &s[pos..])
374 } else {
375 return Err("No unit specified (use s, m, h, or d)".to_string());
376 };
377
378 let amount: i64 = num_str.parse().map_err(|e| format!("Invalid number: {}", e))?;
379
380 match unit {
381 "s" => Ok(Duration::seconds(amount)),
382 "m" => Ok(Duration::minutes(amount)),
383 "h" => Ok(Duration::hours(amount)),
384 "d" => Ok(Duration::days(amount)),
385 _ => Err(format!("Unknown unit: {}", unit)),
386 }
387}
388
389fn parse_trigger_time(s: &str, clock: Arc<VirtualClock>) -> Result<DateTime<Utc>, String> {
391 let s = s.trim();
392
393 if s.starts_with('+') || s.starts_with('-') {
395 let duration = parse_duration(&s[1..])?;
396 let current = clock.now();
397
398 if s.starts_with('+') {
399 Ok(current + duration)
400 } else {
401 Ok(current - duration)
402 }
403 } else {
404 DateTime::parse_from_rfc3339(s)
406 .map(|dt| dt.with_timezone(&Utc))
407 .map_err(|e| format!("Invalid ISO 8601 date: {}", e))
408 }
409}
410
411#[cfg(test)]
412mod tests {
413 use super::*;
414
415 #[test]
416 fn test_parse_duration() {
417 assert_eq!(parse_duration("10s").unwrap(), Duration::seconds(10));
418 assert_eq!(parse_duration("30m").unwrap(), Duration::minutes(30));
419 assert_eq!(parse_duration("2h").unwrap(), Duration::hours(2));
420 assert_eq!(parse_duration("1d").unwrap(), Duration::days(1));
421
422 assert!(parse_duration("").is_err());
423 assert!(parse_duration("10").is_err());
424 assert!(parse_duration("10x").is_err());
425 }
426
427 #[test]
428 fn test_parse_trigger_time_relative() {
429 let clock = Arc::new(VirtualClock::new());
430 let now = Utc::now();
431 clock.enable_and_set(now);
432
433 let future = parse_trigger_time("+1h", clock.clone()).unwrap();
434 assert!((future - now - Duration::hours(1)).num_seconds().abs() < 1);
435
436 let past = parse_trigger_time("-30m", clock.clone()).unwrap();
437 assert!((past - now + Duration::minutes(30)).num_seconds().abs() < 1);
438 }
439}