Skip to main content

mockforge_http/management/
traffic_to_openapi.rs

1// Uses deprecated `mockforge_core::intelligent_behavior::openapi_generator`
2// wrappers pending the eventual intelligent_behavior migration.
3#![allow(deprecated)]
4
5//! OpenAPI inference from recorded traffic
6//! (`POST /__mockforge/mockai/generate-openapi`).
7//!
8//! Split out of the original `management/ai_gen.rs` under #656. This
9//! handler reads recorded HTTP exchanges from `mockforge-recorder` and
10//! synthesizes an OpenAPI spec via `mockforge_core::intelligent_behavior`.
11//!
12//! Stays in `mockforge-http` rather than moving to `mockforge-intelligence`:
13//! `mockforge-recorder` already depends on `mockforge-intelligence`, so
14//! the orchestration layer (this file) can't live in intelligence
15//! without re-introducing a cycle — same constraint as
16//! `handlers::behavioral_cloning`.
17
18use serde::Deserialize;
19
20// Axum + ManagementState imports are only consumed by the feature-gated
21// handler below; gate the imports to match so non-`behavioral-cloning`
22// builds don't trip the unused-imports lint.
23#[cfg(feature = "behavioral-cloning")]
24use super::ManagementState;
25#[cfg(feature = "behavioral-cloning")]
26use axum::{
27    extract::State,
28    http::StatusCode,
29    response::{IntoResponse, Json},
30};
31
32/// Request for OpenAPI generation from recorded traffic
33#[derive(Debug, Deserialize)]
34pub struct GenerateOpenApiFromTrafficRequest {
35    /// Path to recorder database (optional, defaults to ./recordings.db)
36    #[serde(default)]
37    pub database_path: Option<String>,
38    /// Start time for filtering (ISO 8601 format, e.g., 2025-01-01T00:00:00Z)
39    #[serde(default)]
40    pub since: Option<String>,
41    /// End time for filtering (ISO 8601 format)
42    #[serde(default)]
43    pub until: Option<String>,
44    /// Path pattern filter (supports wildcards, e.g., /api/*)
45    #[serde(default)]
46    pub path_pattern: Option<String>,
47    /// Minimum confidence score for including paths (0.0 to 1.0)
48    #[serde(default = "default_min_confidence")]
49    pub min_confidence: f64,
50}
51
52fn default_min_confidence() -> f64 {
53    0.7
54}
55
56/// Generate OpenAPI specification from recorded traffic
57#[cfg(feature = "behavioral-cloning")]
58pub(crate) async fn generate_openapi_from_traffic(
59    State(_state): State<ManagementState>,
60    Json(request): Json<GenerateOpenApiFromTrafficRequest>,
61) -> impl IntoResponse {
62    use chrono::{DateTime, Utc};
63    use mockforge_core::intelligent_behavior::{
64        openapi_generator::{OpenApiGenerationConfig, OpenApiSpecGenerator},
65        IntelligentBehaviorConfig,
66    };
67    use mockforge_recorder::{
68        database::RecorderDatabase,
69        openapi_export::{QueryFilters, RecordingsToOpenApi},
70    };
71    use std::path::PathBuf;
72
73    // Determine database path
74    let db_path = if let Some(ref path) = request.database_path {
75        PathBuf::from(path)
76    } else {
77        std::env::current_dir()
78            .unwrap_or_else(|_| PathBuf::from("."))
79            .join("recordings.db")
80    };
81
82    // Open database
83    let db = match RecorderDatabase::new(&db_path).await {
84        Ok(db) => db,
85        Err(e) => {
86            return (
87                StatusCode::BAD_REQUEST,
88                Json(serde_json::json!({
89                    "error": "Database error",
90                    "message": format!("Failed to open recorder database: {}", e)
91                })),
92            )
93                .into_response();
94        }
95    };
96
97    // Parse time filters
98    let since_dt = if let Some(ref since_str) = request.since {
99        match DateTime::parse_from_rfc3339(since_str) {
100            Ok(dt) => Some(dt.with_timezone(&Utc)),
101            Err(e) => {
102                return (
103                    StatusCode::BAD_REQUEST,
104                    Json(serde_json::json!({
105                        "error": "Invalid date format",
106                        "message": format!("Invalid --since format: {}. Use ISO 8601 format (e.g., 2025-01-01T00:00:00Z)", e)
107                    })),
108                )
109                    .into_response();
110            }
111        }
112    } else {
113        None
114    };
115
116    let until_dt = if let Some(ref until_str) = request.until {
117        match DateTime::parse_from_rfc3339(until_str) {
118            Ok(dt) => Some(dt.with_timezone(&Utc)),
119            Err(e) => {
120                return (
121                    StatusCode::BAD_REQUEST,
122                    Json(serde_json::json!({
123                        "error": "Invalid date format",
124                        "message": format!("Invalid --until format: {}. Use ISO 8601 format (e.g., 2025-01-01T00:00:00Z)", e)
125                    })),
126                )
127                    .into_response();
128            }
129        }
130    } else {
131        None
132    };
133
134    // Build query filters
135    let query_filters = QueryFilters {
136        since: since_dt,
137        until: until_dt,
138        path_pattern: request.path_pattern.clone(),
139        min_status_code: None,
140        max_requests: Some(1000),
141    };
142
143    // Query HTTP exchanges
144    // Note: We need to convert from mockforge-recorder's HttpExchange to mockforge-core's HttpExchange
145    // to avoid version mismatch issues. The converter returns the version from mockforge-recorder's
146    // dependency, so we need to manually convert to the local version.
147    let exchanges_from_recorder =
148        match RecordingsToOpenApi::query_http_exchanges(&db, Some(query_filters)).await {
149            Ok(exchanges) => exchanges,
150            Err(e) => {
151                return (
152                    StatusCode::INTERNAL_SERVER_ERROR,
153                    Json(serde_json::json!({
154                        "error": "Query error",
155                        "message": format!("Failed to query HTTP exchanges: {}", e)
156                    })),
157                )
158                    .into_response();
159            }
160        };
161
162    if exchanges_from_recorder.is_empty() {
163        return (
164            StatusCode::NOT_FOUND,
165            Json(serde_json::json!({
166                "error": "No exchanges found",
167                "message": "No HTTP exchanges found matching the specified filters"
168            })),
169        )
170            .into_response();
171    }
172
173    // Convert to local HttpExchange type to avoid version mismatch
174    use mockforge_core::intelligent_behavior::openapi_generator::HttpExchange as LocalHttpExchange;
175    let exchanges: Vec<LocalHttpExchange> = exchanges_from_recorder
176        .into_iter()
177        .map(|e| LocalHttpExchange {
178            method: e.method,
179            path: e.path,
180            query_params: e.query_params,
181            headers: e.headers,
182            body: e.body,
183            body_encoding: e.body_encoding,
184            status_code: e.status_code,
185            response_headers: e.response_headers,
186            response_body: e.response_body,
187            response_body_encoding: e.response_body_encoding,
188            timestamp: e.timestamp,
189        })
190        .collect();
191
192    // Create OpenAPI generator config
193    let behavior_config = IntelligentBehaviorConfig::default();
194    let gen_config = OpenApiGenerationConfig {
195        min_confidence: request.min_confidence,
196        behavior_model: Some(behavior_config.behavior_model),
197    };
198
199    // Generate OpenAPI spec
200    let generator = OpenApiSpecGenerator::new(gen_config);
201    let result = match generator.generate_from_exchanges(exchanges).await {
202        Ok(result) => result,
203        Err(e) => {
204            return (
205                StatusCode::INTERNAL_SERVER_ERROR,
206                Json(serde_json::json!({
207                    "error": "Generation error",
208                    "message": format!("Failed to generate OpenAPI spec: {}", e)
209                })),
210            )
211                .into_response();
212        }
213    };
214
215    // Prepare response
216    let spec_json = if let Some(ref raw) = result.spec.raw_document {
217        raw.clone()
218    } else {
219        match serde_json::to_value(&result.spec.spec) {
220            Ok(json) => json,
221            Err(e) => {
222                return (
223                    StatusCode::INTERNAL_SERVER_ERROR,
224                    Json(serde_json::json!({
225                        "error": "Serialization error",
226                        "message": format!("Failed to serialize OpenAPI spec: {}", e)
227                    })),
228                )
229                    .into_response();
230            }
231        }
232    };
233
234    // Build response with metadata
235    let response = serde_json::json!({
236        "spec": spec_json,
237        "metadata": {
238            "requests_analyzed": result.metadata.requests_analyzed,
239            "paths_inferred": result.metadata.paths_inferred,
240            "path_confidence": result.metadata.path_confidence,
241            "generated_at": result.metadata.generated_at.to_rfc3339(),
242            "duration_ms": result.metadata.duration_ms,
243        }
244    });
245
246    Json(response).into_response()
247}