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 Fail = 1,
186 Proxy = 2,
188 Mock = 3,
190 Record = 4,
192}
193
194#[derive(Debug, Clone)]
196pub struct ResponseSource {
197 pub priority: ResponsePriority,
199 pub source_type: String,
201 pub metadata: HashMap<String, String>,
203}
204
205impl ResponseSource {
206 pub fn new(priority: ResponsePriority, source_type: String) -> Self {
208 Self {
209 priority,
210 source_type,
211 metadata: HashMap::new(),
212 }
213 }
214
215 pub fn with_metadata(mut self, key: String, value: String) -> Self {
217 self.metadata.insert(key, value);
218 self
219 }
220}
221
222#[derive(Debug, Clone)]
224pub enum RequestHandlerResult {
225 Handled(ResponseSource),
227 Continue,
229 Error(String),
231}
232
233#[cfg(test)]
234mod tests {
235 use super::*;
236 use axum::http::Uri;
237
238 #[test]
239 fn test_request_fingerprint_creation() {
240 let method = Method::GET;
241 let uri = Uri::from_static("/api/users?page=1&limit=10");
242 let mut headers = HeaderMap::new();
243 headers.insert("authorization", "Bearer token123".parse().unwrap());
244 headers.insert("content-type", "application/json".parse().unwrap());
245
246 let fingerprint = RequestFingerprint::new(method, &uri, &headers, None);
247
248 assert_eq!(fingerprint.method, "GET");
249 assert_eq!(fingerprint.path, "/api/users");
250 assert_eq!(fingerprint.query, "limit=10&page=1"); assert_eq!(fingerprint.headers.get("authorization"), Some(&"Bearer token123".to_string()));
252 assert_eq!(fingerprint.headers.get("content-type"), Some(&"application/json".to_string()));
253 }
254
255 #[test]
256 fn test_fingerprint_consistency() {
257 let method = Method::POST;
258 let uri = Uri::from_static("/api/users?b=2&a=1");
259 let mut headers = HeaderMap::new();
260 headers.insert("x-api-key", "key123".parse().unwrap());
261 headers.insert("authorization", "Bearer token".parse().unwrap());
262
263 let fingerprint1 = RequestFingerprint::new(method.clone(), &uri, &headers, None);
264 let fingerprint2 = RequestFingerprint::new(method, &uri, &headers, None);
265
266 assert_eq!(fingerprint1.to_string(), fingerprint2.to_string());
268
269 assert_eq!(fingerprint1.to_hash(), fingerprint1.to_hash());
272 assert_eq!(fingerprint2.to_hash(), fingerprint2.to_hash());
273
274 assert_eq!(fingerprint1.to_hash(), fingerprint2.to_hash());
276
277 assert_eq!(fingerprint1, fingerprint2);
279 }
280
281 #[test]
282 fn test_response_priority_ordering() {
283 assert!(ResponsePriority::Replay < ResponsePriority::Fail);
284 assert!(ResponsePriority::Fail < ResponsePriority::Proxy);
285 assert!(ResponsePriority::Proxy < ResponsePriority::Mock);
286 assert!(ResponsePriority::Mock < ResponsePriority::Record);
287 }
288
289 #[test]
290 fn test_openapi_tags() {
291 use crate::openapi::spec::OpenApiSpec;
292
293 let spec_json = r#"
294 {
295 "openapi": "3.0.0",
296 "info": {"title": "Test API", "version": "1.0.0"},
297 "paths": {
298 "/api/users": {
299 "get": {
300 "tags": ["users", "admin"],
301 "operationId": "getUsers",
302 "responses": {
303 "200": {
304 "description": "Success"
305 }
306 }
307 }
308 }
309 }
310 }
311 "#;
312
313 let spec = OpenApiSpec::from_json(serde_json::from_str(spec_json).unwrap()).unwrap();
314 let method = Method::GET;
315 let uri = Uri::from_static("/api/users");
316 let headers = HeaderMap::new();
317
318 let fingerprint = RequestFingerprint::new(method.clone(), &uri, &headers, None);
319
320 let tags = fingerprint.openapi_tags(&spec).unwrap();
321 assert_eq!(tags, vec!["users", "admin", "getUsers"]);
322
323 let uri2 = Uri::from_static("/api/posts");
325 let fingerprint2 = RequestFingerprint::new(method, &uri2, &headers, None);
326 let tags2 = fingerprint2.openapi_tags(&spec);
327 assert!(tags2.is_none());
328 }
329}