backhopper_core/suites/
matcher.rs1use std::collections::{BTreeMap, BTreeSet};
20use std::fs;
21use std::path::{Path, PathBuf};
22
23use crate::model::names::ModuleName;
24
25pub trait SuiteMatcher {
29 fn modules_referenced_in_suite(
30 &mut self,
31 suite_path: &Path,
32 triggering: &[ModuleName],
33 ) -> BTreeSet<ModuleName>;
34}
35
36#[derive(Debug, Default)]
38pub struct SubstringMatcher {
39 cache: BTreeMap<PathBuf, String>,
40}
41
42impl SubstringMatcher {
43 pub fn new() -> Self {
44 Self::default()
45 }
46
47 fn cached_text(&mut self, suite_path: &Path) -> &str {
48 self.cache
49 .entry(suite_path.to_path_buf())
50 .or_insert_with(|| {
51 fs::read_to_string(suite_path).unwrap_or_else(|e| {
52 tracing::warn!(
53 path = %suite_path.display(),
54 error = %e,
55 "suite text read failed; treating as no-match"
56 );
57 String::new()
58 })
59 })
60 .as_str()
61 }
62}
63
64impl SuiteMatcher for SubstringMatcher {
65 fn modules_referenced_in_suite(
66 &mut self,
67 suite_path: &Path,
68 triggering: &[ModuleName],
69 ) -> BTreeSet<ModuleName> {
70 let text = self.cached_text(suite_path);
71 triggering
72 .iter()
73 .filter(|m| text_references_module(text, m))
74 .cloned()
75 .collect()
76 }
77}
78
79pub fn text_references_module(text: &str, module: &ModuleName) -> bool {
83 text_references_named_module(text, module.as_str())
84}
85
86pub(crate) fn text_references_named_module(text: &str, name: &str) -> bool {
87 let bytes = text.as_bytes();
88 let mut start = 0usize;
89 while let Some(rel) = text[start..].find(name) {
90 let pos = start + rel;
91 if bytes.get(pos + name.len()) == Some(&b':') && (pos == 0 || !is_atom_byte(bytes[pos - 1]))
92 {
93 return true;
94 }
95 start = pos + name.len();
96 }
97 false
98}
99
100fn is_atom_byte(b: u8) -> bool {
101 b.is_ascii_alphanumeric() || b == b'_' || b == b'@'
102}