use once_cell::sync::Lazy;
use regex::Regex;
use std::collections::HashMap;
use std::sync::Arc;
use std::sync::Mutex;
const MAX_CACHE_SIZE: usize = 1000;
pub struct RegexCache {
cache: Arc<Mutex<HashMap<String, Arc<Regex>>>>,
access_times: Arc<Mutex<HashMap<String, std::time::Instant>>>,
max_size: usize,
}
impl RegexCache {
pub fn new() -> Self {
Self::with_capacity(MAX_CACHE_SIZE)
}
pub fn with_capacity(capacity: usize) -> Self {
Self {
cache: Arc::new(Mutex::new(HashMap::with_capacity(capacity))),
access_times: Arc::new(Mutex::new(HashMap::with_capacity(capacity))),
max_size: capacity,
}
}
pub fn get_or_compile(&self, pattern: &str) -> Result<Arc<Regex>, regex::Error> {
let now = std::time::Instant::now();
if let Ok(mut times) = self.access_times.lock() {
times.insert(pattern.to_string(), now);
}
{
if let Ok(cache) = self.cache.lock() {
if let Some(regex) = cache.get(pattern) {
return Ok(regex.clone());
}
}
}
let compiled = Regex::new(pattern)?;
let regex_arc = Arc::new(compiled);
if let Ok(mut cache) = self.cache.lock() {
cache.insert(pattern.to_string(), regex_arc.clone());
}
self.maybe_evict();
Ok(regex_arc)
}
fn maybe_evict(&self) {
let cache_len = { self.cache.lock().map(|c| c.len()).unwrap_or(0) };
if cache_len <= self.max_size {
return;
}
let mut entries: Vec<(String, std::time::Instant)> = {
match self.access_times.lock() {
Ok(times) => times.iter().map(|(k, v)| (k.clone(), *v)).collect(),
Err(_) => return, }
};
entries.sort_by_key(|&(_, time)| time);
let to_remove = cache_len - self.max_size;
for (pattern, _) in entries.into_iter().take(to_remove) {
if let Ok(mut cache) = self.cache.lock() {
cache.remove(&pattern);
}
if let Ok(mut times) = self.access_times.lock() {
times.remove(&pattern);
}
}
}
pub fn clear(&self) {
if let Ok(mut cache) = self.cache.lock() {
cache.clear();
}
if let Ok(mut times) = self.access_times.lock() {
times.clear();
}
}
pub fn stats(&self) -> RegexCacheStats {
let total_patterns = self.cache.lock().map(|c| c.len()).unwrap_or(0);
RegexCacheStats {
total_patterns,
max_capacity: self.max_size,
}
}
}
impl Default for RegexCache {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Copy)]
pub struct RegexCacheStats {
pub total_patterns: usize,
pub max_capacity: usize,
}
static GLOBAL_REGEX_CACHE: Lazy<RegexCache> = Lazy::new(RegexCache::new);
pub fn get_regex(pattern: &str) -> Result<Arc<Regex>, regex::Error> {
GLOBAL_REGEX_CACHE.get_or_compile(pattern)
}
pub mod common {
use super::*;
pub fn email() -> &'static Regex {
static EMAIL: Lazy<Regex> = Lazy::new(|| {
Regex::new(
r"^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$"
).unwrap()
});
&EMAIL
}
pub fn url() -> &'static Regex {
static URL: Lazy<Regex> =
Lazy::new(|| Regex::new(r"^(https?|ftp)://[^\s/$.?#].[^\s]*$").unwrap());
&URL
}
pub fn ipv4() -> &'static Regex {
static IPV4: Lazy<Regex> = Lazy::new(|| {
Regex::new(
r"^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$"
).unwrap()
});
&IPV4
}
pub fn uuid() -> &'static Regex {
static UUID: Lazy<Regex> = Lazy::new(|| {
Regex::new(
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}$",
)
.unwrap()
});
&UUID
}
pub fn phone() -> &'static Regex {
static PHONE: Lazy<Regex> = Lazy::new(|| Regex::new(r"^\+?[1-9]\d{1,14}$").unwrap());
&PHONE
}
pub fn date_iso() -> &'static Regex {
static DATE: Lazy<Regex> = Lazy::new(|| Regex::new(r"^\d{4}-\d{2}-\d{2}$").unwrap());
&DATE
}
pub fn hex_color() -> &'static Regex {
static HEX_COLOR: Lazy<Regex> =
Lazy::new(|| Regex::new(r"^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$").unwrap());
&HEX_COLOR
}
pub fn username() -> &'static Regex {
static USERNAME: Lazy<Regex> = Lazy::new(|| Regex::new(r"^[a-zA-Z0-9_-]{3,20}$").unwrap());
&USERNAME
}
pub fn password_strong() -> &'static Regex {
static PASSWORD_STRONG: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"^.{8,}$").unwrap()
});
&PASSWORD_STRONG
}
pub fn slug() -> &'static Regex {
static SLUG: Lazy<Regex> = Lazy::new(|| Regex::new(r"^[a-z0-9]+(?:-[a-z0-9]+)*$").unwrap());
&SLUG
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_regex_cache_hit() {
let cache = RegexCache::new();
let regex1 = cache.get_or_compile(r"\d+").unwrap();
let regex2 = cache.get_or_compile(r"\d+").unwrap();
assert!(Arc::ptr_eq(®ex1, ®ex2));
}
#[test]
fn test_regex_cache_different_patterns() {
let cache = RegexCache::new();
let regex1 = cache.get_or_compile(r"\d+").unwrap();
let regex2 = cache.get_or_compile(r"[a-z]+").unwrap();
assert!(!Arc::ptr_eq(®ex1, ®ex2));
}
#[test]
fn test_regex_cache_invalid_pattern() {
let cache = RegexCache::new();
let result = cache.get_or_compile(r"(?P<invalid>["); assert!(result.is_err());
}
#[test]
fn test_regex_cache_stats() {
let cache = RegexCache::new();
cache.get_or_compile(r"\d+").unwrap();
cache.get_or_compile(r"[a-z]+").unwrap();
let stats = cache.stats();
assert_eq!(stats.total_patterns, 2);
}
#[test]
fn test_common_email_regex() {
let regex = common::email();
assert!(regex.is_match("user@example.com"));
assert!(regex.is_match("user.name+tag@domain.co.uk"));
assert!(!regex.is_match("invalid@"));
assert!(!regex.is_match("no-at-sign.com"));
}
#[test]
fn test_common_url_regex() {
let regex = common::url();
assert!(regex.is_match("https://example.com"));
assert!(regex.is_match("http://example.com/path"));
assert!(regex.is_match("ftp://files.example.com"));
assert!(!regex.is_match("not-a-url"));
}
#[test]
fn test_common_ipv4_regex() {
let regex = common::ipv4();
assert!(regex.is_match("192.168.1.1"));
assert!(regex.is_match("0.0.0.0"));
assert!(regex.is_match("255.255.255.255"));
assert!(!regex.is_match("256.0.0.0"));
assert!(!regex.is_match("192.168.1"));
}
#[test]
fn test_common_uuid_regex() {
let regex = common::uuid();
assert!(regex.is_match("550e8400-e29b-41d4-a716-446655440000"));
assert!(!regex.is_match("not-a-uuid"));
assert!(!regex.is_match("550e8400-e29b-41d4-a716"));
}
#[test]
fn test_common_date_iso_regex() {
let regex = common::date_iso();
assert!(regex.is_match("2024-03-26"));
assert!(regex.is_match("1999-12-31"));
assert!(!regex.is_match("2024-03-26T12:00:00"));
assert!(!regex.is_match("26-03-2024"));
}
#[test]
fn test_common_hex_color_regex() {
let regex = common::hex_color();
assert!(regex.is_match("#fff"));
assert!(regex.is_match("#FFFFFF"));
assert!(regex.is_match("#123abc"));
assert!(!regex.is_match("#GGG"));
assert!(!regex.is_match("#1234"));
}
#[test]
fn test_common_username_regex() {
let regex = common::username();
assert!(regex.is_match("user123"));
assert!(regex.is_match("User_Name"));
assert!(regex.is_match("test-user"));
assert!(!regex.is_match("ab")); assert!(!regex.is_match("user@domain"));
}
#[test]
fn test_common_slug_regex() {
let regex = common::slug();
assert!(regex.is_match("my-blog-post"));
assert!(regex.is_match("hello-world"));
assert!(!regex.is_match("My-Blog-Post"));
assert!(!regex.is_match("my_blog_post"));
}
#[test]
fn test_global_cache_function() {
let regex1 = get_regex(r"\d{3}").unwrap();
let regex2 = get_regex(r"\d{3}").unwrap();
assert!(regex1.is_match("123"));
assert!(Arc::ptr_eq(®ex1, ®ex2));
}
#[test]
fn test_global_regex_cache_is_thread_safe() {
use std::thread;
let handles: Vec<_> = (0..10)
.map(|i| {
thread::spawn(move || {
let pattern = format!(r"\d{{{}}}", i + 1);
let regex = get_regex(&pattern).unwrap();
regex.is_match(&"1".repeat(i + 1))
})
})
.collect();
for handle in handles {
assert!(handle.join().unwrap());
}
}
#[test]
fn test_global_regex_cache_caching_behavior() {
let regex1 = get_regex(r"test_pattern_\d+").unwrap();
let regex2 = get_regex(r"test_pattern_\d+").unwrap();
assert!(
Arc::ptr_eq(®ex1, ®ex2),
"Cached patterns should share the same Arc"
);
}
#[test]
fn test_common_regex_functions_are_cached() {
let email1 = common::email();
let email2 = common::email();
assert!(
std::ptr::eq(email1, email2),
"Common regex functions should return the same cached instance"
);
}
#[test]
fn test_regex_cache_eviction() {
let cache = RegexCache::with_capacity(5);
for i in 0..5 {
cache.get_or_compile(&format!(r"pattern{}", i)).unwrap();
}
assert_eq!(cache.stats().total_patterns, 5);
cache.get_or_compile("pattern5").unwrap();
assert!(cache.stats().total_patterns <= 5);
}
#[test]
fn test_regex_cache_clear() {
let cache = RegexCache::new();
cache.get_or_compile(r"\d+").unwrap();
cache.get_or_compile(r"[a-z]+").unwrap();
assert_eq!(cache.stats().total_patterns, 2);
cache.clear();
let stats = cache.stats();
assert_eq!(stats.total_patterns, 0);
assert_eq!(stats.max_capacity, MAX_CACHE_SIZE);
}
#[test]
fn test_regex_cache_default() {
let cache = RegexCache::default();
let regex = cache.get_or_compile(r"\d+").unwrap();
assert!(regex.is_match("123"));
assert_eq!(cache.stats().max_capacity, MAX_CACHE_SIZE);
}
#[test]
fn test_common_phone_regex() {
let regex = common::phone();
assert!(regex.is_match("+1234567890"));
assert!(regex.is_match("12345"));
assert!(!regex.is_match("+0123456789"));
assert!(!regex.is_match("abc"));
}
#[test]
fn test_common_password_strong_regex() {
let regex = common::password_strong();
assert!(regex.is_match("password123"));
assert!(regex.is_match("Abcdefgh!"));
assert!(!regex.is_match("short"));
assert!(!regex.is_match("1234567"));
}
}