1use crate::opaque::{is_opaque, is_uuid};
2
3pub fn normalize_path(path: &str) -> String {
6 let mut parts: Vec<String> = Vec::new();
7 for (i, seg) in path.split('/').enumerate() {
8 match segment_token(seg, i) {
9 Some(tok) => parts.push(tok.to_string()),
10 None => parts.push(seg.to_string()),
11 }
12 }
13 parts.join("/")
14}
15
16fn segment_token(seg: &str, index: usize) -> Option<&'static str> {
17 if seg.is_empty() {
18 return None;
19 }
20 if seg.bytes().all(|b| b.is_ascii_digit()) {
22 return if index == 1 && seg.len() == 1 {
23 None
24 } else {
25 Some("{id}")
26 };
27 }
28 if is_uuid(seg) {
29 return Some("{id}");
30 }
31 if is_long_hex(seg) {
32 return Some("{id}");
33 }
34 if is_opaque(seg) {
35 return Some("{blob}");
36 }
37 None
38}
39
40fn is_long_hex(s: &str) -> bool {
41 s.len() >= 16 && s.bytes().all(|b| b.is_ascii_hexdigit())
42}
43
44#[cfg(test)]
45mod tests {
46 use super::normalize_path;
47
48 #[test]
49 fn collapses_numeric_ids() {
50 assert_eq!(
51 normalize_path("/users/123/orders/456"),
52 "/users/{id}/orders/{id}"
53 );
54 }
55
56 #[test]
57 fn collapses_uuid() {
58 assert_eq!(
59 normalize_path("/v1/items/550e8400-e29b-41d4-a716-446655440000"),
60 "/v1/items/{id}"
61 );
62 }
63
64 #[test]
65 fn collapses_long_hex() {
66 assert_eq!(normalize_path("/blob/0123456789abcdef0123"), "/blob/{id}");
67 }
68
69 #[test]
70 fn keeps_normal_words() {
71 assert_eq!(normalize_path("/3/tv/popular"), "/3/tv/popular");
72 }
73
74 #[test]
75 fn preserves_leading_and_trailing_slashes() {
76 assert_eq!(normalize_path("/a/123/"), "/a/{id}/");
77 }
78
79 #[test]
80 fn collapses_opaque_blob_to_blob_token() {
81 assert_eq!(
82 normalize_path("/cfg/eyJtYXhUb3JyZW50cyI6OCwiZGVicmlkIjp0cnVlfQ==/manifest.json"),
83 "/cfg/{blob}/manifest.json"
84 );
85 }
86
87 #[test]
88 fn collapses_percent_encoded_blob() {
89 assert_eq!(
90 normalize_path("/%7B%22NexioTorii%22%3A%22eyJ1c2VFbmdsaXNo%22%7D/manifest.json"),
91 "/{blob}/manifest.json"
92 );
93 }
94
95 #[test]
96 fn numeric_id_still_uses_id_token() {
97 assert_eq!(
98 normalize_path("/users/123/orders/456"),
99 "/users/{id}/orders/{id}"
100 );
101 }
102}