mockforge_recorder/
openapi_export.rs

1//! OpenAPI export utilities for recorder
2//!
3//! This module provides conversion from recorded HTTP exchanges to
4//! the HttpExchange format used by the OpenAPI generator.
5
6use crate::{
7    database::RecorderDatabase,
8    models::{Protocol, RecordedExchange, RecordedRequest, RecordedResponse},
9    query::{execute_query, QueryFilter},
10    Result,
11};
12use chrono::{DateTime, Utc};
13use mockforge_core::intelligent_behavior::openapi_generator::HttpExchange;
14
15/// Converter from recorded exchanges to HttpExchange format
16pub struct RecordingsToOpenApi;
17
18impl RecordingsToOpenApi {
19    /// Convert RecordedExchange to HttpExchange
20    pub fn convert_exchange(exchange: &RecordedExchange) -> HttpExchange {
21        HttpExchange {
22            method: exchange.request.method.clone(),
23            path: exchange.request.path.clone(),
24            query_params: exchange.request.query_params.clone(),
25            headers: exchange.request.headers.clone(),
26            body: exchange.request.body.clone(),
27            body_encoding: exchange.request.body_encoding.clone(),
28            status_code: exchange.response.as_ref().map(|r| r.status_code),
29            response_headers: exchange.response.as_ref().map(|r| r.headers.clone()),
30            response_body: exchange.response.as_ref().and_then(|r| r.body.clone()),
31            response_body_encoding: exchange.response.as_ref().map(|r| r.body_encoding.clone()),
32            timestamp: exchange.request.timestamp,
33        }
34    }
35
36    /// Convert a single RecordedRequest/RecordedResponse pair to HttpExchange
37    pub fn convert_request_response(
38        request: &RecordedRequest,
39        response: Option<&RecordedResponse>,
40    ) -> HttpExchange {
41        HttpExchange {
42            method: request.method.clone(),
43            path: request.path.clone(),
44            query_params: request.query_params.clone(),
45            headers: request.headers.clone(),
46            body: request.body.clone(),
47            body_encoding: request.body_encoding.clone(),
48            status_code: response.map(|r| r.status_code),
49            response_headers: response.map(|r| r.headers.clone()),
50            response_body: response.and_then(|r| r.body.clone()),
51            response_body_encoding: response.map(|r| r.body_encoding.clone()),
52            timestamp: request.timestamp,
53        }
54    }
55
56    /// Convert multiple RecordedExchange to HttpExchange
57    pub fn convert_exchanges(exchanges: &[RecordedExchange]) -> Vec<HttpExchange> {
58        exchanges.iter().map(Self::convert_exchange).collect()
59    }
60
61    /// Query HTTP exchanges from database and convert to HttpExchange format
62    ///
63    /// This method queries the recorder database for HTTP requests/responses
64    /// and converts them to the format expected by the OpenAPI generator.
65    pub async fn query_http_exchanges(
66        db: &RecorderDatabase,
67        filters: Option<QueryFilters>,
68    ) -> Result<Vec<HttpExchange>> {
69        let mut query_filter = QueryFilter {
70            protocol: Some(Protocol::Http),
71            limit: filters.as_ref().and_then(|f| f.max_requests).map(|n| n as i32).or(Some(1000)),
72            ..Default::default()
73        };
74
75        // Apply path pattern filters
76        if let Some(ref filters) = filters {
77            if let Some(ref path_pattern) = filters.path_pattern {
78                query_filter.path = Some(path_pattern.clone());
79            }
80
81            if let Some(status) = filters.min_status_code {
82                query_filter.status_code = Some(status);
83            }
84        }
85
86        // Execute query
87        let query_result = execute_query(db, query_filter).await?;
88
89        // Convert to HttpExchange format
90        let mut exchanges = Self::convert_exchanges(&query_result.exchanges);
91
92        // Apply time range filters if specified
93        if let Some(ref filters) = filters {
94            if let Some(since) = filters.since {
95                exchanges.retain(|e| e.timestamp >= since);
96            }
97
98            if let Some(until) = filters.until {
99                exchanges.retain(|e| e.timestamp <= until);
100            }
101        }
102
103        Ok(exchanges)
104    }
105}
106
107/// Query filters for OpenAPI generation
108#[derive(Debug, Clone)]
109pub struct QueryFilters {
110    /// Time range filter: start timestamp (optional)
111    pub since: Option<DateTime<Utc>>,
112
113    /// Time range filter: end timestamp (optional)
114    pub until: Option<DateTime<Utc>>,
115
116    /// Path pattern filter (supports wildcards)
117    pub path_pattern: Option<String>,
118
119    /// Minimum status code filter
120    pub min_status_code: Option<i32>,
121
122    /// Maximum number of requests to analyze
123    pub max_requests: Option<usize>,
124}
125
126impl Default for QueryFilters {
127    fn default() -> Self {
128        Self {
129            since: None,
130            until: None,
131            path_pattern: None,
132            min_status_code: None,
133            max_requests: Some(1000),
134        }
135    }
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141    use crate::models::{RecordedRequest, RecordedResponse};
142
143    #[test]
144    fn test_convert_exchange() {
145        let request = RecordedRequest {
146            id: "test-123".to_string(),
147            protocol: Protocol::Http,
148            timestamp: Utc::now(),
149            method: "GET".to_string(),
150            path: "/api/test".to_string(),
151            query_params: None,
152            headers: "{}".to_string(),
153            body: None,
154            body_encoding: "utf8".to_string(),
155            client_ip: None,
156            trace_id: None,
157            span_id: None,
158            duration_ms: None,
159            status_code: Some(200),
160            tags: None,
161        };
162
163        let response = Some(RecordedResponse {
164            request_id: "test-123".to_string(),
165            status_code: 200,
166            headers: "{}".to_string(),
167            body: Some(r#"{"result": "ok"}"#.to_string()),
168            body_encoding: "utf8".to_string(),
169            size_bytes: 15,
170            timestamp: Utc::now(),
171        });
172
173        let exchange = RecordedExchange { request, response };
174        let http_exchange = RecordingsToOpenApi::convert_exchange(&exchange);
175
176        assert_eq!(http_exchange.method, "GET");
177        assert_eq!(http_exchange.path, "/api/test");
178        assert_eq!(http_exchange.status_code, Some(200));
179    }
180}