mockforge_http/
ui_builder.rs

1/// UI Builder API for MockForge
2///
3/// Provides REST endpoints for the low-code UI builder that allows visual
4/// creation and editing of mock endpoints without writing code.
5use axum::{
6    extract::{Path, State},
7    http::StatusCode,
8    response::{IntoResponse, Json},
9    routing::{get, post},
10    Router,
11};
12use mockforge_core::config::ServerConfig;
13use mockforge_core::import::asyncapi_import::import_asyncapi_spec;
14use mockforge_core::import::openapi_import::import_openapi_spec;
15use serde::{Deserialize, Serialize};
16use std::sync::Arc;
17use tokio::sync::RwLock;
18use tracing::*;
19
20/// Endpoint configuration for UI builder
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct EndpointConfig {
23    /// Unique identifier for the endpoint
24    pub id: String,
25    /// Protocol type for this endpoint
26    pub protocol: Protocol,
27    /// Human-readable endpoint name
28    pub name: String,
29    /// Optional endpoint description
30    pub description: Option<String>,
31    /// Whether this endpoint is currently enabled
32    pub enabled: bool,
33    /// Protocol-specific configuration
34    pub config: EndpointProtocolConfig,
35}
36
37/// Supported protocols in UI builder
38#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
39#[serde(rename_all = "lowercase")]
40pub enum Protocol {
41    /// HTTP/REST protocol
42    Http,
43    /// gRPC protocol
44    Grpc,
45    /// WebSocket protocol
46    Websocket,
47    /// GraphQL protocol
48    Graphql,
49    /// MQTT protocol
50    Mqtt,
51    /// SMTP protocol
52    Smtp,
53    /// Kafka protocol
54    Kafka,
55    /// AMQP protocol
56    Amqp,
57    /// FTP protocol
58    Ftp,
59}
60
61/// Protocol-specific endpoint configuration
62#[derive(Debug, Clone, Serialize, Deserialize)]
63#[serde(tag = "type")]
64pub enum EndpointProtocolConfig {
65    /// HTTP endpoint configuration
66    Http(HttpEndpointConfig),
67    /// gRPC endpoint configuration
68    Grpc(GrpcEndpointConfig),
69    /// WebSocket endpoint configuration
70    Websocket(WebsocketEndpointConfig),
71}
72
73/// HTTP endpoint configuration
74#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct HttpEndpointConfig {
76    /// HTTP method (GET, POST, PUT, etc.)
77    pub method: String,
78    /// API path pattern
79    pub path: String,
80    /// Optional request validation and schema configuration
81    pub request: Option<HttpRequestConfig>,
82    /// Response configuration
83    pub response: HttpResponseConfig,
84    /// Optional behavior configuration (latency, failure injection, etc.)
85    pub behavior: Option<EndpointBehavior>,
86}
87
88/// HTTP request configuration
89#[derive(Debug, Clone, Serialize, Deserialize)]
90pub struct HttpRequestConfig {
91    /// Optional validation settings
92    pub validation: Option<ValidationConfig>,
93    /// Optional custom request headers
94    pub headers: Option<Vec<HeaderConfig>>,
95    /// Optional query parameter definitions
96    pub query_params: Option<Vec<QueryParamConfig>>,
97    /// Optional JSON schema for request body
98    pub body_schema: Option<serde_json::Value>,
99}
100
101/// HTTP response configuration
102#[derive(Debug, Clone, Serialize, Deserialize)]
103pub struct HttpResponseConfig {
104    /// HTTP status code to return
105    pub status: u16,
106    /// Optional custom response headers
107    pub headers: Option<Vec<HeaderConfig>>,
108    /// Response body configuration
109    pub body: ResponseBody,
110}
111
112/// Response body configuration
113#[derive(Debug, Clone, Serialize, Deserialize)]
114#[serde(tag = "type")]
115pub enum ResponseBody {
116    /// Static JSON response content
117    Static {
118        /// JSON value to return
119        content: serde_json::Value,
120    },
121    /// Template-based response with variable expansion
122    Template {
123        /// Template string with variables (e.g., "{{uuid}}", "{{now}}")
124        template: String,
125    },
126    /// Faker-generated response from schema
127    Faker {
128        /// JSON schema for data generation
129        schema: serde_json::Value,
130    },
131    /// AI-generated response
132    AI {
133        /// Prompt for AI generation
134        prompt: String,
135    },
136}
137
138/// Header configuration
139#[derive(Debug, Clone, Serialize, Deserialize)]
140pub struct HeaderConfig {
141    /// Header name
142    pub name: String,
143    /// Header value
144    pub value: String,
145}
146
147/// Query parameter configuration
148#[derive(Debug, Clone, Serialize, Deserialize)]
149pub struct QueryParamConfig {
150    /// Parameter name
151    pub name: String,
152    /// Whether this parameter is required
153    pub required: bool,
154    /// Optional JSON schema for validation
155    pub schema: Option<serde_json::Value>,
156}
157
158/// Validation configuration
159#[derive(Debug, Clone, Serialize, Deserialize)]
160pub struct ValidationConfig {
161    /// Validation mode (off, warn, or enforce)
162    pub mode: ValidationMode,
163    /// Optional JSON schema for validation
164    pub schema: Option<serde_json::Value>,
165}
166
167/// Validation mode
168#[derive(Debug, Clone, Serialize, Deserialize)]
169#[serde(rename_all = "lowercase")]
170pub enum ValidationMode {
171    /// Validation disabled
172    Off,
173    /// Validation enabled but only warns on errors
174    Warn,
175    /// Validation enabled and rejects invalid requests
176    Enforce,
177}
178
179/// Endpoint behavior configuration (chaos engineering)
180#[derive(Debug, Clone, Serialize, Deserialize)]
181pub struct EndpointBehavior {
182    /// Optional latency injection configuration
183    pub latency: Option<LatencyConfig>,
184    /// Optional failure injection configuration
185    pub failure: Option<FailureConfig>,
186    /// Optional traffic shaping configuration
187    pub traffic_shaping: Option<TrafficShapingConfig>,
188}
189
190/// Latency configuration
191#[derive(Debug, Clone, Serialize, Deserialize)]
192pub struct LatencyConfig {
193    /// Base latency in milliseconds
194    pub base_ms: u64,
195    /// Random jitter to add to base latency (milliseconds)
196    pub jitter_ms: u64,
197    /// Distribution type for latency simulation
198    pub distribution: LatencyDistribution,
199}
200
201/// Latency distribution type
202#[derive(Debug, Clone, Serialize, Deserialize)]
203#[serde(rename_all = "lowercase")]
204pub enum LatencyDistribution {
205    /// Fixed latency (base + uniform jitter)
206    Fixed,
207    /// Normal/Gaussian distribution
208    Normal {
209        /// Standard deviation in milliseconds
210        std_dev_ms: f64,
211    },
212    /// Pareto distribution (for realistic network simulation)
213    Pareto {
214        /// Shape parameter for Pareto distribution
215        shape: f64,
216    },
217}
218
219/// Failure configuration
220#[derive(Debug, Clone, Serialize, Deserialize)]
221pub struct FailureConfig {
222    /// Error injection rate (0.0 to 1.0, where 1.0 = 100%)
223    pub error_rate: f64,
224    /// List of HTTP status codes to randomly return on failure
225    pub status_codes: Vec<u16>,
226    /// Optional custom error message
227    pub error_message: Option<String>,
228}
229
230/// Traffic shaping configuration
231#[derive(Debug, Clone, Serialize, Deserialize)]
232pub struct TrafficShapingConfig {
233    /// Optional bandwidth limit in bytes per second
234    pub bandwidth_limit_bps: Option<u64>,
235    /// Optional packet loss rate (0.0 to 1.0)
236    pub packet_loss_rate: Option<f64>,
237}
238
239/// gRPC endpoint configuration
240#[derive(Debug, Clone, Serialize, Deserialize)]
241pub struct GrpcEndpointConfig {
242    /// gRPC service name
243    pub service: String,
244    /// gRPC method name
245    pub method: String,
246    /// Path to proto file
247    pub proto_file: String,
248    /// Request message type name
249    pub request_type: String,
250    /// Response message type name
251    pub response_type: String,
252    /// Response configuration
253    pub response: GrpcResponseConfig,
254    /// Optional behavior configuration
255    pub behavior: Option<EndpointBehavior>,
256}
257
258/// gRPC response configuration
259#[derive(Debug, Clone, Serialize, Deserialize)]
260pub struct GrpcResponseConfig {
261    /// Response body configuration
262    pub body: ResponseBody,
263    /// Optional metadata (gRPC headers)
264    pub metadata: Option<Vec<HeaderConfig>>,
265}
266
267/// WebSocket endpoint configuration
268#[derive(Debug, Clone, Serialize, Deserialize)]
269pub struct WebsocketEndpointConfig {
270    /// WebSocket connection path
271    pub path: String,
272    /// Action to perform when connection is established
273    pub on_connect: Option<WebsocketAction>,
274    /// Action to perform when message is received
275    pub on_message: Option<WebsocketAction>,
276    /// Action to perform when connection is closed
277    pub on_disconnect: Option<WebsocketAction>,
278    /// Optional behavior configuration
279    pub behavior: Option<EndpointBehavior>,
280}
281
282/// WebSocket action type
283#[derive(Debug, Clone, Serialize, Deserialize)]
284#[serde(tag = "type")]
285pub enum WebsocketAction {
286    /// Send a message to the client
287    Send {
288        /// Message body to send
289        message: ResponseBody,
290    },
291    /// Broadcast a message to all connected clients
292    Broadcast {
293        /// Message body to broadcast
294        message: ResponseBody,
295    },
296    /// Echo received messages back to sender
297    Echo,
298    /// Close the connection
299    Close {
300        /// WebSocket close code
301        code: u16,
302        /// Close reason message
303        reason: String,
304    },
305}
306
307/// Configuration validation result
308#[derive(Debug, Clone, Serialize, Deserialize)]
309pub struct ValidationResult {
310    /// Whether the configuration is valid
311    pub valid: bool,
312    /// List of validation errors if any
313    pub errors: Vec<ValidationError>,
314    /// List of validation warnings if any
315    pub warnings: Vec<String>,
316}
317
318/// Validation error
319#[derive(Debug, Clone, Serialize, Deserialize)]
320pub struct ValidationError {
321    /// Field name that failed validation
322    pub field: String,
323    /// Error message describing the issue
324    pub message: String,
325}
326
327/// UI Builder state
328#[derive(Clone)]
329pub struct UIBuilderState {
330    /// Collection of endpoint configurations
331    pub endpoints: Arc<RwLock<Vec<EndpointConfig>>>,
332    /// Server configuration
333    pub server_config: Arc<RwLock<ServerConfig>>,
334}
335
336impl UIBuilderState {
337    /// Create a new UI builder state
338    ///
339    /// # Arguments
340    /// * `server_config` - Server configuration for the mock server
341    pub fn new(server_config: ServerConfig) -> Self {
342        Self {
343            endpoints: Arc::new(RwLock::new(Vec::new())),
344            server_config: Arc::new(RwLock::new(server_config)),
345        }
346    }
347}
348
349/// List all endpoints
350async fn list_endpoints(State(state): State<UIBuilderState>) -> Json<serde_json::Value> {
351    let endpoints = state.endpoints.read().await;
352    Json(serde_json::json!({
353        "endpoints": *endpoints,
354        "total": endpoints.len(),
355        "enabled": endpoints.iter().filter(|e| e.enabled).count(),
356        "by_protocol": {
357            "http": endpoints.iter().filter(|e| e.protocol == Protocol::Http).count(),
358            "grpc": endpoints.iter().filter(|e| e.protocol == Protocol::Grpc).count(),
359            "websocket": endpoints.iter().filter(|e| e.protocol == Protocol::Websocket).count(),
360        }
361    }))
362}
363
364/// Get a specific endpoint by ID
365async fn get_endpoint(
366    State(state): State<UIBuilderState>,
367    Path(id): Path<String>,
368) -> Result<Json<EndpointConfig>, StatusCode> {
369    let endpoints = state.endpoints.read().await;
370    endpoints
371        .iter()
372        .find(|e| e.id == id)
373        .cloned()
374        .map(Json)
375        .ok_or(StatusCode::NOT_FOUND)
376}
377
378/// Create a new endpoint
379async fn create_endpoint(
380    State(state): State<UIBuilderState>,
381    Json(mut endpoint): Json<EndpointConfig>,
382) -> Result<Json<EndpointConfig>, StatusCode> {
383    let mut endpoints = state.endpoints.write().await;
384
385    // Generate ID if not provided
386    if endpoint.id.is_empty() {
387        endpoint.id = uuid::Uuid::new_v4().to_string();
388    }
389
390    // Check for duplicate ID
391    if endpoints.iter().any(|e| e.id == endpoint.id) {
392        return Err(StatusCode::CONFLICT);
393    }
394
395    info!(
396        endpoint_id = %endpoint.id,
397        protocol = ?endpoint.protocol,
398        "Creating new endpoint"
399    );
400
401    endpoints.push(endpoint.clone());
402    Ok(Json(endpoint))
403}
404
405/// Update an existing endpoint
406async fn update_endpoint(
407    State(state): State<UIBuilderState>,
408    Path(id): Path<String>,
409    Json(updated): Json<EndpointConfig>,
410) -> Result<Json<EndpointConfig>, StatusCode> {
411    let mut endpoints = state.endpoints.write().await;
412
413    let endpoint = endpoints.iter_mut().find(|e| e.id == id).ok_or(StatusCode::NOT_FOUND)?;
414
415    info!(
416        endpoint_id = %id,
417        protocol = ?updated.protocol,
418        "Updating endpoint"
419    );
420
421    *endpoint = updated.clone();
422    Ok(Json(updated))
423}
424
425/// Delete an endpoint
426async fn delete_endpoint(
427    State(state): State<UIBuilderState>,
428    Path(id): Path<String>,
429) -> Result<StatusCode, StatusCode> {
430    let mut endpoints = state.endpoints.write().await;
431
432    let index = endpoints.iter().position(|e| e.id == id).ok_or(StatusCode::NOT_FOUND)?;
433
434    info!(endpoint_id = %id, "Deleting endpoint");
435
436    endpoints.remove(index);
437    Ok(StatusCode::NO_CONTENT)
438}
439
440/// Validate endpoint configuration
441async fn validate_endpoint(
442    State(_state): State<UIBuilderState>,
443    Json(endpoint): Json<EndpointConfig>,
444) -> Json<ValidationResult> {
445    let mut errors = Vec::new();
446    let mut warnings = Vec::new();
447
448    // Validate based on protocol
449    match &endpoint.config {
450        EndpointProtocolConfig::Http(http_config) => {
451            // Validate HTTP method
452            let valid_methods = ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"];
453            if !valid_methods.contains(&http_config.method.to_uppercase().as_str()) {
454                errors.push(ValidationError {
455                    field: "method".to_string(),
456                    message: format!("Invalid HTTP method: {}", http_config.method),
457                });
458            }
459
460            // Validate path
461            if !http_config.path.starts_with('/') {
462                errors.push(ValidationError {
463                    field: "path".to_string(),
464                    message: "Path must start with /".to_string(),
465                });
466            }
467
468            // Validate status code
469            if !(100..600).contains(&http_config.response.status) {
470                errors.push(ValidationError {
471                    field: "status".to_string(),
472                    message: "Status code must be between 100 and 599".to_string(),
473                });
474            }
475        }
476        EndpointProtocolConfig::Grpc(grpc_config) => {
477            // Validate gRPC service and method names
478            if grpc_config.service.is_empty() {
479                errors.push(ValidationError {
480                    field: "service".to_string(),
481                    message: "Service name is required".to_string(),
482                });
483            }
484            if grpc_config.method.is_empty() {
485                errors.push(ValidationError {
486                    field: "method".to_string(),
487                    message: "Method name is required".to_string(),
488                });
489            }
490        }
491        EndpointProtocolConfig::Websocket(ws_config) => {
492            // Validate WebSocket path
493            if !ws_config.path.starts_with('/') {
494                errors.push(ValidationError {
495                    field: "path".to_string(),
496                    message: "Path must start with /".to_string(),
497                });
498            }
499        }
500    }
501
502    // Check for behavior configuration warnings
503    if let Some(EndpointProtocolConfig::Http(http_config)) = Some(&endpoint.config) {
504        if let Some(behavior) = &http_config.behavior {
505            if let Some(failure) = &behavior.failure {
506                if failure.error_rate > 0.5 {
507                    warnings.push("High error rate configured (>50%)".to_string());
508                }
509            }
510        }
511    }
512
513    Json(ValidationResult {
514        valid: errors.is_empty(),
515        errors,
516        warnings,
517    })
518}
519
520/// Export configuration as YAML
521async fn export_config(
522    State(state): State<UIBuilderState>,
523) -> Result<impl IntoResponse, StatusCode> {
524    let server_config = state.server_config.read().await;
525
526    match serde_yaml::to_string(&*server_config) {
527        Ok(yaml) => Ok((StatusCode::OK, [("Content-Type", "application/x-yaml")], yaml)),
528        Err(e) => {
529            error!(error = %e, "Failed to serialize config to YAML");
530            Err(StatusCode::INTERNAL_SERVER_ERROR)
531        }
532    }
533}
534
535/// Import configuration from YAML/JSON
536#[derive(Debug, Deserialize)]
537struct ImportRequest {
538    config: String,
539    format: ConfigFormat,
540}
541
542#[derive(Debug, Deserialize)]
543#[serde(rename_all = "lowercase")]
544enum ConfigFormat {
545    Yaml,
546    Json,
547}
548
549async fn import_config(
550    State(state): State<UIBuilderState>,
551    Json(request): Json<ImportRequest>,
552) -> Result<Json<serde_json::Value>, StatusCode> {
553    let config: ServerConfig = match request.format {
554        ConfigFormat::Yaml => serde_yaml::from_str(&request.config).map_err(|e| {
555            error!(error = %e, "Failed to parse YAML config");
556            StatusCode::BAD_REQUEST
557        })?,
558        ConfigFormat::Json => serde_json::from_str(&request.config).map_err(|e| {
559            error!(error = %e, "Failed to parse JSON config");
560            StatusCode::BAD_REQUEST
561        })?,
562    };
563
564    let mut server_config = state.server_config.write().await;
565    *server_config = config;
566
567    info!("Configuration imported successfully");
568
569    Ok(Json(serde_json::json!({
570        "success": true,
571        "message": "Configuration imported successfully"
572    })))
573}
574
575/// Get current server configuration
576async fn get_config(State(state): State<UIBuilderState>) -> Json<ServerConfig> {
577    let config = state.server_config.read().await;
578    Json(config.clone())
579}
580
581/// Update server configuration
582async fn update_config(
583    State(state): State<UIBuilderState>,
584    Json(new_config): Json<ServerConfig>,
585) -> Result<Json<ServerConfig>, StatusCode> {
586    let mut config = state.server_config.write().await;
587    *config = new_config.clone();
588
589    info!("Server configuration updated");
590
591    Ok(Json(new_config))
592}
593
594/// Import OpenAPI specification request
595#[derive(Debug, Deserialize)]
596struct ImportOpenApiRequest {
597    content: String,
598    base_url: Option<String>,
599    auto_enable: Option<bool>,
600}
601
602/// Import OpenAPI specification response
603#[derive(Debug, Serialize)]
604struct ImportOpenApiResponse {
605    success: bool,
606    endpoints_created: usize,
607    warnings: Vec<String>,
608    spec_info: OpenApiSpecInfoResponse,
609}
610
611#[derive(Debug, Serialize)]
612struct OpenApiSpecInfoResponse {
613    title: String,
614    version: String,
615    description: Option<String>,
616    openapi_version: String,
617    servers: Vec<String>,
618}
619
620/// Import OpenAPI/Swagger specification and auto-generate endpoints
621async fn import_openapi_spec_handler(
622    State(state): State<UIBuilderState>,
623    Json(request): Json<ImportOpenApiRequest>,
624) -> Result<Json<ImportOpenApiResponse>, (StatusCode, Json<serde_json::Value>)> {
625    info!("Importing OpenAPI specification");
626
627    // Import the OpenAPI spec
628    let import_result = import_openapi_spec(&request.content, request.base_url.as_deref())
629        .map_err(|e| {
630            error!(error = %e, "Failed to import OpenAPI spec");
631            (
632                StatusCode::BAD_REQUEST,
633                Json(serde_json::json!({
634                    "error": "Failed to import OpenAPI specification",
635                    "details": e
636                })),
637            )
638        })?;
639
640    let auto_enable = request.auto_enable.unwrap_or(true);
641    let mut endpoints = state.endpoints.write().await;
642    let mut created_count = 0;
643
644    // Convert imported routes to EndpointConfig
645    for route in import_result.routes {
646        let endpoint_id = uuid::Uuid::new_v4().to_string();
647
648        // Convert response body to ResponseBody enum
649        let response_body = ResponseBody::Static {
650            content: route.response.body,
651        };
652
653        // Convert headers to HeaderConfig
654        let response_headers: Option<Vec<HeaderConfig>> = if route.response.headers.is_empty() {
655            None
656        } else {
657            Some(
658                route
659                    .response
660                    .headers
661                    .into_iter()
662                    .map(|(name, value)| HeaderConfig { name, value })
663                    .collect(),
664            )
665        };
666
667        let endpoint = EndpointConfig {
668            id: endpoint_id.clone(),
669            protocol: Protocol::Http,
670            name: format!("{} {}", route.method.to_uppercase(), route.path),
671            description: Some(format!(
672                "Auto-generated from OpenAPI spec: {} v{}",
673                import_result.spec_info.title, import_result.spec_info.version
674            )),
675            enabled: auto_enable,
676            config: EndpointProtocolConfig::Http(HttpEndpointConfig {
677                method: route.method.to_uppercase(),
678                path: route.path,
679                request: None,
680                response: HttpResponseConfig {
681                    status: route.response.status,
682                    headers: response_headers,
683                    body: response_body,
684                },
685                behavior: None,
686            }),
687        };
688
689        info!(
690            endpoint_id = %endpoint_id,
691            method = %endpoint.name,
692            "Created endpoint from OpenAPI spec"
693        );
694
695        endpoints.push(endpoint);
696        created_count += 1;
697    }
698
699    Ok(Json(ImportOpenApiResponse {
700        success: true,
701        endpoints_created: created_count,
702        warnings: import_result.warnings,
703        spec_info: OpenApiSpecInfoResponse {
704            title: import_result.spec_info.title,
705            version: import_result.spec_info.version,
706            description: import_result.spec_info.description,
707            openapi_version: import_result.spec_info.openapi_version,
708            servers: import_result.spec_info.servers,
709        },
710    }))
711}
712
713/// Export endpoints as OpenAPI specification
714async fn export_openapi_spec_handler(
715    State(state): State<UIBuilderState>,
716) -> Result<impl IntoResponse, StatusCode> {
717    let endpoints = state.endpoints.read().await;
718    let server_config = state.server_config.read().await;
719
720    // Build OpenAPI spec from endpoints
721    let http_host = &server_config.http.host;
722    let http_port = server_config.http.port;
723
724    let mut spec = serde_json::json!({
725        "openapi": "3.0.0",
726        "info": {
727            "title": "MockForge Generated API",
728            "version": "1.0.0",
729            "description": "API specification generated from MockForge endpoints"
730        },
731        "servers": [
732            {
733                "url": format!("http://{}:{}", http_host, http_port)
734            }
735        ],
736        "paths": {}
737    });
738
739    let paths = spec["paths"].as_object_mut().unwrap();
740
741    // Group endpoints by path
742    for endpoint in endpoints.iter() {
743        if endpoint.protocol != Protocol::Http {
744            continue; // Only export HTTP endpoints for now
745        }
746
747        if let EndpointProtocolConfig::Http(http_config) = &endpoint.config {
748            let path = &http_config.path;
749
750            if !paths.contains_key(path) {
751                paths.insert(path.clone(), serde_json::json!({}));
752            }
753
754            let method = http_config.method.to_lowercase();
755
756            // Build response body
757            let response_body_content = match &http_config.response.body {
758                ResponseBody::Static { content } => content.clone(),
759                ResponseBody::Template { template } => serde_json::json!({
760                    "type": "string",
761                    "example": template
762                }),
763                ResponseBody::Faker { schema } => schema.clone(),
764                ResponseBody::AI { prompt } => serde_json::json!({
765                    "type": "string",
766                    "description": prompt
767                }),
768            };
769
770            let operation = serde_json::json!({
771                "summary": &endpoint.name,
772                "description": endpoint.description.as_ref().unwrap_or(&String::new()),
773                "operationId": &endpoint.id,
774                "responses": {
775                    http_config.response.status.to_string(): {
776                        "description": format!("Response with status {}", http_config.response.status),
777                        "content": {
778                            "application/json": {
779                                "schema": {
780                                    "type": "object"
781                                },
782                                "example": response_body_content
783                            }
784                        }
785                    }
786                }
787            });
788
789            paths[path][&method] = operation;
790        }
791    }
792
793    match serde_json::to_string_pretty(&spec) {
794        Ok(json) => Ok((StatusCode::OK, [("Content-Type", "application/json")], json)),
795        Err(e) => {
796            error!(error = %e, "Failed to serialize OpenAPI spec to JSON");
797            Err(StatusCode::INTERNAL_SERVER_ERROR)
798        }
799    }
800}
801
802/// Import AsyncAPI specification request
803#[derive(Debug, Deserialize)]
804struct ImportAsyncApiRequest {
805    content: String,
806    base_url: Option<String>,
807    auto_enable: Option<bool>,
808}
809
810/// Import AsyncAPI specification response
811#[derive(Debug, Serialize)]
812struct ImportAsyncApiResponse {
813    success: bool,
814    endpoints_created: usize,
815    warnings: Vec<String>,
816    spec_info: AsyncApiSpecInfoResponse,
817}
818
819#[derive(Debug, Serialize)]
820struct AsyncApiSpecInfoResponse {
821    title: String,
822    version: String,
823    description: Option<String>,
824    asyncapi_version: String,
825    servers: Vec<String>,
826}
827
828/// Import AsyncAPI specification and auto-generate WebSocket/MQTT/Kafka endpoints
829async fn import_asyncapi_spec_handler(
830    State(state): State<UIBuilderState>,
831    Json(request): Json<ImportAsyncApiRequest>,
832) -> Result<Json<ImportAsyncApiResponse>, (StatusCode, Json<serde_json::Value>)> {
833    info!("Importing AsyncAPI specification");
834
835    // Import the AsyncAPI spec
836    let import_result = import_asyncapi_spec(&request.content, request.base_url.as_deref())
837        .map_err(|e| {
838            error!(error = %e, "Failed to import AsyncAPI spec");
839            (
840                StatusCode::BAD_REQUEST,
841                Json(serde_json::json!({
842                    "error": "Failed to import AsyncAPI specification",
843                    "details": e
844                })),
845            )
846        })?;
847
848    let auto_enable = request.auto_enable.unwrap_or(true);
849    let mut endpoints = state.endpoints.write().await;
850    let mut created_count = 0;
851
852    // Convert imported channels to EndpointConfig
853    for channel in import_result.channels {
854        let endpoint_id = uuid::Uuid::new_v4().to_string();
855
856        // Map AsyncAPI protocol to MockForge protocol
857        let (protocol, config) = match channel.protocol {
858            mockforge_core::import::asyncapi_import::ChannelProtocol::Websocket => {
859                // Create WebSocket endpoint
860                let on_message = if let Some(op) = channel.operations.first() {
861                    if let Some(example) = &op.example_message {
862                        Some(WebsocketAction::Send {
863                            message: ResponseBody::Static {
864                                content: example.clone(),
865                            },
866                        })
867                    } else {
868                        Some(WebsocketAction::Echo)
869                    }
870                } else {
871                    Some(WebsocketAction::Echo)
872                };
873
874                (
875                    Protocol::Websocket,
876                    EndpointProtocolConfig::Websocket(WebsocketEndpointConfig {
877                        path: channel.path.clone(),
878                        on_connect: None,
879                        on_message,
880                        on_disconnect: None,
881                        behavior: None,
882                    }),
883                )
884            }
885            _ => {
886                // For MQTT/Kafka/AMQP, we'll skip for now as they need special handling
887                // Log a warning and continue
888                warn!(
889                    "Skipping channel '{}' with protocol {:?} - not yet supported for UI Builder",
890                    channel.name, channel.protocol
891                );
892                continue;
893            }
894        };
895
896        let endpoint = EndpointConfig {
897            id: endpoint_id.clone(),
898            protocol,
899            name: format!("{} - {}", channel.name, channel.path),
900            description: channel.description.or_else(|| {
901                Some(format!(
902                    "Auto-generated from AsyncAPI spec: {} v{}",
903                    import_result.spec_info.title, import_result.spec_info.version
904                ))
905            }),
906            enabled: auto_enable,
907            config,
908        };
909
910        info!(
911            endpoint_id = %endpoint_id,
912            name = %endpoint.name,
913            "Created endpoint from AsyncAPI spec"
914        );
915
916        endpoints.push(endpoint);
917        created_count += 1;
918    }
919
920    Ok(Json(ImportAsyncApiResponse {
921        success: true,
922        endpoints_created: created_count,
923        warnings: import_result.warnings,
924        spec_info: AsyncApiSpecInfoResponse {
925            title: import_result.spec_info.title,
926            version: import_result.spec_info.version,
927            description: import_result.spec_info.description,
928            asyncapi_version: import_result.spec_info.asyncapi_version,
929            servers: import_result.spec_info.servers,
930        },
931    }))
932}
933
934/// Resolve tokens in a ResponseBody
935pub async fn resolve_response_body_tokens(
936    body: &ResponseBody,
937) -> Result<serde_json::Value, String> {
938    use crate::token_response::resolve_response_tokens;
939
940    match body {
941        ResponseBody::Static { content } => resolve_response_tokens(content.clone()).await,
942        ResponseBody::Template { template } => {
943            // Templates can also contain tokens, parse and resolve
944            let value = serde_json::Value::String(template.clone());
945            resolve_response_tokens(value).await
946        }
947        ResponseBody::Faker { schema } => {
948            // Faker schemas may contain tokens
949            resolve_response_tokens(schema.clone()).await
950        }
951        ResponseBody::AI { prompt: _ } => {
952            // AI prompts are handled separately by the AI handler
953            // Return as-is since AI generation happens at response time
954            Ok(serde_json::json!({"_ai_prompt": true}))
955        }
956    }
957}
958
959/// Create the UI Builder router
960pub fn create_ui_builder_router(state: UIBuilderState) -> Router {
961    Router::new()
962        .route("/endpoints", get(list_endpoints).post(create_endpoint))
963        .route(
964            "/endpoints/{id}",
965            get(get_endpoint).put(update_endpoint).delete(delete_endpoint),
966        )
967        .route("/endpoints/validate", post(validate_endpoint))
968        .route("/config", get(get_config).put(update_config))
969        .route("/config/export", get(export_config))
970        .route("/config/import", post(import_config))
971        .route("/openapi/import", post(import_openapi_spec_handler))
972        .route("/openapi/export", get(export_openapi_spec_handler))
973        .route("/asyncapi/import", post(import_asyncapi_spec_handler))
974        .with_state(state)
975}
976
977#[cfg(test)]
978mod tests {
979    use super::*;
980
981    #[test]
982    fn test_endpoint_serialization() {
983        let endpoint = EndpointConfig {
984            id: "test-1".to_string(),
985            protocol: Protocol::Http,
986            name: "Test Endpoint".to_string(),
987            description: Some("A test endpoint".to_string()),
988            enabled: true,
989            config: EndpointProtocolConfig::Http(HttpEndpointConfig {
990                method: "GET".to_string(),
991                path: "/test".to_string(),
992                request: None,
993                response: HttpResponseConfig {
994                    status: 200,
995                    headers: None,
996                    body: ResponseBody::Static {
997                        content: serde_json::json!({"message": "Hello"}),
998                    },
999                },
1000                behavior: None,
1001            }),
1002        };
1003
1004        let json = serde_json::to_string(&endpoint).unwrap();
1005        let deserialized: EndpointConfig = serde_json::from_str(&json).unwrap();
1006
1007        assert_eq!(endpoint.id, deserialized.id);
1008        assert_eq!(endpoint.protocol, deserialized.protocol);
1009    }
1010
1011    #[test]
1012    fn test_validation() {
1013        // Test invalid HTTP method
1014        let endpoint = EndpointConfig {
1015            id: "test-1".to_string(),
1016            protocol: Protocol::Http,
1017            name: "Test".to_string(),
1018            description: None,
1019            enabled: true,
1020            config: EndpointProtocolConfig::Http(HttpEndpointConfig {
1021                method: "INVALID".to_string(),
1022                path: "/test".to_string(),
1023                request: None,
1024                response: HttpResponseConfig {
1025                    status: 200,
1026                    headers: None,
1027                    body: ResponseBody::Static {
1028                        content: serde_json::json!({}),
1029                    },
1030                },
1031                behavior: None,
1032            }),
1033        };
1034
1035        // Validation would catch this in the async function
1036        assert_eq!(endpoint.protocol, Protocol::Http);
1037    }
1038}