use globset::{Glob, GlobSet, GlobSetBuilder};
use std::path::Path;
use std::sync::Arc;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum GlobError {
#[error("invalid glob pattern '{pattern}': {source}")]
InvalidPattern {
pattern: String,
source: globset::Error,
},
#[error("failed to build glob set: {0}")]
BuildFailed(#[from] globset::Error),
}
#[derive(Debug, Clone)]
pub struct GlobMatcher {
matcher: Arc<GlobSet>,
}
impl GlobMatcher {
pub fn new<S: AsRef<str>>(patterns: &[S]) -> Result<Self, GlobError> {
let mut builder = GlobSetBuilder::new();
for pattern in patterns {
let pat = pattern.as_ref();
let glob = Glob::new(pat).map_err(|source| GlobError::InvalidPattern {
pattern: pat.to_owned(),
source,
})?;
let _ = builder.add(glob);
}
let matcher = builder.build()?;
Ok(Self {
matcher: Arc::new(matcher),
})
}
#[must_use]
pub fn empty() -> Self {
Self {
matcher: Arc::new(GlobSet::empty()),
}
}
#[must_use]
pub fn is_match(&self, path: &Path) -> bool {
self.matcher.is_match(path)
}
}
impl Default for GlobMatcher {
fn default() -> Self {
Self::empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_empty_matcher() {
let matcher = GlobMatcher::empty();
assert!(!matcher.is_match(Path::new("test.txt")));
}
#[test]
fn test_extension_match() {
let matcher = GlobMatcher::new(&["*.rs"]).unwrap();
assert!(matcher.is_match(Path::new("main.rs")));
assert!(matcher.is_match(Path::new("src/lib.rs")));
assert!(!matcher.is_match(Path::new("README.md")));
}
#[test]
fn test_directory_match() {
let matcher = GlobMatcher::new(&["target/**"]).unwrap();
assert!(matcher.is_match(Path::new("target/debug/luff")));
assert!(!matcher.is_match(Path::new("src/main.rs")));
}
#[test]
fn test_multiple_patterns() {
let matcher = GlobMatcher::new(&["*.rs", "*.md"]).unwrap();
assert!(matcher.is_match(Path::new("main.rs")));
assert!(matcher.is_match(Path::new("README.md")));
assert!(!matcher.is_match(Path::new("config.toml")));
}
#[test]
fn test_accepts_owned_strings() {
let patterns = vec!["*.rs".to_string(), "*.md".to_string()];
let matcher = GlobMatcher::new(&patterns).unwrap();
assert!(matcher.is_match(Path::new("main.rs")));
}
#[test]
fn test_accepts_str_slices() {
let matcher = GlobMatcher::new(&["*.rs", "*.md"]).unwrap();
assert!(matcher.is_match(Path::new("main.rs")));
}
#[test]
fn test_invalid_pattern() {
let result = GlobMatcher::new(&["["]);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(matches!(err, GlobError::InvalidPattern { .. }));
let msg = err.to_string();
assert!(msg.contains('['), "error should contain the pattern: {msg}");
}
}