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