use crate::adapters::RegexPatternMatcher;
use crate::ports::{CompiledPattern, PatternError, PatternMatcher};
use std::sync::OnceLock;
#[must_use]
pub fn default_matcher() -> &'static (dyn PatternMatcher + 'static) {
static MATCHER: OnceLock<RegexPatternMatcher> = OnceLock::new();
MATCHER.get_or_init(RegexPatternMatcher::new)
}
#[must_use]
pub(crate) fn compile_patterns(patterns: &[&str]) -> Vec<CompiledPattern> {
let matcher = default_matcher();
patterns
.iter()
.map(|pattern| {
matcher
.compile(pattern)
.unwrap_or_else(|err| panic!("hardcoded pattern must compile: {pattern}: {err}"))
})
.collect()
}
pub(crate) fn try_compile(pattern: &str) -> Result<CompiledPattern, PatternError> {
default_matcher().compile(pattern)
}
#[macro_export]
macro_rules! lazy_pattern {
($name:ident, $pattern:expr $(,)?) => {
$crate::lazy_pattern!(@build (), $name, $pattern);
};
($vis:vis $name:ident, $pattern:expr $(,)?) => {
$crate::lazy_pattern!(@build ($vis), $name, $pattern);
};
(@build ($($vis:tt)*), $name:ident, $pattern:expr) => {
$($vis)* static $name: std::sync::LazyLock<$crate::ports::CompiledPattern> =
std::sync::LazyLock::new(|| {
// Use `unwrap_or_else(|err| panic!(...))` instead of
$crate::adapters::pattern_helpers::default_matcher()
.compile($pattern)
.unwrap_or_else(|err| panic!(
"hardcoded pattern must compile: {}: {}",
stringify!($name),
err,
))
});
};
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_matcher_returns_stable_singleton() {
let a: *const dyn PatternMatcher = default_matcher();
let b: *const dyn PatternMatcher = default_matcher();
assert!(std::ptr::addr_eq(a, b));
}
lazy_pattern!(LAZY_DIGITS, r"\d+");
#[test]
fn lazy_pattern_macro_drives_all_three_operations() {
assert!(LAZY_DIGITS.is_match("abc 42"));
assert!(!LAZY_DIGITS.is_match("no digits here"));
assert_eq!(LAZY_DIGITS.find_matches("a 1 b 2 c").len(), 2);
assert_eq!(LAZY_DIGITS.captures_iter("a 1 b 2 c").len(), 2);
}
#[test]
fn compile_patterns_compiles_every_input_in_order() {
let inputs = [r"\bfoo\b", r"\d+", r"(?i)bar"];
let compiled = compile_patterns(&inputs);
assert_eq!(compiled.len(), inputs.len());
assert!(compiled[0].is_match("say foo here"));
assert!(!compiled[0].is_match("foobar only"));
assert!(compiled[1].is_match("answer 42"));
assert!(compiled[2].is_match("BAR"));
}
#[test]
#[should_panic(expected = "hardcoded pattern must compile")]
fn compile_patterns_panics_on_invalid_literal() {
let inputs = [r"[unterminated"];
let _ = compile_patterns(&inputs);
}
#[test]
fn try_compile_returns_compiled_pattern_for_valid_input() {
let compiled = try_compile(r"^hello\s+world$").expect("valid pattern must compile");
assert!(compiled.is_match("hello world"));
assert!(!compiled.is_match("hello there"));
}
#[test]
fn try_compile_returns_pattern_error_for_invalid_input() {
let result = try_compile(r"[unterminated");
assert!(
result.is_err(),
"malformed pattern must surface as Result::Err, not panic"
);
}
}