1use axum::{
58 extract::{Path, Query, State},
59 http::StatusCode,
60 response::Json,
61 routing::get,
62 Router,
63};
64use mockforge_data::token_resolver::TokenResolver;
65use serde::{Deserialize, Serialize};
66use serde_json::{json, Value};
67use std::collections::HashMap;
68use std::sync::Arc;
69use tokio::sync::RwLock;
70use tracing::{info, warn};
71
72#[derive(Debug, Deserialize, Serialize)]
74pub struct ListQueryParams {
75 #[serde(default)]
77 page: Option<usize>,
78 #[serde(default)]
80 limit: Option<usize>,
81 #[serde(default)]
83 sort: Option<String>,
84 #[serde(flatten)]
86 filters: HashMap<String, String>,
87}
88
89impl Default for ListQueryParams {
90 fn default() -> Self {
91 Self {
92 page: Some(1),
93 limit: Some(50),
94 sort: None,
95 filters: HashMap::new(),
96 }
97 }
98}
99
100#[derive(Clone)]
102pub struct QuickMockState {
103 data: Arc<RwLock<HashMap<String, Vec<Value>>>>,
105 resolver: Arc<TokenResolver>,
107}
108
109impl QuickMockState {
110 pub fn new() -> Self {
112 Self {
113 data: Arc::new(RwLock::new(HashMap::new())),
114 resolver: Arc::new(TokenResolver::new()),
115 }
116 }
117
118 pub async fn from_json(json_data: Value) -> Result<Self, String> {
120 let state = Self::new();
121
122 if let Value::Object(obj) = json_data {
123 let mut data = state.data.write().await;
124
125 for (key, value) in obj {
126 if let Value::Array(arr) = value {
128 let mut resolved_items = Vec::new();
130 for item in arr {
131 match state.resolver.resolve(&item).await {
132 Ok(resolved) => resolved_items.push(resolved),
133 Err(e) => {
134 warn!("Failed to resolve tokens in {}: {}", key, e)
135 }
136 }
137 }
138 data.insert(key, resolved_items);
139 } else {
140 match state.resolver.resolve(&value).await {
142 Ok(resolved) => {
143 data.insert(key, vec![resolved]);
144 }
145 Err(e) => warn!("Failed to resolve tokens in {}: {}", key, e),
146 }
147 }
148 }
149 }
150
151 Ok(state)
152 }
153
154 pub async fn resource_names(&self) -> Vec<String> {
156 let data = self.data.read().await;
157 data.keys().cloned().collect()
158 }
159}
160
161impl Default for QuickMockState {
162 fn default() -> Self {
163 Self::new()
164 }
165}
166
167pub async fn build_quick_router(state: QuickMockState) -> Router {
169 let resource_names = state.resource_names().await;
170 let mut router = Router::new();
171
172 for resource in resource_names {
173 let resource_router = Router::new()
175 .route(
176 "/",
177 get({
178 let resource = resource.clone();
179 move |State(state): State<QuickMockState>,
180 Query(params): Query<ListQueryParams>| {
181 let resource = resource.clone();
182 async move { list_handler_impl(state, resource, params).await }
183 }
184 })
185 .post({
186 let resource = resource.clone();
187 move |State(state): State<QuickMockState>, Json(payload): Json<Value>| {
188 let resource = resource.clone();
189 async move { create_handler_impl(state, resource, payload).await }
190 }
191 }),
192 )
193 .route(
194 "/{id}",
195 get({
196 let resource = resource.clone();
197 move |State(state): State<QuickMockState>, Path(id): Path<String>| {
198 let resource = resource.clone();
199 async move { get_handler_impl(state, resource, id).await }
200 }
201 })
202 .put({
203 let resource = resource.clone();
204 move |State(state): State<QuickMockState>,
205 Path(id): Path<String>,
206 Json(payload): Json<Value>| {
207 let resource = resource.clone();
208 async move { update_handler_impl(state, resource, id, payload).await }
209 }
210 })
211 .delete({
212 let resource = resource.clone();
213 move |State(state): State<QuickMockState>, Path(id): Path<String>| {
214 let resource = resource.clone();
215 async move { delete_handler_impl(state, resource, id).await }
216 }
217 }),
218 );
219
220 router = router.nest(&format!("/{}", resource), resource_router);
221
222 info!("Registered routes for /{}", resource);
223 }
224
225 router = router.route("/__quick/info", get(info_handler));
227
228 router.with_state(state)
229}
230
231async fn list_handler_impl(
233 state: QuickMockState,
234 resource: String,
235 params: ListQueryParams,
236) -> Result<Json<Value>, StatusCode> {
237 let data = state.data.read().await;
238
239 if let Some(items) = data.get(&resource) {
240 let mut filtered_items: Vec<&Value> = items.iter().collect();
241
242 for (key, value) in ¶ms.filters {
244 if key == "page" || key == "limit" || key == "sort" {
246 continue;
247 }
248
249 filtered_items.retain(|item| {
250 if let Some(field_value) = item.get(key) {
251 match field_value {
253 Value::String(s) => s.contains(value),
254 Value::Number(n) => n.to_string() == *value,
255 Value::Bool(b) => b.to_string() == *value,
256 _ => false,
257 }
258 } else {
259 false
260 }
261 });
262 }
263
264 if let Some(sort) = ¶ms.sort {
266 let parts: Vec<&str> = sort.split(':').collect();
267 let field = parts.first().unwrap_or(&"id");
268 let direction = parts.get(1).unwrap_or(&"asc");
269
270 filtered_items.sort_by(|a, b| {
271 let a_val = a.get(*field);
272 let b_val = b.get(*field);
273
274 let cmp = match (a_val, b_val) {
275 (Some(Value::String(a)), Some(Value::String(b))) => a.cmp(b),
276 (Some(Value::Number(a)), Some(Value::Number(b))) => {
277 if let (Some(a_f), Some(b_f)) = (a.as_f64(), b.as_f64()) {
278 a_f.partial_cmp(&b_f).unwrap_or(std::cmp::Ordering::Equal)
279 } else {
280 std::cmp::Ordering::Equal
281 }
282 }
283 (Some(Value::Bool(a)), Some(Value::Bool(b))) => a.cmp(b),
284 _ => std::cmp::Ordering::Equal,
285 };
286
287 if *direction == "desc" {
288 cmp.reverse()
289 } else {
290 cmp
291 }
292 });
293 }
294
295 let total = filtered_items.len();
297
298 let page = params.page.unwrap_or(1).max(1);
300 let limit = params.limit.unwrap_or(50).clamp(1, 1000);
301 let offset = (page - 1) * limit;
302
303 let paginated_items: Vec<Value> =
304 filtered_items.into_iter().skip(offset).take(limit).cloned().collect();
305
306 let total_pages = total.div_ceil(limit);
307
308 Ok(Json(json!({
309 "data": paginated_items,
310 "pagination": {
311 "page": page,
312 "limit": limit,
313 "total": total,
314 "totalPages": total_pages,
315 "hasNext": page < total_pages,
316 "hasPrev": page > 1
317 }
318 })))
319 } else {
320 Err(StatusCode::NOT_FOUND)
321 }
322}
323
324async fn get_handler_impl(
326 state: QuickMockState,
327 resource: String,
328 id: String,
329) -> Result<Json<Value>, StatusCode> {
330 let data = state.data.read().await;
331
332 if let Some(items) = data.get(&resource) {
333 for item in items {
335 if let Some(item_id) = item.get("id") {
336 let matches = match item_id {
338 Value::String(s) => s == &id,
339 Value::Number(n) => n.to_string() == id,
340 _ => false,
341 };
342
343 if matches {
344 return Ok(Json(item.clone()));
345 }
346 }
347 }
348
349 if let Ok(index) = id.parse::<usize>() {
351 if let Some(item) = items.get(index) {
352 return Ok(Json(item.clone()));
353 }
354 }
355
356 Err(StatusCode::NOT_FOUND)
357 } else {
358 Err(StatusCode::NOT_FOUND)
359 }
360}
361
362async fn create_handler_impl(
364 state: QuickMockState,
365 resource: String,
366 mut payload: Value,
367) -> Result<(StatusCode, Json<Value>), StatusCode> {
368 payload = state
370 .resolver
371 .resolve(&payload)
372 .await
373 .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
374
375 let mut data = state.data.write().await;
376
377 if let Some(items) = data.get_mut(&resource) {
378 if payload.get("id").is_none() {
380 let new_id = items.len() + 1;
381 if let Value::Object(obj) = &mut payload {
382 obj.insert("id".to_string(), json!(new_id));
383 }
384 }
385
386 items.push(payload.clone());
387 Ok((StatusCode::CREATED, Json(payload)))
388 } else {
389 let mut new_payload = payload.clone();
391 if new_payload.get("id").is_none() {
392 if let Value::Object(obj) = &mut new_payload {
393 obj.insert("id".to_string(), json!(1));
394 }
395 }
396
397 data.insert(resource, vec![new_payload.clone()]);
398 Ok((StatusCode::CREATED, Json(new_payload)))
399 }
400}
401
402async fn update_handler_impl(
404 state: QuickMockState,
405 resource: String,
406 id: String,
407 mut payload: Value,
408) -> Result<Json<Value>, StatusCode> {
409 payload = state
411 .resolver
412 .resolve(&payload)
413 .await
414 .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
415
416 let mut data = state.data.write().await;
417
418 if let Some(items) = data.get_mut(&resource) {
419 for item in items.iter_mut() {
421 if let Some(item_id) = item.get("id") {
422 let matches = match item_id {
423 Value::String(s) => s == &id,
424 Value::Number(n) => n.to_string() == id,
425 _ => false,
426 };
427
428 if matches {
429 *item = payload.clone();
430 return Ok(Json(payload));
431 }
432 }
433 }
434
435 if let Ok(index) = id.parse::<usize>() {
437 if let Some(item) = items.get_mut(index) {
438 *item = payload.clone();
439 return Ok(Json(payload));
440 }
441 }
442
443 Err(StatusCode::NOT_FOUND)
444 } else {
445 Err(StatusCode::NOT_FOUND)
446 }
447}
448
449async fn delete_handler_impl(
451 state: QuickMockState,
452 resource: String,
453 id: String,
454) -> Result<StatusCode, StatusCode> {
455 let mut data = state.data.write().await;
456
457 if let Some(items) = data.get_mut(&resource) {
458 let original_len = items.len();
460 items.retain(|item| {
461 if let Some(item_id) = item.get("id") {
462 let matches = match item_id {
463 Value::String(s) => s == &id,
464 Value::Number(n) => n.to_string() == id,
465 _ => false,
466 };
467 !matches
468 } else {
469 true
470 }
471 });
472
473 if items.len() < original_len {
474 return Ok(StatusCode::NO_CONTENT);
475 }
476
477 if let Ok(index) = id.parse::<usize>() {
479 if index < items.len() {
480 items.remove(index);
481 return Ok(StatusCode::NO_CONTENT);
482 }
483 }
484
485 Err(StatusCode::NOT_FOUND)
486 } else {
487 Err(StatusCode::NOT_FOUND)
488 }
489}
490
491async fn info_handler(State(state): State<QuickMockState>) -> Json<Value> {
493 let data = state.data.read().await;
494 let mut resources = HashMap::new();
495
496 for (name, items) in data.iter() {
497 resources.insert(
498 name.clone(),
499 json!({
500 "count": items.len(),
501 "endpoints": {
502 "list": format!("GET /{}", name),
503 "get": format!("GET /{}/:id or GET /{}/{{id}}", name, name),
504 "create": format!("POST /{}", name),
505 "update": format!("PUT /{}/:id or PUT /{}/{{id}}", name, name),
506 "delete": format!("DELETE /{}/:id or DELETE /{}/{{id}}", name, name),
507 },
508 "queryParams": {
509 "page": "Page number (1-indexed, default: 1)",
510 "limit": "Items per page (1-1000, default: 50)",
511 "sort": "Sort field and direction (e.g., name:asc, id:desc)",
512 "filters": "Any field can be used as filter (e.g., ?role=admin&status=active)"
513 },
514 "examples": {
515 "pagination": format!("GET /{}?page=2&limit=10", name),
516 "filtering": format!("GET /{}?name=Alice", name),
517 "sorting": format!("GET /{}?sort=name:asc", name),
518 "combined": format!("GET /{}?role=admin&sort=name:desc&page=1&limit=20", name)
519 }
520 }),
521 );
522 }
523
524 Json(json!({
525 "mode": "quick",
526 "version": "1.1.0",
527 "features": [
528 "CRUD operations",
529 "Pagination",
530 "Filtering",
531 "Sorting",
532 "Dynamic token resolution ($random, $faker, $ai)"
533 ],
534 "resources": resources,
535 "info": "/__quick/info (this endpoint)"
536 }))
537}
538
539#[cfg(test)]
540mod tests {
541 use super::*;
542 use axum::body::Body;
543 use axum::http::{Request, StatusCode};
544 use tower::ServiceExt;
545
546 #[tokio::test]
547 async fn test_quick_mock_from_json() {
548 let json_data = json!({
549 "users": [
550 {"id": 1, "name": "Alice"},
551 {"id": 2, "name": "Bob"}
552 ],
553 "posts": [
554 {"id": 1, "title": "First Post"}
555 ]
556 });
557
558 let state = QuickMockState::from_json(json_data).await.unwrap();
559 let resource_names = state.resource_names().await;
560
561 assert_eq!(resource_names.len(), 2);
562 assert!(resource_names.contains(&"users".to_string()));
563 assert!(resource_names.contains(&"posts".to_string()));
564 }
565
566 #[tokio::test]
567 async fn test_list_handler() {
568 let json_data = json!({
569 "users": [
570 {"id": 1, "name": "Alice"},
571 {"id": 2, "name": "Bob"}
572 ]
573 });
574
575 let state = QuickMockState::from_json(json_data).await.unwrap();
576 let router = build_quick_router(state).await;
577
578 let response = router
579 .oneshot(Request::builder().uri("/users").body(Body::empty()).unwrap())
580 .await
581 .unwrap();
582
583 assert_eq!(response.status(), StatusCode::OK);
584 }
585
586 #[tokio::test]
587 async fn test_create_handler() {
588 let json_data = json!({
589 "users": []
590 });
591
592 let state = QuickMockState::from_json(json_data).await.unwrap();
593 let router = build_quick_router(state).await;
594
595 let response = router
596 .oneshot(
597 Request::builder()
598 .method("POST")
599 .uri("/users")
600 .header("content-type", "application/json")
601 .body(Body::from(r#"{"name":"Charlie"}"#))
602 .unwrap(),
603 )
604 .await
605 .unwrap();
606
607 assert_eq!(response.status(), StatusCode::CREATED);
608 }
609
610 #[tokio::test]
611 async fn test_pagination() {
612 let json_data = json!({
613 "users": [
614 {"id": 1, "name": "Alice"},
615 {"id": 2, "name": "Bob"},
616 {"id": 3, "name": "Charlie"},
617 {"id": 4, "name": "David"},
618 {"id": 5, "name": "Eve"}
619 ]
620 });
621
622 let state = QuickMockState::from_json(json_data).await.unwrap();
623 let router = build_quick_router(state).await;
624
625 let response = router
627 .clone()
628 .oneshot(Request::builder().uri("/users?page=1&limit=2").body(Body::empty()).unwrap())
629 .await
630 .unwrap();
631
632 assert_eq!(response.status(), StatusCode::OK);
633
634 let response = router
636 .oneshot(Request::builder().uri("/users?page=2&limit=2").body(Body::empty()).unwrap())
637 .await
638 .unwrap();
639
640 assert_eq!(response.status(), StatusCode::OK);
641 }
642
643 #[tokio::test]
644 async fn test_filtering() {
645 let json_data = json!({
646 "users": [
647 {"id": 1, "name": "Alice", "role": "admin"},
648 {"id": 2, "name": "Bob", "role": "user"},
649 {"id": 3, "name": "Charlie", "role": "admin"}
650 ]
651 });
652
653 let state = QuickMockState::from_json(json_data).await.unwrap();
654 let router = build_quick_router(state).await;
655
656 let response = router
657 .oneshot(Request::builder().uri("/users?role=admin").body(Body::empty()).unwrap())
658 .await
659 .unwrap();
660
661 assert_eq!(response.status(), StatusCode::OK);
662 }
663
664 #[tokio::test]
665 async fn test_sorting() {
666 let json_data = json!({
667 "users": [
668 {"id": 3, "name": "Charlie"},
669 {"id": 1, "name": "Alice"},
670 {"id": 2, "name": "Bob"}
671 ]
672 });
673
674 let state = QuickMockState::from_json(json_data).await.unwrap();
675 let router = build_quick_router(state).await;
676
677 let response = router
679 .clone()
680 .oneshot(Request::builder().uri("/users?sort=name:asc").body(Body::empty()).unwrap())
681 .await
682 .unwrap();
683
684 assert_eq!(response.status(), StatusCode::OK);
685
686 let response = router
688 .oneshot(Request::builder().uri("/users?sort=name:desc").body(Body::empty()).unwrap())
689 .await
690 .unwrap();
691
692 assert_eq!(response.status(), StatusCode::OK);
693 }
694
695 #[tokio::test]
696 async fn test_get_by_id() {
697 let json_data = json!({
698 "users": [
699 {"id": 1, "name": "Alice"},
700 {"id": 2, "name": "Bob"}
701 ]
702 });
703
704 let state = QuickMockState::from_json(json_data).await.unwrap();
705 let router = build_quick_router(state).await;
706
707 let response = router
708 .oneshot(Request::builder().uri("/users/1").body(Body::empty()).unwrap())
709 .await
710 .unwrap();
711
712 assert_eq!(response.status(), StatusCode::OK);
713 }
714
715 #[tokio::test]
716 async fn test_update_handler() {
717 let json_data = json!({
718 "users": [
719 {"id": 1, "name": "Alice"}
720 ]
721 });
722
723 let state = QuickMockState::from_json(json_data).await.unwrap();
724 let router = build_quick_router(state).await;
725
726 let response = router
727 .oneshot(
728 Request::builder()
729 .method("PUT")
730 .uri("/users/1")
731 .header("content-type", "application/json")
732 .body(Body::from(r#"{"id":1,"name":"Alice Updated"}"#))
733 .unwrap(),
734 )
735 .await
736 .unwrap();
737
738 assert_eq!(response.status(), StatusCode::OK);
739 }
740
741 #[tokio::test]
742 async fn test_delete_handler() {
743 let json_data = json!({
744 "users": [
745 {"id": 1, "name": "Alice"}
746 ]
747 });
748
749 let state = QuickMockState::from_json(json_data).await.unwrap();
750 let router = build_quick_router(state).await;
751
752 let response = router
753 .oneshot(
754 Request::builder().method("DELETE").uri("/users/1").body(Body::empty()).unwrap(),
755 )
756 .await
757 .unwrap();
758
759 assert_eq!(response.status(), StatusCode::NO_CONTENT);
760 }
761
762 #[tokio::test]
763 async fn test_combined_query_params() {
764 let json_data = json!({
765 "users": [
766 {"id": 1, "name": "Alice", "role": "admin"},
767 {"id": 2, "name": "Bob", "role": "user"},
768 {"id": 3, "name": "Charlie", "role": "admin"},
769 {"id": 4, "name": "David", "role": "user"}
770 ]
771 });
772
773 let state = QuickMockState::from_json(json_data).await.unwrap();
774 let router = build_quick_router(state).await;
775
776 let response = router
778 .oneshot(
779 Request::builder()
780 .uri("/users?role=admin&sort=name:asc&page=1&limit=10")
781 .body(Body::empty())
782 .unwrap(),
783 )
784 .await
785 .unwrap();
786
787 assert_eq!(response.status(), StatusCode::OK);
788 }
789}