use std::collections::{BTreeMap, BTreeSet};
use std::fs;
use std::path::{Path, PathBuf};
use crate::model::names::ModuleName;
pub trait SuiteMatcher {
fn modules_referenced_in_suite(
&mut self,
suite_path: &Path,
triggering: &[ModuleName],
) -> BTreeSet<ModuleName>;
}
#[derive(Debug, Default)]
pub struct SubstringMatcher {
cache: BTreeMap<PathBuf, String>,
}
impl SubstringMatcher {
pub fn new() -> Self {
Self::default()
}
fn cached_text(&mut self, suite_path: &Path) -> &str {
self.cache
.entry(suite_path.to_path_buf())
.or_insert_with(|| {
fs::read_to_string(suite_path).unwrap_or_else(|e| {
tracing::warn!(
path = %suite_path.display(),
error = %e,
"suite text read failed; treating as no-match"
);
String::new()
})
})
.as_str()
}
}
impl SuiteMatcher for SubstringMatcher {
fn modules_referenced_in_suite(
&mut self,
suite_path: &Path,
triggering: &[ModuleName],
) -> BTreeSet<ModuleName> {
let text = self.cached_text(suite_path);
triggering
.iter()
.filter(|m| text_references_module(text, m))
.cloned()
.collect()
}
}
pub fn text_references_module(text: &str, module: &ModuleName) -> bool {
text_references_named_module(text, module.as_str())
}
pub(crate) fn text_references_named_module(text: &str, name: &str) -> bool {
let bytes = text.as_bytes();
let mut start = 0usize;
while let Some(rel) = text[start..].find(name) {
let pos = start + rel;
if bytes.get(pos + name.len()) == Some(&b':') && (pos == 0 || !is_atom_byte(bytes[pos - 1]))
{
return true;
}
start = pos + name.len();
}
false
}
fn is_atom_byte(b: u8) -> bool {
b.is_ascii_alphanumeric() || b == b'_' || b == b'@'
}