use dashmap::DashMap;
use once_cell::sync::Lazy;
use std::sync::atomic::{AtomicU64, Ordering};
#[cfg(not(test))]
const MAX_CACHE_ENTRIES: usize = 10_000;
#[cfg(test)]
const MAX_CACHE_ENTRIES: usize = 10;
const INITIAL_CAPACITY: usize = 1_000;
static TEMPLATE_CACHE: Lazy<DashMap<String, String>> =
Lazy::new(|| DashMap::with_capacity(INITIAL_CAPACITY));
static CACHE_HITS: AtomicU64 = AtomicU64::new(0);
static CACHE_MISSES: AtomicU64 = AtomicU64::new(0);
static EVICTIONS: AtomicU64 = AtomicU64::new(0);
#[inline]
pub fn intern_template(template: &str) -> String {
if let Some(cached) = TEMPLATE_CACHE.get(template) {
CACHE_HITS.fetch_add(1, Ordering::Relaxed);
return cached.value().clone();
}
CACHE_MISSES.fetch_add(1, Ordering::Relaxed);
if TEMPLATE_CACHE.len() >= MAX_CACHE_ENTRIES {
if let Some(entry) = TEMPLATE_CACHE.iter().next() {
let key = entry.key().clone();
drop(entry); TEMPLATE_CACHE.remove(&key);
EVICTIONS.fetch_add(1, Ordering::Relaxed);
}
}
let owned = template.to_string();
TEMPLATE_CACHE.insert(owned.clone(), owned.clone());
owned
}
#[inline]
pub fn normalize_and_intern(path: &str) -> String {
let normalized = normalize_path_to_template(path);
intern_template(&normalized)
}
#[inline]
fn normalize_path_to_template(path: &str) -> String {
path.split('/')
.map(|segment| {
if !segment.is_empty() && segment.chars().all(|c| c.is_ascii_digit()) {
return "{id}";
}
if segment.len() == 36 && segment.chars().filter(|&c| c == '-').count() == 4 {
let hex_parts: Vec<&str> = segment.split('-').collect();
if hex_parts.len() == 5
&& hex_parts
.iter()
.all(|p| p.chars().all(|c| c.is_ascii_hexdigit()))
{
return "{id}";
}
}
if segment.len() == 24 && segment.chars().all(|c| c.is_ascii_hexdigit()) {
return "{id}";
}
segment
})
.collect::<Vec<&str>>()
.join("/")
}
pub fn cache_stats() -> (u64, u64, u64, usize) {
(
CACHE_HITS.load(Ordering::Relaxed),
CACHE_MISSES.load(Ordering::Relaxed),
EVICTIONS.load(Ordering::Relaxed),
TEMPLATE_CACHE.len(),
)
}
#[cfg(test)]
pub fn clear_cache() {
TEMPLATE_CACHE.clear();
CACHE_HITS.store(0, Ordering::Relaxed);
CACHE_MISSES.store(0, Ordering::Relaxed);
EVICTIONS.store(0, Ordering::Relaxed);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_intern_template_caches() {
let template = "/api/intern_test_unique_12345/{id}";
let first = intern_template(template);
let second = intern_template(template);
assert_eq!(first, second);
assert_eq!(first, template);
let (_, _, _, size) = cache_stats();
assert!(size >= 1, "Cache should have at least one entry");
}
#[test]
fn test_normalize_numeric_ids() {
assert_eq!(
normalize_path_to_template("/api/users/123"),
"/api/users/{id}"
);
assert_eq!(
normalize_path_to_template("/api/users/123/posts/456"),
"/api/users/{id}/posts/{id}"
);
}
#[test]
fn test_normalize_uuid() {
assert_eq!(
normalize_path_to_template("/api/orders/550e8400-e29b-41d4-a716-446655440000"),
"/api/orders/{id}"
);
}
#[test]
fn test_normalize_mongodb_objectid() {
assert_eq!(
normalize_path_to_template("/api/documents/507f1f77bcf86cd799439011"),
"/api/documents/{id}"
);
}
#[test]
fn test_normalize_preserves_non_ids() {
assert_eq!(
normalize_path_to_template("/api/v1/products"),
"/api/v1/products"
);
assert_eq!(
normalize_path_to_template("/api/users/me/profile"),
"/api/users/me/profile"
);
}
#[test]
fn test_normalize_and_intern() {
let result = normalize_and_intern("/api/users/123");
assert_eq!(result, "/api/users/{id}");
let result2 = normalize_and_intern("/api/users/456");
assert_eq!(result2, "/api/users/{id}");
assert_eq!(result, result2);
}
#[test]
fn test_cache_eviction() {
clear_cache();
for i in 0..MAX_CACHE_ENTRIES {
intern_template(&format!("/api/eviction_test_{}", i));
}
let (_, _, evictions_before, size) = cache_stats();
assert_eq!(size, MAX_CACHE_ENTRIES);
assert_eq!(evictions_before, 0);
intern_template("/api/eviction_trigger");
let (_, _, evictions_after, size_after) = cache_stats();
assert_eq!(size_after, MAX_CACHE_ENTRIES);
assert!(
evictions_after > evictions_before,
"Eviction should have occurred"
);
}
}