use crate::utils::build_regex;
use regex::{Error, Regex};
use std::collections::HashMap;
use std::sync::{OnceLock, RwLock};
type CacheKey = (String, bool);
fn cache() -> &'static RwLock<HashMap<CacheKey, Regex>> {
static CACHE: OnceLock<RwLock<HashMap<CacheKey, Regex>>> = OnceLock::new();
CACHE.get_or_init(|| RwLock::new(HashMap::new()))
}
pub(crate) fn with_cached_regex<R>(
pattern: &str,
case_insensitive: bool,
f: impl FnOnce(&Regex) -> R,
) -> Result<R, Error> {
let key = (pattern.to_string(), case_insensitive);
if let Some(re) = cache().read().unwrap().get(&key) {
return Ok(f(re));
}
let re = build_regex(pattern, case_insensitive)?;
let result = f(&re);
cache().write().unwrap().entry(key).or_insert(re);
Ok(result)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn compiles_once_and_reuses_the_cached_regex() {
let pattern = r"\bfloat(ing)?.*?error";
let cases = [
("floating point error", true),
("no match here", false),
("float error too", true),
];
for (subject, want) in cases {
let matched = with_cached_regex(pattern, true, |re| re.is_match(subject)).unwrap();
assert_eq!(matched, want, "{subject}");
}
}
#[test]
fn case_sensitivity_is_part_of_the_cache_key() {
assert!(with_cached_regex("error", true, |re| re.is_match("ERROR")).unwrap());
assert!(!with_cached_regex("error", false, |re| re.is_match("ERROR")).unwrap());
assert!(with_cached_regex("error", false, |re| re.is_match("error")).unwrap());
}
#[test]
fn invalid_pattern_errors_without_poisoning_the_cache() {
let bad_pattern = r"\bgene(s?\b"; assert!(with_cached_regex(bad_pattern, true, |re| re.is_match("anything")).is_err());
assert!(with_cached_regex(r"\bgenes?\b", true, |re| re.is_match("genes")).unwrap());
}
}