mockforge_recorder/
models.rs

1//! Data models for recorded requests and responses
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6
7/// Protocol type
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)]
9#[sqlx(type_name = "TEXT")]
10#[sqlx(rename_all = "lowercase")]
11pub enum Protocol {
12    #[sqlx(rename = "http")]
13    Http,
14    #[sqlx(rename = "grpc")]
15    Grpc,
16    #[sqlx(rename = "websocket")]
17    WebSocket,
18    #[sqlx(rename = "graphql")]
19    GraphQL,
20}
21
22impl Protocol {
23    pub fn as_str(&self) -> &'static str {
24        match self {
25            Protocol::Http => "http",
26            Protocol::Grpc => "grpc",
27            Protocol::WebSocket => "websocket",
28            Protocol::GraphQL => "graphql",
29        }
30    }
31}
32
33impl std::fmt::Display for Protocol {
34    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
35        write!(f, "{}", self.as_str())
36    }
37}
38
39/// Tracing context for requests (OpenTelemetry)
40#[derive(Debug, Clone, Default)]
41pub struct RequestContext {
42    /// Client IP address
43    pub client_ip: Option<String>,
44    /// Trace ID (from OpenTelemetry)
45    pub trace_id: Option<String>,
46    /// Span ID (from OpenTelemetry)
47    pub span_id: Option<String>,
48}
49
50impl RequestContext {
51    /// Create a new request context
52    pub fn new(client_ip: Option<&str>, trace_id: Option<&str>, span_id: Option<&str>) -> Self {
53        Self {
54            client_ip: client_ip.map(|s| s.to_string()),
55            trace_id: trace_id.map(|s| s.to_string()),
56            span_id: span_id.map(|s| s.to_string()),
57        }
58    }
59}
60
61/// Recorded HTTP/API request
62#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct RecordedRequest {
64    /// Unique request ID
65    pub id: String,
66    /// Protocol type
67    pub protocol: Protocol,
68    /// Timestamp
69    pub timestamp: DateTime<Utc>,
70    /// HTTP method or gRPC method name
71    pub method: String,
72    /// Request path or endpoint
73    pub path: String,
74    /// Query parameters (for HTTP)
75    pub query_params: Option<String>,
76    /// Request headers (JSON)
77    pub headers: String,
78    /// Request body (may be base64 encoded for binary)
79    pub body: Option<String>,
80    /// Body encoding (utf8, base64)
81    pub body_encoding: String,
82    /// Client IP address
83    pub client_ip: Option<String>,
84    /// Trace ID (from OpenTelemetry)
85    pub trace_id: Option<String>,
86    /// Span ID (from OpenTelemetry)
87    pub span_id: Option<String>,
88    /// Duration in milliseconds
89    pub duration_ms: Option<i64>,
90    /// Response status code
91    pub status_code: Option<i32>,
92    /// Tags for categorization (JSON array)
93    pub tags: Option<String>,
94}
95
96impl RecordedRequest {
97    /// Parse headers from JSON string
98    pub fn headers_map(&self) -> HashMap<String, String> {
99        serde_json::from_str(&self.headers).unwrap_or_default()
100    }
101
102    /// Parse query parameters
103    pub fn query_params_map(&self) -> HashMap<String, String> {
104        self.query_params
105            .as_ref()
106            .and_then(|q| serde_json::from_str(q).ok())
107            .unwrap_or_default()
108    }
109
110    /// Parse tags
111    pub fn tags_vec(&self) -> Vec<String> {
112        self.tags
113            .as_ref()
114            .and_then(|t| serde_json::from_str(t).ok())
115            .unwrap_or_default()
116    }
117
118    /// Decode body based on encoding
119    pub fn decoded_body(&self) -> Option<Vec<u8>> {
120        self.body.as_ref().map(|body| {
121            if self.body_encoding == "base64" {
122                base64::Engine::decode(&base64::engine::general_purpose::STANDARD, body)
123                    .unwrap_or_else(|_| body.as_bytes().to_vec())
124            } else {
125                body.as_bytes().to_vec()
126            }
127        })
128    }
129}
130
131/// Recorded response
132#[derive(Debug, Clone, Serialize, Deserialize)]
133pub struct RecordedResponse {
134    /// Response ID (same as request ID)
135    pub request_id: String,
136    /// Response status code
137    pub status_code: i32,
138    /// Response headers (JSON)
139    pub headers: String,
140    /// Response body (may be base64 encoded for binary)
141    pub body: Option<String>,
142    /// Body encoding (utf8, base64)
143    pub body_encoding: String,
144    /// Response size in bytes
145    pub size_bytes: i64,
146    /// Timestamp
147    pub timestamp: DateTime<Utc>,
148}
149
150impl RecordedResponse {
151    /// Parse headers from JSON string
152    pub fn headers_map(&self) -> HashMap<String, String> {
153        serde_json::from_str(&self.headers).unwrap_or_default()
154    }
155
156    /// Decode body based on encoding
157    pub fn decoded_body(&self) -> Option<Vec<u8>> {
158        self.body.as_ref().map(|body| {
159            if self.body_encoding == "base64" {
160                base64::Engine::decode(&base64::engine::general_purpose::STANDARD, body)
161                    .unwrap_or_else(|_| body.as_bytes().to_vec())
162            } else {
163                body.as_bytes().to_vec()
164            }
165        })
166    }
167}
168
169/// Request/Response pair
170#[derive(Debug, Clone, Serialize, Deserialize)]
171pub struct RecordedExchange {
172    pub request: RecordedRequest,
173    pub response: Option<RecordedResponse>,
174}
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179
180    #[test]
181    fn test_protocol_display() {
182        assert_eq!(Protocol::Http.to_string(), "http");
183        assert_eq!(Protocol::Grpc.to_string(), "grpc");
184        assert_eq!(Protocol::WebSocket.to_string(), "websocket");
185        assert_eq!(Protocol::GraphQL.to_string(), "graphql");
186    }
187
188    #[test]
189    fn test_headers_parsing() {
190        let request = RecordedRequest {
191            id: "test".to_string(),
192            protocol: Protocol::Http,
193            timestamp: Utc::now(),
194            method: "GET".to_string(),
195            path: "/test".to_string(),
196            query_params: None,
197            headers: r#"{"content-type":"application/json"}"#.to_string(),
198            body: None,
199            body_encoding: "utf8".to_string(),
200            client_ip: None,
201            trace_id: None,
202            span_id: None,
203            duration_ms: None,
204            status_code: None,
205            tags: Some(r#"["test","api"]"#.to_string()),
206        };
207
208        let headers = request.headers_map();
209        assert_eq!(headers.get("content-type"), Some(&"application/json".to_string()));
210
211        let tags = request.tags_vec();
212        assert_eq!(tags, vec!["test", "api"]);
213    }
214}