use std::collections::{HashMap, HashSet};
use std::fmt;
use std::sync::RwLock;
use super::types::{CompiledMatcher, MatcherId};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RegistryError {
DuplicateId(String),
}
impl fmt::Display for RegistryError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
RegistryError::DuplicateId(id) => write!(f, "duplicate matcher id: {}", id),
}
}
}
impl std::error::Error for RegistryError {}
#[derive(Clone)]
struct RegistryEntry {
matcher: CompiledMatcher,
enabled: bool,
insertion_order: usize,
}
struct RegistryInner {
entries: Vec<RegistryEntry>,
id_index: HashMap<String, usize>,
next_insertion_order: usize,
}
pub struct MatcherRegistry {
inner: RwLock<RegistryInner>,
}
impl MatcherRegistry {
pub fn new() -> Self {
Self {
inner: RwLock::new(RegistryInner {
entries: Vec::new(),
id_index: HashMap::new(),
next_insertion_order: 0,
}),
}
}
}
impl Default for MatcherRegistry {
fn default() -> Self {
Self::new()
}
}
impl MatcherRegistry {
pub fn register(&self, matchers: Vec<CompiledMatcher>) -> Result<(), RegistryError> {
let mut seen: HashSet<String> = HashSet::new();
for m in &matchers {
if !seen.insert(m.id.0.clone()) {
return Err(RegistryError::DuplicateId(m.id.0.clone()));
}
}
let mut inner = self.inner.write().unwrap();
for m in &matchers {
if inner.id_index.contains_key(&m.id.0) {
return Err(RegistryError::DuplicateId(m.id.0.clone()));
}
}
for m in matchers {
let key = m.id.0.clone();
let insertion = inner.next_insertion_order;
inner.next_insertion_order += 1;
let idx = inner.entries.len();
inner.id_index.insert(key, idx);
inner.entries.push(RegistryEntry {
matcher: m,
enabled: true,
insertion_order: insertion,
});
}
Ok(())
}
pub fn replace_all(&self, new_matchers: Vec<CompiledMatcher>) -> Result<(), RegistryError> {
let mut seen: HashSet<String> = HashSet::new();
for m in &new_matchers {
if !seen.insert(m.id.0.clone()) {
return Err(RegistryError::DuplicateId(m.id.0.clone()));
}
}
let mut new_entries: Vec<RegistryEntry> = Vec::new();
let mut id_index: HashMap<String, usize> = HashMap::new();
let mut inner = self.inner.write().unwrap();
let mut next = inner.next_insertion_order;
for m in new_matchers {
let insertion = next;
next += 1;
let idx = new_entries.len();
id_index.insert(m.id.0.clone(), idx);
new_entries.push(RegistryEntry {
matcher: m,
enabled: true,
insertion_order: insertion,
});
}
inner.entries = new_entries;
inner.id_index = id_index;
inner.next_insertion_order = next;
Ok(())
}
pub fn get(&self, id: &MatcherId) -> Option<CompiledMatcher> {
let inner = self.inner.read().unwrap();
inner
.id_index
.get(&id.0)
.and_then(|&idx| inner.entries.get(idx).map(|e| e.matcher.clone()))
}
pub fn iter_active(&self) -> Vec<CompiledMatcher> {
let inner = self.inner.read().unwrap();
let mut list: Vec<(u32, usize, CompiledMatcher)> = inner
.entries
.iter()
.filter(|e| e.enabled)
.map(|e| (e.matcher.priority, e.insertion_order, e.matcher.clone()))
.collect();
list.sort_by(|a, b| b.0.cmp(&a.0).then(a.1.cmp(&b.1)));
list.into_iter().map(|(_, _, m)| m).collect()
}
pub fn get_by_source(&self, source: &str) -> Vec<CompiledMatcher> {
let inner = self.inner.read().unwrap();
inner
.entries
.iter()
.filter(|e| e.enabled && e.matcher.source == source)
.map(|e| e.matcher.clone())
.collect()
}
pub fn set_enabled(&self, id: &MatcherId, enabled: bool) -> bool {
let mut inner = self.inner.write().unwrap();
if let Some(idx) = inner.id_index.get(&id.0).copied()
&& let Some(entry) = inner.entries.get_mut(idx)
{
entry.enabled = enabled;
return true;
}
false
}
pub fn disable_by_source(&self, source: &str) -> usize {
let mut inner = self.inner.write().unwrap();
let mut count = 0usize;
for e in inner.entries.iter_mut() {
if e.enabled && e.matcher.source == source {
e.enabled = false;
count += 1;
}
}
count
}
pub fn unregister_by_ids(&self, ids: &[MatcherId]) -> usize {
let remove: std::collections::HashSet<String> = ids.iter().map(|id| id.0.clone()).collect();
let mut inner = self.inner.write().unwrap();
let mut new_entries: Vec<RegistryEntry> = Vec::new();
let mut new_id_index: HashMap<String, usize> = HashMap::new();
let mut removed = 0usize;
for e in inner.entries.drain(..) {
if remove.contains(&e.matcher.id.0) {
removed += 1;
continue;
}
let idx = new_entries.len();
new_id_index.insert(e.matcher.id.0.clone(), idx);
new_entries.push(e);
}
inner.entries = new_entries;
inner.id_index = new_id_index;
removed
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Arc;
use regex::Regex;
use super::super::types::{EmitTemplate, EmitSeverity, EndCondition};
fn make_matcher(id: &str, start_pat: &str, priority: u32) -> CompiledMatcher {
CompiledMatcher {
id: MatcherId(id.to_string()),
source: "test".to_string(),
priority,
schema_version: 1,
start: Arc::new(Regex::new(start_pat).unwrap()),
body: vec![],
max_lines: None,
end: EndCondition::NextStart,
emit: EmitTemplate {
severity: EmitSeverity::Error,
message: "{{ message }}".to_string(),
file: None,
line: None,
column: None,
code: None,
},
}
}
#[test]
fn basic_registration_and_priority() {
let reg = MatcherRegistry::new();
let m1 = make_matcher("m1", r"^a", 1);
let m2 = make_matcher("m2", r"^b", 10);
let m3 = make_matcher("m3", r"^c", 5);
reg.register(vec![m1.clone(), m2.clone(), m3.clone()]).unwrap();
assert!(reg.get(&m1.id).is_some());
assert!(reg.get(&m2.id).is_some());
let order: Vec<String> = reg.iter_active().into_iter().map(|m| m.id.0).collect();
assert_eq!(order, vec!["m2".to_string(), "m3".to_string(), "m1".to_string()]);
}
#[test]
fn duplicate_id_rejected() {
let reg = MatcherRegistry::new();
let m1 = make_matcher("dup", r"^a", 1);
reg.register(vec![m1.clone()]).unwrap();
let mdup = make_matcher("dup", r"^b", 2);
assert_eq!(reg.register(vec![mdup]).unwrap_err(), RegistryError::DuplicateId("dup".to_string()));
}
#[test]
fn enable_disable_behavior() {
let reg = MatcherRegistry::new();
let m1 = make_matcher("m1", r"^a", 1);
let m2 = make_matcher("m2", r"^b", 2);
reg.register(vec![m1.clone(), m2.clone()]).unwrap();
assert!(reg.set_enabled(&m1.id, false));
let order: Vec<String> = reg.iter_active().into_iter().map(|m| m.id.0).collect();
assert_eq!(order, vec!["m2".to_string()]);
}
#[test]
fn replace_all_hot_reload() {
let reg = MatcherRegistry::new();
let a = make_matcher("a", r"^a", 1);
let b = make_matcher("b", r"^b", 1);
reg.register(vec![a.clone(), b.clone()]).unwrap();
let x = make_matcher("x", r"^x", 5);
let y = make_matcher("y", r"^y", 2);
reg.replace_all(vec![x.clone(), y.clone()]).unwrap();
assert!(reg.get(&a.id).is_none());
assert!(reg.get(&b.id).is_none());
assert!(reg.get(&x.id).is_some());
assert!(reg.get(&y.id).is_some());
let order: Vec<String> = reg.iter_active().into_iter().map(|m| m.id.0).collect();
assert_eq!(order, vec!["x".to_string(), "y".to_string()]);
}
}