1use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6
7#[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#[derive(Debug, Clone, Default)]
41pub struct RequestContext {
42 pub client_ip: Option<String>,
44 pub trace_id: Option<String>,
46 pub span_id: Option<String>,
48}
49
50impl RequestContext {
51 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#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct RecordedRequest {
64 pub id: String,
66 pub protocol: Protocol,
68 pub timestamp: DateTime<Utc>,
70 pub method: String,
72 pub path: String,
74 pub query_params: Option<String>,
76 pub headers: String,
78 pub body: Option<String>,
80 pub body_encoding: String,
82 pub client_ip: Option<String>,
84 pub trace_id: Option<String>,
86 pub span_id: Option<String>,
88 pub duration_ms: Option<i64>,
90 pub status_code: Option<i32>,
92 pub tags: Option<String>,
94}
95
96impl RecordedRequest {
97 pub fn headers_map(&self) -> HashMap<String, String> {
99 serde_json::from_str(&self.headers).unwrap_or_default()
100 }
101
102 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 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
133pub struct RecordedResponse {
134 pub request_id: String,
136 pub status_code: i32,
138 pub headers: String,
140 pub body: Option<String>,
142 pub body_encoding: String,
144 pub size_bytes: i64,
146 pub timestamp: DateTime<Utc>,
148}
149
150impl RecordedResponse {
151 pub fn headers_map(&self) -> HashMap<String, String> {
153 serde_json::from_str(&self.headers).unwrap_or_default()
154 }
155
156 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#[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}