use axum::http::{HeaderMap, Method, Uri};
use openapiv3;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fmt;
use std::hash::{Hash, Hasher};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct RequestFingerprint {
pub method: String,
pub path: String,
pub query: String,
pub headers: HashMap<String, String>,
pub body_hash: Option<String>,
}
impl RequestFingerprint {
pub fn new(method: Method, uri: &Uri, headers: &HeaderMap, body: Option<&[u8]>) -> Self {
let mut query_parts = Vec::new();
if let Some(query) = uri.query() {
let mut params: Vec<&str> = query.split('&').collect();
params.sort(); query_parts = params;
}
let mut important_headers = HashMap::new();
let important_header_names = [
"authorization",
"content-type",
"accept",
"user-agent",
"x-request-id",
"x-api-key",
"x-auth-token",
];
for header_name in &important_header_names {
if let Some(header_value) = headers.get(*header_name) {
if let Ok(value_str) = header_value.to_str() {
important_headers.insert(header_name.to_string(), value_str.to_string());
}
}
}
let body_hash = body.map(|b| {
use std::collections::hash_map::DefaultHasher;
let mut hasher = DefaultHasher::new();
b.hash(&mut hasher);
format!("{:x}", hasher.finish())
});
Self {
method: method.to_string(),
path: uri.path().to_string(),
query: query_parts.join("&"),
headers: important_headers,
body_hash,
}
}
}
impl fmt::Display for RequestFingerprint {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut parts = Vec::new();
parts.push(self.method.clone());
parts.push(self.path.clone());
parts.push(self.query.clone());
let mut sorted_headers: Vec<_> = self.headers.iter().collect();
sorted_headers.sort_by_key(|(k, _)| *k);
for (key, value) in sorted_headers {
parts.push(format!("{}:{}", key, value));
}
if let Some(ref hash) = self.body_hash {
parts.push(format!("body:{}", hash));
}
write!(f, "{}", parts.join("|"))
}
}
impl RequestFingerprint {
pub fn to_hash(&self) -> String {
use std::collections::hash_map::DefaultHasher;
let mut hasher = DefaultHasher::new();
self.method.hash(&mut hasher);
self.path.hash(&mut hasher);
self.query.hash(&mut hasher);
let mut sorted_headers: Vec<_> = self.headers.iter().collect();
sorted_headers.sort_by_key(|(k, _)| *k);
for (k, v) in sorted_headers {
k.hash(&mut hasher);
v.hash(&mut hasher);
}
self.body_hash.hash(&mut hasher);
format!("{:x}", hasher.finish())
}
pub fn tags(&self) -> Vec<String> {
let mut tags = Vec::new();
for segment in self.path.split('/').filter(|s| !s.is_empty()) {
if !segment.starts_with('{') && !segment.starts_with(':') {
tags.push(segment.to_string());
}
}
tags.push(self.method.to_lowercase());
tags
}
pub fn openapi_tags(&self, spec: &crate::openapi::spec::OpenApiSpec) -> Option<Vec<String>> {
if let Some(operation) = self.find_operation(spec) {
let mut tags = operation.tags.clone();
if let Some(operation_id) = &operation.operation_id {
tags.push(operation_id.clone());
}
Some(tags)
} else {
None
}
}
fn find_operation<'a>(
&self,
spec: &'a crate::openapi::spec::OpenApiSpec,
) -> Option<&'a openapiv3::Operation> {
if let Some(path_item) = spec.spec.paths.paths.get(&self.path) {
if let Some(item) = path_item.as_item() {
let operation = match self.method.as_str() {
"GET" => &item.get,
"POST" => &item.post,
"PUT" => &item.put,
"DELETE" => &item.delete,
"PATCH" => &item.patch,
"HEAD" => &item.head,
"OPTIONS" => &item.options,
"TRACE" => &item.trace,
_ => &None,
};
operation.as_ref()
} else {
None
}
} else {
None
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum ResponsePriority {
Replay = 0,
Stateful = 1,
Fail = 2,
Proxy = 3,
Mock = 4,
Record = 5,
}
#[derive(Debug, Clone)]
pub struct ResponseSource {
pub priority: ResponsePriority,
pub source_type: String,
pub metadata: HashMap<String, String>,
}
impl ResponseSource {
pub fn new(priority: ResponsePriority, source_type: String) -> Self {
Self {
priority,
source_type,
metadata: HashMap::new(),
}
}
pub fn with_metadata(mut self, key: String, value: String) -> Self {
self.metadata.insert(key, value);
self
}
}
#[derive(Debug, Clone)]
pub enum RequestHandlerResult {
Handled(ResponseSource),
Continue,
Error(String),
}
#[cfg(test)]
mod tests {
use super::*;
use axum::http::Uri;
#[test]
fn test_request_fingerprint_creation() {
let method = Method::GET;
let uri = Uri::from_static("/api/users?page=1&limit=10");
let mut headers = HeaderMap::new();
headers.insert("authorization", "Bearer token123".parse().unwrap());
headers.insert("content-type", "application/json".parse().unwrap());
let fingerprint = RequestFingerprint::new(method, &uri, &headers, None);
assert_eq!(fingerprint.method, "GET");
assert_eq!(fingerprint.path, "/api/users");
assert_eq!(fingerprint.query, "limit=10&page=1"); assert_eq!(fingerprint.headers.get("authorization"), Some(&"Bearer token123".to_string()));
assert_eq!(fingerprint.headers.get("content-type"), Some(&"application/json".to_string()));
}
#[test]
fn test_fingerprint_consistency() {
let method = Method::POST;
let uri = Uri::from_static("/api/users?b=2&a=1");
let mut headers = HeaderMap::new();
headers.insert("x-api-key", "key123".parse().unwrap());
headers.insert("authorization", "Bearer token".parse().unwrap());
let fingerprint1 = RequestFingerprint::new(method.clone(), &uri, &headers, None);
let fingerprint2 = RequestFingerprint::new(method, &uri, &headers, None);
assert_eq!(fingerprint1.to_string(), fingerprint2.to_string());
assert_eq!(fingerprint1.to_hash(), fingerprint1.to_hash());
assert_eq!(fingerprint2.to_hash(), fingerprint2.to_hash());
assert_eq!(fingerprint1.to_hash(), fingerprint2.to_hash());
assert_eq!(fingerprint1, fingerprint2);
}
#[test]
fn test_response_priority_ordering() {
assert!(ResponsePriority::Replay < ResponsePriority::Stateful);
assert!(ResponsePriority::Stateful < ResponsePriority::Fail);
assert!(ResponsePriority::Fail < ResponsePriority::Proxy);
assert!(ResponsePriority::Proxy < ResponsePriority::Mock);
assert!(ResponsePriority::Mock < ResponsePriority::Record);
}
#[test]
fn test_openapi_tags() {
use crate::openapi::spec::OpenApiSpec;
let spec_json = r#"
{
"openapi": "3.0.0",
"info": {"title": "Test API", "version": "1.0.0"},
"paths": {
"/api/users": {
"get": {
"tags": ["users", "admin"],
"operationId": "getUsers",
"responses": {
"200": {
"description": "Success"
}
}
}
}
}
}
"#;
let spec = OpenApiSpec::from_json(serde_json::from_str(spec_json).unwrap()).unwrap();
let method = Method::GET;
let uri = Uri::from_static("/api/users");
let headers = HeaderMap::new();
let fingerprint = RequestFingerprint::new(method.clone(), &uri, &headers, None);
let tags = fingerprint.openapi_tags(&spec).unwrap();
assert_eq!(tags, vec!["users", "admin", "getUsers"]);
let uri2 = Uri::from_static("/api/posts");
let fingerprint2 = RequestFingerprint::new(method, &uri2, &headers, None);
let tags2 = fingerprint2.openapi_tags(&spec);
assert!(tags2.is_none());
}
}