1use axum::http::{HeaderMap, Method, Uri};
5use openapiv3;
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::fmt;
9use std::hash::{Hash, Hasher};
10
11#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
13pub struct RequestFingerprint {
14 pub method: String,
16 pub path: String,
18 pub query: String,
20 pub headers: HashMap<String, String>,
22 pub body_hash: Option<String>,
24}
25
26impl RequestFingerprint {
27 pub fn new(method: Method, uri: &Uri, headers: &HeaderMap, body: Option<&[u8]>) -> Self {
29 let mut query_parts = Vec::new();
30 if let Some(query) = uri.query() {
31 let mut params: Vec<&str> = query.split('&').collect();
32 params.sort(); query_parts = params;
34 }
35
36 let mut important_headers = HashMap::new();
38 let important_header_names = [
39 "authorization",
40 "content-type",
41 "accept",
42 "user-agent",
43 "x-request-id",
44 "x-api-key",
45 "x-auth-token",
46 ];
47
48 for header_name in &important_header_names {
49 if let Some(header_value) = headers.get(*header_name) {
50 if let Ok(value_str) = header_value.to_str() {
51 important_headers.insert(header_name.to_string(), value_str.to_string());
52 }
53 }
54 }
55
56 let body_hash = body.map(|b| {
58 use std::collections::hash_map::DefaultHasher;
59 let mut hasher = DefaultHasher::new();
60 b.hash(&mut hasher);
61 format!("{:x}", hasher.finish())
62 });
63
64 Self {
65 method: method.to_string(),
66 path: uri.path().to_string(),
67 query: query_parts.join("&"),
68 headers: important_headers,
69 body_hash,
70 }
71 }
72}
73
74impl fmt::Display for RequestFingerprint {
75 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76 let mut parts = Vec::new();
77 parts.push(self.method.clone());
78 parts.push(self.path.clone());
79 parts.push(self.query.clone());
80
81 let mut sorted_headers: Vec<_> = self.headers.iter().collect();
83 sorted_headers.sort_by_key(|(k, _)| *k);
84 for (key, value) in sorted_headers {
85 parts.push(format!("{}:{}", key, value));
86 }
87
88 if let Some(ref hash) = self.body_hash {
89 parts.push(format!("body:{}", hash));
90 }
91
92 write!(f, "{}", parts.join("|"))
93 }
94}
95
96impl RequestFingerprint {
97 pub fn to_hash(&self) -> String {
99 use std::collections::hash_map::DefaultHasher;
100 let mut hasher = DefaultHasher::new();
101 self.method.hash(&mut hasher);
102 self.path.hash(&mut hasher);
103 self.query.hash(&mut hasher);
104
105 let mut sorted_headers: Vec<_> = self.headers.iter().collect();
107 sorted_headers.sort_by_key(|(k, _)| *k);
108 for (k, v) in sorted_headers {
109 k.hash(&mut hasher);
110 v.hash(&mut hasher);
111 }
112
113 self.body_hash.hash(&mut hasher);
114 format!("{:x}", hasher.finish())
115 }
116
117 pub fn tags(&self) -> Vec<String> {
119 let mut tags = Vec::new();
121
122 for segment in self.path.split('/').filter(|s| !s.is_empty()) {
124 if !segment.starts_with('{') && !segment.starts_with(':') {
125 tags.push(segment.to_string());
126 }
127 }
128
129 tags.push(self.method.to_lowercase());
131
132 tags
133 }
134
135 pub fn openapi_tags(&self, spec: &crate::openapi::spec::OpenApiSpec) -> Option<Vec<String>> {
137 if let Some(operation) = self.find_operation(spec) {
139 let mut tags = operation.tags.clone();
140 if let Some(operation_id) = &operation.operation_id {
141 tags.push(operation_id.clone());
142 }
143 Some(tags)
144 } else {
145 None
146 }
147 }
148
149 fn find_operation<'a>(
151 &self,
152 spec: &'a crate::openapi::spec::OpenApiSpec,
153 ) -> Option<&'a openapiv3::Operation> {
154 if let Some(path_item) = spec.spec.paths.paths.get(&self.path) {
156 if let Some(item) = path_item.as_item() {
157 let operation = match self.method.as_str() {
159 "GET" => &item.get,
160 "POST" => &item.post,
161 "PUT" => &item.put,
162 "DELETE" => &item.delete,
163 "PATCH" => &item.patch,
164 "HEAD" => &item.head,
165 "OPTIONS" => &item.options,
166 "TRACE" => &item.trace,
167 _ => &None,
168 };
169 operation.as_ref()
170 } else {
171 None
172 }
173 } else {
174 None
175 }
176 }
177}
178
179#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
181pub enum ResponsePriority {
182 Replay = 0,
184 Stateful = 1,
186 Fail = 2,
188 Proxy = 3,
190 Mock = 4,
192 Record = 5,
194}
195
196#[derive(Debug, Clone)]
198pub struct ResponseSource {
199 pub priority: ResponsePriority,
201 pub source_type: String,
203 pub metadata: HashMap<String, String>,
205}
206
207impl ResponseSource {
208 pub fn new(priority: ResponsePriority, source_type: String) -> Self {
210 Self {
211 priority,
212 source_type,
213 metadata: HashMap::new(),
214 }
215 }
216
217 pub fn with_metadata(mut self, key: String, value: String) -> Self {
219 self.metadata.insert(key, value);
220 self
221 }
222}
223
224#[derive(Debug, Clone)]
226pub enum RequestHandlerResult {
227 Handled(ResponseSource),
229 Continue,
231 Error(String),
233}
234
235#[cfg(test)]
236mod tests {
237 use super::*;
238 use axum::http::Uri;
239
240 #[test]
241 fn test_request_fingerprint_creation() {
242 let method = Method::GET;
243 let uri = Uri::from_static("/api/users?page=1&limit=10");
244 let mut headers = HeaderMap::new();
245 headers.insert("authorization", "Bearer token123".parse().unwrap());
246 headers.insert("content-type", "application/json".parse().unwrap());
247
248 let fingerprint = RequestFingerprint::new(method, &uri, &headers, None);
249
250 assert_eq!(fingerprint.method, "GET");
251 assert_eq!(fingerprint.path, "/api/users");
252 assert_eq!(fingerprint.query, "limit=10&page=1"); assert_eq!(fingerprint.headers.get("authorization"), Some(&"Bearer token123".to_string()));
254 assert_eq!(fingerprint.headers.get("content-type"), Some(&"application/json".to_string()));
255 }
256
257 #[test]
258 fn test_fingerprint_consistency() {
259 let method = Method::POST;
260 let uri = Uri::from_static("/api/users?b=2&a=1");
261 let mut headers = HeaderMap::new();
262 headers.insert("x-api-key", "key123".parse().unwrap());
263 headers.insert("authorization", "Bearer token".parse().unwrap());
264
265 let fingerprint1 = RequestFingerprint::new(method.clone(), &uri, &headers, None);
266 let fingerprint2 = RequestFingerprint::new(method, &uri, &headers, None);
267
268 assert_eq!(fingerprint1.to_string(), fingerprint2.to_string());
270
271 assert_eq!(fingerprint1.to_hash(), fingerprint1.to_hash());
274 assert_eq!(fingerprint2.to_hash(), fingerprint2.to_hash());
275
276 assert_eq!(fingerprint1.to_hash(), fingerprint2.to_hash());
278
279 assert_eq!(fingerprint1, fingerprint2);
281 }
282
283 #[test]
284 fn test_response_priority_ordering() {
285 assert!(ResponsePriority::Replay < ResponsePriority::Stateful);
286 assert!(ResponsePriority::Stateful < ResponsePriority::Fail);
287 assert!(ResponsePriority::Fail < ResponsePriority::Proxy);
288 assert!(ResponsePriority::Proxy < ResponsePriority::Mock);
289 assert!(ResponsePriority::Mock < ResponsePriority::Record);
290 }
291
292 #[test]
293 fn test_openapi_tags() {
294 use crate::openapi::spec::OpenApiSpec;
295
296 let spec_json = r#"
297 {
298 "openapi": "3.0.0",
299 "info": {"title": "Test API", "version": "1.0.0"},
300 "paths": {
301 "/api/users": {
302 "get": {
303 "tags": ["users", "admin"],
304 "operationId": "getUsers",
305 "responses": {
306 "200": {
307 "description": "Success"
308 }
309 }
310 }
311 }
312 }
313 }
314 "#;
315
316 let spec = OpenApiSpec::from_json(serde_json::from_str(spec_json).unwrap()).unwrap();
317 let method = Method::GET;
318 let uri = Uri::from_static("/api/users");
319 let headers = HeaderMap::new();
320
321 let fingerprint = RequestFingerprint::new(method.clone(), &uri, &headers, None);
322
323 let tags = fingerprint.openapi_tags(&spec).unwrap();
324 assert_eq!(tags, vec!["users", "admin", "getUsers"]);
325
326 let uri2 = Uri::from_static("/api/posts");
328 let fingerprint2 = RequestFingerprint::new(method, &uri2, &headers, None);
329 let tags2 = fingerprint2.openapi_tags(&spec);
330 assert!(tags2.is_none());
331 }
332}