Skip to main content

scud_weave/
matcher.rs

1//! Glob pattern matching for event patterns.
2
3use globset::{Glob, GlobMatcher};
4use serde::{Deserialize, Serialize};
5
6/// A glob pattern for matching file paths and targets.
7///
8/// Wraps globset for efficient glob matching.
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct GlobPattern {
11    pattern: String,
12    #[serde(skip)]
13    matcher: Option<GlobMatcher>,
14}
15
16impl GlobPattern {
17    /// Create a new glob pattern from a string.
18    pub fn new(pattern: &str) -> Self {
19        let matcher = Glob::new(pattern).ok().map(|g| g.compile_matcher());
20        GlobPattern {
21            pattern: pattern.to_string(),
22            matcher,
23        }
24    }
25
26    /// Check if a string matches this pattern.
27    pub fn matches(&self, text: &str) -> bool {
28        // "-" means "everything" (SCG convention)
29        if self.pattern == "-" {
30            return true;
31        }
32        if let Some(ref m) = self.matcher {
33            return m.is_match(text);
34        }
35        // Fallback: if pattern didn't compile, try exact match
36        self.pattern == text
37    }
38
39    /// Ensure the compiled matcher is populated (needed after deserialization).
40    pub fn ensure_compiled(&mut self) {
41        if self.matcher.is_none() {
42            self.matcher = Glob::new(&self.pattern).ok().map(|g| g.compile_matcher());
43        }
44    }
45
46    /// Get the raw pattern string.
47    pub fn as_str(&self) -> &str {
48        &self.pattern
49    }
50}
51
52impl PartialEq for GlobPattern {
53    fn eq(&self, other: &Self) -> bool {
54        self.pattern == other.pattern
55    }
56}
57
58impl std::fmt::Display for GlobPattern {
59    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
60        f.write_str(&self.pattern)
61    }
62}
63
64#[cfg(test)]
65mod tests {
66    use super::*;
67
68    #[test]
69    fn test_exact_match() {
70        let pat = GlobPattern::new("src/main.rs");
71        assert!(pat.matches("src/main.rs"));
72        assert!(!pat.matches("src/lib.rs"));
73    }
74
75    #[test]
76    fn test_wildcard_all() {
77        let pat = GlobPattern::new("**");
78        assert!(pat.matches("anything"));
79        assert!(pat.matches("src/foo/bar.rs"));
80    }
81
82    #[test]
83    fn test_directory_glob() {
84        let pat = GlobPattern::new("src/**");
85        assert!(pat.matches("src/main.rs"));
86        assert!(pat.matches("src/foo/bar.rs"));
87        assert!(!pat.matches("tests/test.rs"));
88    }
89
90    #[test]
91    fn test_dash_means_everything() {
92        let pat = GlobPattern::new("-");
93        assert!(pat.matches("anything"));
94    }
95
96    #[test]
97    fn test_star_glob() {
98        let pat = GlobPattern::new("*.rs");
99        assert!(pat.matches("main.rs"));
100        // globset matches * across path separators by default
101        assert!(pat.matches("src/main.rs"));
102    }
103
104    #[test]
105    fn test_complex_glob() {
106        let pat = GlobPattern::new("src/**/*.rs");
107        assert!(pat.matches("src/weave/mod.rs"));
108        assert!(pat.matches("src/a/b/c.rs"));
109        assert!(!pat.matches("tests/test.rs"));
110    }
111
112    #[test]
113    fn test_serde_roundtrip() {
114        let pat = GlobPattern::new("src/**");
115        let json = serde_json::to_string(&pat).unwrap();
116        let mut parsed: GlobPattern = serde_json::from_str(&json).unwrap();
117        parsed.ensure_compiled();
118        assert!(parsed.matches("src/main.rs"));
119        assert!(!parsed.matches("tests/test.rs"));
120    }
121}