synapse_pingora/utils/
path_normalizer.rs1use lazy_static::lazy_static;
2use regex::Regex;
3
4lazy_static! {
5 static ref UUID_REGEX: Regex = Regex::new(
7 r"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"
8 ).unwrap();
9
10 static ref NUMERIC_ID_REGEX: Regex = Regex::new(
12 r"^\d+$"
13 ).unwrap();
14
15 static ref BASE64_ID_REGEX: Regex = Regex::new(
17 r"^[A-Za-z0-9_-]{16,}$"
18 ).unwrap();
19
20 static ref OBJECTID_REGEX: Regex = Regex::new(
22 r"^[0-9a-fA-F]{24}$"
23 ).unwrap();
24}
25
26pub fn normalize_path(path: &str) -> String {
28 if path == "/" || path.is_empty() {
29 return path.to_string();
30 }
31
32 let mut path_str = path.to_string();
33 if path_str.len() > 1 && path_str.ends_with('/') {
35 path_str.pop();
36 }
37
38 let parts: Vec<&str> = path_str.split('/').collect();
39 let mut normalized_parts = Vec::new();
40
41 for part in parts {
42 if part.is_empty() {
43 normalized_parts.push(part.to_string());
44 continue;
45 }
46
47 if UUID_REGEX.is_match(part)
48 || OBJECTID_REGEX.is_match(part)
49 || NUMERIC_ID_REGEX.is_match(part)
50 || BASE64_ID_REGEX.is_match(part)
51 {
52 normalized_parts.push("{id}".to_string());
53 } else {
54 normalized_parts.push(part.to_string());
55 }
56 }
57
58 normalized_parts.join("/")
59}
60
61pub fn endpoint_key(method: &str, path: &str) -> String {
63 format!("{} {}", method.to_uppercase(), normalize_path(path))
64}
65
66#[cfg(test)]
67mod tests {
68 use super::*;
69
70 #[test]
71 fn test_normalize_numeric_ids() {
72 assert_eq!(normalize_path("/api/users/123"), "/api/users/{id}");
73 assert_eq!(
74 normalize_path("/api/users/123/posts/456"),
75 "/api/users/{id}/posts/{id}"
76 );
77 }
78
79 #[test]
80 fn test_normalize_uuids() {
81 assert_eq!(
82 normalize_path("/api/orders/550e8400-e29b-41d4-a716-446655440000"),
83 "/api/orders/{id}"
84 );
85 }
86
87 #[test]
88 fn test_normalize_objectids() {
89 assert_eq!(
90 normalize_path("/api/docs/507f1f77bcf86cd799439011"),
91 "/api/docs/{id}"
92 );
93 }
94
95 #[test]
96 fn test_preserve_static_paths() {
97 assert_eq!(normalize_path("/api/health"), "/api/health");
98 assert_eq!(normalize_path("/api/v1/config"), "/api/v1/config");
99 }
100
101 #[test]
102 fn test_root_and_empty() {
103 assert_eq!(normalize_path("/"), "/");
104 assert_eq!(normalize_path(""), "");
105 }
106
107 #[test]
108 fn test_trailing_slash() {
109 assert_eq!(normalize_path("/api/users/123/"), "/api/users/{id}");
110 assert_eq!(normalize_path("/api/health/"), "/api/health");
111 }
112
113 #[test]
114 fn test_endpoint_key() {
115 assert_eq!(endpoint_key("GET", "/api/users/123"), "GET /api/users/{id}");
116 }
117}