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