oo-ide 0.0.4

∞ is a terminal IDE focused on low distraction, high usability.
Documentation
//! Matcher registry for compiled log matchers.
//!
//! Provides deterministic, priority-aware storage and lookup for
//! `CompiledMatcher` instances.  The registry is thread-safe (internal
//! `RwLock`) and supports atomic hot-reload via `replace_all`.

use std::collections::{HashMap, HashSet};
use std::fmt;
use std::sync::RwLock;

use super::types::{CompiledMatcher, MatcherId};

// ---------------------------------------------------------------------------
// Errors
// ---------------------------------------------------------------------------

/// Errors returned by `MatcherRegistry` operations.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RegistryError {
    /// A matcher with the same id already exists.
    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 {}

// ---------------------------------------------------------------------------
// Internal storage types
// ---------------------------------------------------------------------------

#[derive(Clone)]
struct RegistryEntry {
    matcher: CompiledMatcher,
    enabled: bool,
    insertion_order: usize,
}

struct RegistryInner {
    entries: Vec<RegistryEntry>,
    /// matcher id -> index in `entries`
    id_index: HashMap<String, usize>,
    /// Monotonic insertion counter used for deterministic tie-breaking.
    next_insertion_order: usize,
}

// ---------------------------------------------------------------------------
// Public registry
// ---------------------------------------------------------------------------

/// In-memory registry of compiled log matchers.
///
/// Lightweight and deterministic: callers may iterate active matchers via
/// `iter_active()` which returns matchers ordered by (priority desc,
/// insertion_order asc).
pub struct MatcherRegistry {
    inner: RwLock<RegistryInner>,
}

impl MatcherRegistry {
    /// Create an empty registry.
    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 {


    /// Register a batch of matchers.
    ///
    /// Fails if any matcher ID duplicates an existing ID or another in the
    /// provided batch.  Registration is atomic: either all provided matchers
    /// are inserted or none are.
    pub fn register(&self, matchers: Vec<CompiledMatcher>) -> Result<(), RegistryError> {
        // Quick validation: duplicates within the new batch.
        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();
        // Check against existing IDs.
        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(())
    }

    /// Atomically replace the entire registry with `new_matchers`.
    ///
    /// Validates duplicate IDs within `new_matchers` first.
    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();

        // Use the current insertion counter to continue deterministic ordering.
        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(())
    }

    /// Get a matcher by ID (clone returned).  Cloning `CompiledMatcher` is
    /// cheap because regex fields are behind `Arc`.
    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()))
    }

    /// Iterator-like snapshot of active matchers, ordered by priority
    /// (descending) then insertion order (ascending).
    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();

        // Sort: priority desc, insertion asc.
        list.sort_by(|a, b| b.0.cmp(&a.0).then(a.1.cmp(&b.1)));
        list.into_iter().map(|(_, _, m)| m).collect()
    }

    /// Lookup all active matchers for a given source.
    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()
    }

    /// Enable or disable a matcher by id.  Returns `true` if the matcher was
    /// found and its state changed (or set to the requested value).
    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
    }

    /// Disable all matchers matching `source`.  Returns number disabled.
    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
    }

    /// Unregister matchers by their IDs.  Returns number removed.
    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
    }
}

// ---------------------------------------------------------------------------
// Unit tests
// ---------------------------------------------------------------------------

#[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();

        // Lookup by id
        assert!(reg.get(&m1.id).is_some());
        assert!(reg.get(&m2.id).is_some());

        // Iteration order: m2 (10), m3 (5), m1 (1)
        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();

        // old ids gone
        assert!(reg.get(&a.id).is_none());
        assert!(reg.get(&b.id).is_none());
        // new ids present
        assert!(reg.get(&x.id).is_some());
        assert!(reg.get(&y.id).is_some());

        // priority order: x (5), y (2)
        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()]);
    }
}