mockforge_ui/handlers/
playground.rs

1//! Playground API handlers for Admin UI
2//!
3//! Provides endpoints for the interactive GraphQL + REST playground that allows
4//! users to test and visualize mock endpoints.
5
6use axum::{
7    extract::{Path, State},
8    response::Json,
9};
10use chrono::Utc;
11use mockforge_core::request_logger::{get_global_logger, RequestLogEntry};
12use serde::{Deserialize, Serialize};
13use serde_json::{json, Value};
14use std::collections::HashMap;
15
16use crate::handlers::AdminState;
17use crate::models::ApiResponse;
18
19/// Endpoint information for playground
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct PlaygroundEndpoint {
22    /// Protocol type (rest, graphql)
23    pub protocol: String,
24    /// HTTP method (for REST) or operation type (for GraphQL)
25    pub method: String,
26    /// Endpoint path or GraphQL operation name
27    pub path: String,
28    /// Optional description
29    pub description: Option<String>,
30    /// Whether this endpoint is enabled
31    pub enabled: bool,
32}
33
34/// Request to execute a REST endpoint
35#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct ExecuteRestRequest {
37    /// HTTP method
38    pub method: String,
39    /// Request path
40    pub path: String,
41    /// Optional request headers
42    pub headers: Option<HashMap<String, String>>,
43    /// Optional request body
44    pub body: Option<Value>,
45    /// Base URL (defaults to HTTP server address)
46    pub base_url: Option<String>,
47    /// Whether to use MockAI for response generation
48    #[serde(default)]
49    pub use_mockai: bool,
50    /// Optional workspace ID for workspace-scoped requests
51    pub workspace_id: Option<String>,
52}
53
54/// Request to execute a GraphQL query
55#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct ExecuteGraphQLRequest {
57    /// GraphQL query string
58    pub query: String,
59    /// Optional variables
60    pub variables: Option<HashMap<String, Value>>,
61    /// Optional operation name
62    pub operation_name: Option<String>,
63    /// Base URL (defaults to GraphQL server address)
64    pub base_url: Option<String>,
65    /// Optional workspace ID for workspace-scoped requests
66    pub workspace_id: Option<String>,
67}
68
69/// Response from executing a request
70#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct ExecuteResponse {
72    /// Response status code
73    pub status_code: u16,
74    /// Response headers
75    pub headers: HashMap<String, String>,
76    /// Response body
77    pub body: Value,
78    /// Response time in milliseconds
79    pub response_time_ms: u64,
80    /// Request ID for history tracking
81    pub request_id: String,
82    /// Error message if any
83    pub error: Option<String>,
84}
85
86/// GraphQL introspection result
87#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct GraphQLIntrospectionResult {
89    /// Full introspection query result
90    pub schema: Value,
91    /// Available query types
92    pub query_types: Vec<String>,
93    /// Available mutation types
94    pub mutation_types: Vec<String>,
95    /// Available subscription types
96    pub subscription_types: Vec<String>,
97}
98
99/// Request history entry
100#[derive(Debug, Clone, Serialize, Deserialize)]
101pub struct PlaygroundHistoryEntry {
102    /// Request ID
103    pub id: String,
104    /// Protocol type
105    pub protocol: String,
106    /// HTTP method or GraphQL operation type
107    pub method: String,
108    /// Request path or GraphQL query
109    pub path: String,
110    /// Response status code
111    pub status_code: u16,
112    /// Response time in milliseconds
113    pub response_time_ms: u64,
114    /// Timestamp
115    pub timestamp: chrono::DateTime<chrono::Utc>,
116    /// Request headers (for REST)
117    pub request_headers: Option<HashMap<String, String>>,
118    /// Request body (for REST)
119    pub request_body: Option<Value>,
120    /// GraphQL query (for GraphQL)
121    pub graphql_query: Option<String>,
122    /// GraphQL variables (for GraphQL)
123    pub graphql_variables: Option<HashMap<String, Value>>,
124}
125
126/// Code snippet generation request
127#[derive(Debug, Clone, Serialize, Deserialize)]
128pub struct CodeSnippetRequest {
129    /// Protocol type
130    pub protocol: String,
131    /// HTTP method (for REST)
132    pub method: Option<String>,
133    /// Request path
134    pub path: String,
135    /// Request headers
136    pub headers: Option<HashMap<String, String>>,
137    /// Request body
138    pub body: Option<Value>,
139    /// GraphQL query (for GraphQL)
140    pub graphql_query: Option<String>,
141    /// GraphQL variables (for GraphQL)
142    pub graphql_variables: Option<HashMap<String, Value>>,
143    /// Base URL
144    pub base_url: String,
145}
146
147/// Code snippet response
148#[derive(Debug, Clone, Serialize, Deserialize)]
149pub struct CodeSnippetResponse {
150    /// Generated code snippets by language
151    pub snippets: HashMap<String, String>,
152}
153
154/// List available endpoints for playground
155pub async fn list_playground_endpoints(
156    State(state): State<AdminState>,
157    axum::extract::Query(params): axum::extract::Query<HashMap<String, String>>,
158) -> Json<ApiResponse<Vec<PlaygroundEndpoint>>> {
159    // Get workspace_id from query params (optional)
160    let workspace_id = params.get("workspace_id");
161    let mut endpoints = Vec::new();
162
163    // Get REST endpoints from HTTP server
164    if let Some(http_addr) = state.http_server_addr {
165        let mut url = format!("http://{}/__mockforge/routes", http_addr);
166
167        // Add workspace_id to route query if provided
168        if let Some(ws_id) = workspace_id {
169            url = format!("{}?workspace_id={}", url, ws_id);
170        }
171
172        if let Ok(response) = reqwest::get(&url).await {
173            if response.status().is_success() {
174                if let Ok(body) = response.json::<Value>().await {
175                    if let Some(routes) = body.get("routes").and_then(|r| r.as_array()) {
176                        for route in routes {
177                            // Filter by workspace if workspace_id is provided and route has workspace info
178                            if let Some(ws_id) = workspace_id {
179                                if let Some(route_workspace) =
180                                    route.get("workspace_id").and_then(|w| w.as_str())
181                                {
182                                    if route_workspace != ws_id {
183                                        continue; // Skip routes not belonging to this workspace
184                                    }
185                                }
186                            }
187
188                            if let (Some(method), Some(path)) = (
189                                route.get("method").and_then(|m| m.as_str()),
190                                route.get("path").and_then(|p| p.as_str()),
191                            ) {
192                                endpoints.push(PlaygroundEndpoint {
193                                    protocol: "rest".to_string(),
194                                    method: method.to_string(),
195                                    path: path.to_string(),
196                                    description: route
197                                        .get("description")
198                                        .and_then(|d| d.as_str())
199                                        .map(|s| s.to_string()),
200                                    enabled: true,
201                                });
202                            }
203                        }
204                    }
205                }
206            }
207        }
208    }
209
210    // Get GraphQL endpoint if GraphQL server is available
211    if state.graphql_server_addr.is_some() {
212        endpoints.push(PlaygroundEndpoint {
213            protocol: "graphql".to_string(),
214            method: "query".to_string(),
215            path: "/graphql".to_string(),
216            description: Some("GraphQL endpoint".to_string()),
217            enabled: true,
218        });
219    }
220
221    Json(ApiResponse::success(endpoints))
222}
223
224/// Execute a REST request
225pub async fn execute_rest_request(
226    State(state): State<AdminState>,
227    axum::extract::Json(request): axum::extract::Json<ExecuteRestRequest>,
228) -> Json<ApiResponse<ExecuteResponse>> {
229    let start_time = std::time::Instant::now();
230    let request_id = uuid::Uuid::new_v4().to_string();
231
232    // Determine base URL
233    let base_url = request.base_url.unwrap_or_else(|| {
234        state
235            .http_server_addr
236            .map(|addr| format!("http://{}", addr))
237            .unwrap_or_else(|| "http://localhost:3000".to_string())
238    });
239
240    // Build full URL
241    let url = if request.path.starts_with("http") {
242        request.path.clone()
243    } else {
244        format!("{}{}", base_url, request.path)
245    };
246
247    // Create HTTP client
248    let client = reqwest::Client::builder()
249        .timeout(std::time::Duration::from_secs(30))
250        .build()
251        .unwrap_or_else(|_| reqwest::Client::new());
252
253    // Build request
254    let mut http_request = match request.method.as_str() {
255        "GET" => client.get(&url),
256        "POST" => client.post(&url),
257        "PUT" => client.put(&url),
258        "DELETE" => client.delete(&url),
259        "PATCH" => client.patch(&url),
260        _ => {
261            return Json(ApiResponse::error(format!(
262                "Unsupported HTTP method: {}",
263                request.method
264            )));
265        }
266    };
267
268    // Add headers
269    let mut headers = request.headers.clone().unwrap_or_default();
270
271    // Add MockAI preview header if requested
272    if request.use_mockai {
273        headers.insert("X-MockAI-Preview".to_string(), "true".to_string());
274    }
275
276    // Add workspace ID header if provided
277    if let Some(ws_id) = &request.workspace_id {
278        headers.insert("X-Workspace-ID".to_string(), ws_id.clone());
279    }
280
281    for (key, value) in &headers {
282        http_request = http_request.header(key, value);
283    }
284
285    // Add body
286    if let Some(body) = &request.body {
287        http_request = http_request.json(body);
288    }
289
290    // Execute request
291    let response = http_request.send().await;
292
293    let response_time_ms = start_time.elapsed().as_millis() as u64;
294
295    match response {
296        Ok(resp) => {
297            let status_code = resp.status().as_u16();
298
299            // Get response headers
300            let mut headers = HashMap::new();
301            for (key, value) in resp.headers() {
302                if let Ok(value_str) = value.to_str() {
303                    headers.insert(key.to_string(), value_str.to_string());
304                }
305            }
306
307            // Get response body
308            let body = resp
309                .json::<Value>()
310                .await
311                .unwrap_or_else(|_| json!({ "error": "Failed to parse response as JSON" }));
312
313            // Log request
314            if let Some(logger) = get_global_logger() {
315                // Store workspace_id in metadata for filtering
316                let mut metadata = HashMap::new();
317                if let Some(ws_id) =
318                    request.workspace_id.as_ref().or_else(|| headers.get("X-Workspace-ID"))
319                {
320                    metadata.insert("workspace_id".to_string(), ws_id.clone());
321                }
322
323                let log_entry = RequestLogEntry {
324                    id: request_id.clone(),
325                    timestamp: Utc::now(),
326                    server_type: "http".to_string(),
327                    method: request.method.clone(),
328                    path: request.path.clone(),
329                    status_code,
330                    response_time_ms,
331                    client_ip: None,
332                    user_agent: Some("MockForge-Playground".to_string()),
333                    headers: headers.clone(),
334                    query_params: HashMap::new(), // Query params not available in playground
335                    response_size_bytes: serde_json::to_string(&body)
336                        .map(|s| s.len() as u64)
337                        .unwrap_or(0),
338                    error_message: None,
339                    metadata,
340                    reality_metadata: None,
341                };
342                logger.log_request(log_entry).await;
343            }
344
345            Json(ApiResponse::success(ExecuteResponse {
346                status_code,
347                headers,
348                body: body.clone(),
349                response_time_ms,
350                request_id,
351                error: None,
352            }))
353        }
354        Err(e) => {
355            let error_msg = e.to_string();
356            Json(ApiResponse::success(ExecuteResponse {
357                status_code: 0,
358                headers: HashMap::new(),
359                body: json!({ "error": error_msg }),
360                response_time_ms,
361                request_id,
362                error: Some(error_msg),
363            }))
364        }
365    }
366}
367
368/// Execute a GraphQL query
369pub async fn execute_graphql_query(
370    State(state): State<AdminState>,
371    axum::extract::Json(request): axum::extract::Json<ExecuteGraphQLRequest>,
372) -> Json<ApiResponse<ExecuteResponse>> {
373    let start_time = std::time::Instant::now();
374    let request_id = uuid::Uuid::new_v4().to_string();
375
376    // Determine base URL
377    let base_url = request.base_url.unwrap_or_else(|| {
378        state
379            .graphql_server_addr
380            .map(|addr| format!("http://{}", addr))
381            .unwrap_or_else(|| "http://localhost:4000".to_string())
382    });
383
384    // Build GraphQL request
385    let mut graphql_body = json!({
386        "query": request.query
387    });
388
389    if let Some(variables) = &request.variables {
390        graphql_body["variables"] = json!(variables);
391    }
392
393    if let Some(operation_name) = &request.operation_name {
394        graphql_body["operationName"] = json!(operation_name);
395    }
396
397    // Create HTTP client
398    let client = reqwest::Client::builder()
399        .timeout(std::time::Duration::from_secs(30))
400        .build()
401        .unwrap_or_else(|_| reqwest::Client::new());
402
403    // Execute GraphQL request
404    let url = format!("{}/graphql", base_url);
405    let mut graphql_request = client.post(&url).header("Content-Type", "application/json");
406
407    // Add workspace ID header if provided
408    if let Some(ws_id) = &request.workspace_id {
409        graphql_request = graphql_request.header("X-Workspace-ID", ws_id);
410    }
411
412    let response = graphql_request.json(&graphql_body).send().await;
413
414    let response_time_ms = start_time.elapsed().as_millis() as u64;
415
416    match response {
417        Ok(resp) => {
418            let status_code = resp.status().as_u16();
419
420            // Get response headers
421            let mut headers = HashMap::new();
422            for (key, value) in resp.headers() {
423                if let Ok(value_str) = value.to_str() {
424                    headers.insert(key.to_string(), value_str.to_string());
425                }
426            }
427
428            // Get response body
429            let body = resp
430                .json::<Value>()
431                .await
432                .unwrap_or_else(|_| json!({ "error": "Failed to parse response as JSON" }));
433
434            // Log request
435            if let Some(logger) = get_global_logger() {
436                // Store workspace_id and GraphQL query/variables in metadata
437                let mut metadata = HashMap::new();
438                if let Some(ws_id) = &request.workspace_id {
439                    metadata.insert("workspace_id".to_string(), ws_id.clone());
440                }
441                metadata.insert("query".to_string(), request.query.clone());
442                if let Some(variables) = &request.variables {
443                    if let Ok(vars_str) = serde_json::to_string(variables) {
444                        metadata.insert("variables".to_string(), vars_str);
445                    }
446                }
447
448                let has_errors = body.get("errors").is_some();
449                let log_entry = RequestLogEntry {
450                    id: request_id.clone(),
451                    timestamp: Utc::now(),
452                    server_type: "graphql".to_string(),
453                    method: "POST".to_string(),
454                    path: "/graphql".to_string(),
455                    status_code,
456                    response_time_ms,
457                    client_ip: None,
458                    user_agent: Some("MockForge-Playground".to_string()),
459                    headers: HashMap::new(),
460                    query_params: HashMap::new(), // Query params not available in playground
461                    response_size_bytes: serde_json::to_string(&body)
462                        .map(|s| s.len() as u64)
463                        .unwrap_or(0),
464                    error_message: if has_errors {
465                        Some("GraphQL errors in response".to_string())
466                    } else {
467                        None
468                    },
469                    reality_metadata: None,
470                    metadata: {
471                        let mut meta = HashMap::new();
472                        meta.insert("query".to_string(), request.query.clone());
473                        if let Some(vars) = &request.variables {
474                            if let Ok(vars_str) = serde_json::to_string(vars) {
475                                meta.insert("variables".to_string(), vars_str);
476                            }
477                        }
478                        meta
479                    },
480                };
481                logger.log_request(log_entry).await;
482            }
483
484            let has_errors = body.get("errors").is_some();
485            Json(ApiResponse::success(ExecuteResponse {
486                status_code,
487                headers,
488                body: body.clone(),
489                response_time_ms,
490                request_id,
491                error: if has_errors {
492                    Some("GraphQL errors in response".to_string())
493                } else {
494                    None
495                },
496            }))
497        }
498        Err(e) => {
499            let error_msg = e.to_string();
500            Json(ApiResponse::success(ExecuteResponse {
501                status_code: 0,
502                headers: HashMap::new(),
503                body: json!({ "error": error_msg }),
504                response_time_ms,
505                request_id,
506                error: Some(error_msg),
507            }))
508        }
509    }
510}
511
512/// Perform GraphQL introspection
513pub async fn graphql_introspect(
514    State(state): State<AdminState>,
515) -> Json<ApiResponse<GraphQLIntrospectionResult>> {
516    // Determine base URL
517    let base_url = state
518        .graphql_server_addr
519        .map(|addr| format!("http://{}", addr))
520        .unwrap_or_else(|| "http://localhost:4000".to_string());
521
522    // Standard GraphQL introspection query
523    let introspection_query = r#"
524        query IntrospectionQuery {
525            __schema {
526                queryType { name }
527                mutationType { name }
528                subscriptionType { name }
529                types {
530                    ...FullType
531                }
532                directives {
533                    name
534                    description
535                    locations
536                    args {
537                        ...InputValue
538                    }
539                }
540            }
541        }
542
543        fragment FullType on __Type {
544            kind
545            name
546            description
547            fields(includeDeprecated: true) {
548                name
549                description
550                args {
551                    ...InputValue
552                }
553                type {
554                    ...TypeRef
555                }
556                isDeprecated
557                deprecationReason
558            }
559            inputFields {
560                ...InputValue
561            }
562            interfaces {
563                ...TypeRef
564            }
565            enumValues(includeDeprecated: true) {
566                name
567                description
568                isDeprecated
569                deprecationReason
570            }
571            possibleTypes {
572                ...TypeRef
573            }
574        }
575
576        fragment InputValue on __InputValue {
577            name
578            description
579            type {
580                ...TypeRef
581            }
582            defaultValue
583        }
584
585        fragment TypeRef on __Type {
586            kind
587            name
588            ofType {
589                kind
590                name
591                ofType {
592                    kind
593                    name
594                    ofType {
595                        kind
596                        name
597                        ofType {
598                            kind
599                            name
600                            ofType {
601                                kind
602                                name
603                                ofType {
604                                    kind
605                                    name
606                                    ofType {
607                                        kind
608                                        name
609                                    }
610                                }
611                            }
612                        }
613                    }
614                }
615            }
616        }
617    "#;
618
619    let client = reqwest::Client::builder()
620        .timeout(std::time::Duration::from_secs(30))
621        .build()
622        .unwrap_or_else(|_| reqwest::Client::new());
623
624    let url = format!("{}/graphql", base_url);
625    let response = client
626        .post(&url)
627        .header("Content-Type", "application/json")
628        .json(&json!({
629            "query": introspection_query
630        }))
631        .send()
632        .await;
633
634    match response {
635        Ok(resp) => {
636            if let Ok(body) = resp.json::<Value>().await {
637                if let Some(data) = body.get("data").and_then(|d| d.get("__schema")) {
638                    let schema = data.clone();
639
640                    // Extract query, mutation, and subscription types
641                    let query_types = schema
642                        .get("queryType")
643                        .and_then(|q| q.get("name"))
644                        .and_then(|n| n.as_str())
645                        .map(|_| vec!["Query".to_string()])
646                        .unwrap_or_default();
647
648                    let mutation_types = schema
649                        .get("mutationType")
650                        .and_then(|m| m.get("name"))
651                        .and_then(|n| n.as_str())
652                        .map(|_| vec!["Mutation".to_string()])
653                        .unwrap_or_default();
654
655                    let subscription_types = schema
656                        .get("subscriptionType")
657                        .and_then(|s| s.get("name"))
658                        .and_then(|n| n.as_str())
659                        .map(|_| vec!["Subscription".to_string()])
660                        .unwrap_or_default();
661
662                    Json(ApiResponse::success(GraphQLIntrospectionResult {
663                        schema: schema.clone(),
664                        query_types,
665                        mutation_types,
666                        subscription_types,
667                    }))
668                } else {
669                    Json(ApiResponse::error("Failed to parse introspection response".to_string()))
670                }
671            } else {
672                Json(ApiResponse::error("Failed to parse response".to_string()))
673            }
674        }
675        Err(e) => Json(ApiResponse::error(format!("Failed to execute introspection query: {}", e))),
676    }
677}
678
679/// Get request history
680pub async fn get_request_history(
681    State(_state): State<AdminState>,
682    axum::extract::Query(params): axum::extract::Query<HashMap<String, String>>,
683) -> Json<ApiResponse<Vec<PlaygroundHistoryEntry>>> {
684    let logger = match get_global_logger() {
685        Some(logger) => logger,
686        None => {
687            return Json(ApiResponse::error("Request logger not initialized".to_string()));
688        }
689    };
690
691    // Get limit from query params
692    let limit = params.get("limit").and_then(|l| l.parse::<usize>().ok()).unwrap_or(100);
693
694    // Get protocol filter
695    let protocol_filter = params.get("protocol");
696
697    // Get workspace_id filter
698    let workspace_id_filter = params.get("workspace_id");
699
700    // Get logs
701    let mut logs = if let Some(protocol) = protocol_filter {
702        logger
703            .get_logs_by_server(protocol, Some(limit * 2)) // Get more to account for filtering
704            .await
705    } else {
706        logger.get_recent_logs(Some(limit * 2)).await
707    };
708
709    // Filter by workspace_id if provided
710    if let Some(ws_id) = workspace_id_filter {
711        logs.retain(|log| log.metadata.get("workspace_id").map(|w| w == ws_id).unwrap_or(false));
712    }
713
714    // Limit after filtering
715    logs.truncate(limit);
716
717    // Convert to playground history entries
718    let history: Vec<PlaygroundHistoryEntry> = logs
719        .into_iter()
720        .map(|log| {
721            // Extract GraphQL query and variables from metadata
722            let graphql_query = log.metadata.get("query").cloned();
723            let graphql_variables = log
724                .metadata
725                .get("variables")
726                .and_then(|v| serde_json::from_str::<HashMap<String, Value>>(v).ok());
727
728            PlaygroundHistoryEntry {
729                id: log.id,
730                protocol: log.server_type.clone(),
731                method: log.method.clone(),
732                path: log.path.clone(),
733                status_code: log.status_code,
734                response_time_ms: log.response_time_ms,
735                timestamp: log.timestamp,
736                request_headers: if log.server_type == "http" {
737                    Some(log.headers.clone())
738                } else {
739                    None
740                },
741                request_body: None, // Request body not stored in logs currently
742                graphql_query,
743                graphql_variables,
744            }
745        })
746        .collect();
747
748    Json(ApiResponse::success(history))
749}
750
751/// Replay a request from history
752pub async fn replay_request(
753    State(state): State<AdminState>,
754    Path(id): Path<String>,
755) -> Json<ApiResponse<ExecuteResponse>> {
756    let logger = match get_global_logger() {
757        Some(logger) => logger,
758        None => {
759            return Json(ApiResponse::error("Request logger not initialized".to_string()));
760        }
761    };
762
763    // Get all logs and find the one with matching ID
764    let logs = logger.get_recent_logs(None).await;
765    let log_entry = logs.into_iter().find(|log| log.id == id);
766
767    match log_entry {
768        Some(log) => {
769            if log.server_type == "graphql" {
770                // Replay GraphQL request
771                if let Some(query) = log.metadata.get("query") {
772                    let variables = log
773                        .metadata
774                        .get("variables")
775                        .and_then(|v| serde_json::from_str::<HashMap<String, Value>>(v).ok());
776
777                    let graphql_request = ExecuteGraphQLRequest {
778                        query: query.clone(),
779                        variables,
780                        operation_name: None,
781                        base_url: None,
782                        workspace_id: log.metadata.get("workspace_id").cloned(),
783                    };
784
785                    execute_graphql_query(State(state), axum::extract::Json(graphql_request)).await
786                } else {
787                    Json(ApiResponse::error("GraphQL query not found in log entry".to_string()))
788                }
789            } else {
790                // Replay REST request
791                let rest_request = ExecuteRestRequest {
792                    method: log.method.clone(),
793                    path: log.path.clone(),
794                    headers: Some(log.headers.clone()),
795                    body: None, // Request body not stored in logs
796                    base_url: None,
797                    use_mockai: false,
798                    workspace_id: log.metadata.get("workspace_id").cloned(),
799                };
800
801                execute_rest_request(State(state), axum::extract::Json(rest_request)).await
802            }
803        }
804        None => Json(ApiResponse::error(format!("Request with ID {} not found", id))),
805    }
806}
807
808/// Generate code snippets
809pub async fn generate_code_snippet(
810    State(_state): State<AdminState>,
811    axum::extract::Json(request): axum::extract::Json<CodeSnippetRequest>,
812) -> Json<ApiResponse<CodeSnippetResponse>> {
813    let mut snippets = HashMap::new();
814
815    if request.protocol == "rest" {
816        // Generate curl snippet
817        let mut curl_parts = vec!["curl".to_string()];
818        if let Some(method) = &request.method {
819            if method != "GET" {
820                curl_parts.push(format!("-X {}", method));
821            }
822        }
823
824        if let Some(headers) = &request.headers {
825            for (key, value) in headers {
826                curl_parts.push(format!("-H \"{}: {}\"", key, value));
827            }
828        }
829
830        if let Some(body) = &request.body {
831            curl_parts.push(format!("-d '{}'", serde_json::to_string(body).unwrap_or_default()));
832        }
833
834        let url = if request.path.starts_with("http") {
835            request.path.clone()
836        } else {
837            format!("{}{}", request.base_url, request.path)
838        };
839        curl_parts.push(format!("\"{}\"", url));
840
841        snippets.insert("curl".to_string(), curl_parts.join(" \\\n  "));
842
843        // Generate JavaScript fetch snippet
844        let mut js_code = String::new();
845        js_code.push_str("fetch(");
846        js_code.push_str(&format!("\"{}\"", url));
847        js_code.push_str(", {\n");
848
849        if let Some(method) = &request.method {
850            js_code.push_str(&format!("  method: \"{}\",\n", method));
851        }
852
853        if let Some(headers) = &request.headers {
854            js_code.push_str("  headers: {\n");
855            for (key, value) in headers {
856                js_code.push_str(&format!("    \"{}\": \"{}\",\n", key, value));
857            }
858            js_code.push_str("  },\n");
859        }
860
861        if let Some(body) = &request.body {
862            js_code.push_str(&format!(
863                "  body: JSON.stringify({}),\n",
864                serde_json::to_string(body).unwrap_or_default()
865            ));
866        }
867
868        js_code.push_str("})");
869        snippets.insert("javascript".to_string(), js_code);
870
871        // Generate Python requests snippet
872        let mut python_code = String::new();
873        python_code.push_str("import requests\n\n");
874        python_code.push_str("response = requests.");
875
876        let method = request.method.as_deref().unwrap_or("get").to_lowercase();
877        python_code.push_str(&method);
878        python_code.push_str("(\n");
879        python_code.push_str(&format!("    \"{}\"", url));
880
881        if let Some(headers) = &request.headers {
882            python_code.push_str(",\n    headers={\n");
883            for (key, value) in headers {
884                python_code.push_str(&format!("        \"{}\": \"{}\",\n", key, value));
885            }
886            python_code.push_str("    }");
887        }
888
889        if let Some(body) = &request.body {
890            python_code.push_str(",\n    json=");
891            python_code.push_str(&serde_json::to_string(body).unwrap_or_default());
892        }
893
894        python_code.push_str("\n)");
895        snippets.insert("python".to_string(), python_code);
896    } else if request.protocol == "graphql" {
897        // Generate GraphQL snippets
898        if let Some(query) = &request.graphql_query {
899            // curl snippet
900            let mut curl_parts = vec!["curl".to_string(), "-X POST".to_string()];
901            curl_parts.push("-H \"Content-Type: application/json\"".to_string());
902
903            let mut graphql_body = json!({ "query": query });
904            if let Some(vars) = &request.graphql_variables {
905                graphql_body["variables"] = json!(vars);
906            }
907
908            curl_parts
909                .push(format!("-d '{}'", serde_json::to_string(&graphql_body).unwrap_or_default()));
910            curl_parts.push(format!("\"{}/graphql\"", request.base_url));
911
912            snippets.insert("curl".to_string(), curl_parts.join(" \\\n  "));
913
914            // JavaScript fetch snippet
915            let mut js_code = String::new();
916            js_code.push_str("fetch(\"");
917            js_code.push_str(&format!("{}/graphql", request.base_url));
918            js_code.push_str("\", {\n");
919            js_code.push_str("  method: \"POST\",\n");
920            js_code.push_str("  headers: {\n");
921            js_code.push_str("    \"Content-Type\": \"application/json\",\n");
922            js_code.push_str("  },\n");
923            js_code.push_str("  body: JSON.stringify({\n");
924            js_code.push_str(&format!("    query: `{}`,\n", query.replace('`', "\\`")));
925            if let Some(vars) = &request.graphql_variables {
926                js_code.push_str("    variables: ");
927                js_code.push_str(&serde_json::to_string(vars).unwrap_or_default());
928                js_code.push_str(",\n");
929            }
930            js_code.push_str("  }),\n");
931            js_code.push_str("})");
932            snippets.insert("javascript".to_string(), js_code);
933        }
934    }
935
936    Json(ApiResponse::success(CodeSnippetResponse { snippets }))
937}
938
939#[cfg(test)]
940mod tests {
941    use super::*;
942    use serde_json::json;
943
944    #[test]
945    fn test_code_snippet_generation_rest_get() {
946        let request = CodeSnippetRequest {
947            protocol: "rest".to_string(),
948            method: Some("GET".to_string()),
949            path: "/api/users".to_string(),
950            headers: None,
951            body: None,
952            graphql_query: None,
953            graphql_variables: None,
954            base_url: "http://localhost:3000".to_string(),
955        };
956
957        // Test that we can serialize the request
958        let serialized = serde_json::to_string(&request).unwrap();
959        assert!(serialized.contains("GET"));
960        assert!(serialized.contains("/api/users"));
961    }
962
963    #[test]
964    fn test_code_snippet_generation_rest_post() {
965        let request = CodeSnippetRequest {
966            protocol: "rest".to_string(),
967            method: Some("POST".to_string()),
968            path: "/api/users".to_string(),
969            headers: Some({
970                let mut h = HashMap::new();
971                h.insert("Content-Type".to_string(), "application/json".to_string());
972                h
973            }),
974            body: Some(json!({ "name": "John" })),
975            graphql_query: None,
976            graphql_variables: None,
977            base_url: "http://localhost:3000".to_string(),
978        };
979
980        // Test that we can serialize the request
981        let serialized = serde_json::to_string(&request).unwrap();
982        assert!(serialized.contains("POST"));
983        assert!(serialized.contains("Content-Type"));
984    }
985
986    #[test]
987    fn test_code_snippet_generation_graphql() {
988        let request = CodeSnippetRequest {
989            protocol: "graphql".to_string(),
990            method: None,
991            path: "/graphql".to_string(),
992            headers: None,
993            body: None,
994            graphql_query: Some("query { user(id: 1) { name } }".to_string()),
995            graphql_variables: None,
996            base_url: "http://localhost:4000".to_string(),
997        };
998
999        // Test that we can serialize the request
1000        let serialized = serde_json::to_string(&request).unwrap();
1001        assert!(serialized.contains("graphql"));
1002        assert!(serialized.contains("user(id: 1)"));
1003    }
1004
1005    #[test]
1006    fn test_playground_endpoint_serialization() {
1007        let endpoint = PlaygroundEndpoint {
1008            protocol: "rest".to_string(),
1009            method: "GET".to_string(),
1010            path: "/api/users".to_string(),
1011            description: Some("Get users".to_string()),
1012            enabled: true,
1013        };
1014
1015        let serialized = serde_json::to_string(&endpoint).unwrap();
1016        assert!(serialized.contains("rest"));
1017        assert!(serialized.contains("GET"));
1018        assert!(serialized.contains("/api/users"));
1019    }
1020
1021    #[test]
1022    fn test_execute_response_serialization() {
1023        let response = ExecuteResponse {
1024            status_code: 200,
1025            headers: {
1026                let mut h = HashMap::new();
1027                h.insert("Content-Type".to_string(), "application/json".to_string());
1028                h
1029            },
1030            body: json!({ "success": true }),
1031            response_time_ms: 150,
1032            request_id: "test-id".to_string(),
1033            error: None,
1034        };
1035
1036        let serialized = serde_json::to_string(&response).unwrap();
1037        assert!(serialized.contains("200"));
1038        assert!(serialized.contains("test-id"));
1039    }
1040}