use std::path::Path;
use crate::glob::glob_match;
use crate::glob_path::GlobPath;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FilterResult {
Include,
Exclude,
NoMatch,
}
#[derive(Debug, Clone, Default)]
pub struct IncludeExclude {
rules: Vec<(FilterAction, CompiledRule)>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum FilterAction {
Include,
Exclude,
}
#[derive(Debug, Clone)]
struct CompiledRule {
glob: Option<GlobPath>,
raw: String,
}
impl CompiledRule {
fn new(pattern: &str) -> Self {
let glob = GlobPath::new(pattern).ok();
Self {
glob,
raw: pattern.to_string(),
}
}
fn matches(&self, path: &Path) -> bool {
if let Some(ref glob) = self.glob {
return glob.matches(path);
}
let path_str = path.to_string_lossy();
if let Some(name) = path.file_name() {
let name_str = name.to_string_lossy();
if glob_match(&self.raw, &name_str) {
return true;
}
}
glob_match(&self.raw, &path_str)
}
}
impl IncludeExclude {
pub fn new() -> Self {
Self::default()
}
pub fn include(&mut self, pattern: &str) {
self.rules
.push((FilterAction::Include, CompiledRule::new(pattern)));
}
pub fn exclude(&mut self, pattern: &str) {
self.rules
.push((FilterAction::Exclude, CompiledRule::new(pattern)));
}
pub fn check(&self, path: &Path) -> FilterResult {
for (action, rule) in &self.rules {
if rule.matches(path) {
return match action {
FilterAction::Include => FilterResult::Include,
FilterAction::Exclude => FilterResult::Exclude,
};
}
}
FilterResult::NoMatch
}
pub fn should_exclude(&self, path: &Path) -> bool {
matches!(self.check(path), FilterResult::Exclude)
}
pub fn is_empty(&self) -> bool {
self.rules.is_empty()
}
pub fn len(&self) -> usize {
self.rules.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_empty_filter() {
let filter = IncludeExclude::new();
assert_eq!(filter.check(Path::new("any.txt")), FilterResult::NoMatch);
assert!(!filter.should_exclude(Path::new("any.txt")));
}
#[test]
fn test_include() {
let mut filter = IncludeExclude::new();
filter.include("*.rs");
assert_eq!(filter.check(Path::new("main.rs")), FilterResult::Include);
assert_eq!(filter.check(Path::new("main.txt")), FilterResult::NoMatch);
}
#[test]
fn test_exclude() {
let mut filter = IncludeExclude::new();
filter.exclude("*.log");
assert_eq!(filter.check(Path::new("app.log")), FilterResult::Exclude);
assert!(filter.should_exclude(Path::new("app.log")));
assert!(!filter.should_exclude(Path::new("app.txt")));
}
#[test]
fn test_order_matters() {
let mut filter = IncludeExclude::new();
filter.include("*.rs");
filter.exclude("*_test.rs");
assert_eq!(
filter.check(Path::new("parser_test.rs")),
FilterResult::Include
);
let mut filter = IncludeExclude::new();
filter.exclude("*_test.rs");
filter.include("*.rs");
assert_eq!(
filter.check(Path::new("parser_test.rs")),
FilterResult::Exclude
);
}
#[test]
fn test_globstar_patterns() {
let mut filter = IncludeExclude::new();
filter.include("**/*.rs");
filter.exclude("**/test/**");
assert_eq!(filter.check(Path::new("src/main.rs")), FilterResult::Include);
assert_eq!(
filter.check(Path::new("src/lib/utils.rs")),
FilterResult::Include
);
}
#[test]
fn test_path_patterns() {
let mut filter = IncludeExclude::new();
filter.exclude("logs/*");
assert!(filter.should_exclude(Path::new("logs/app.log")));
assert!(!filter.should_exclude(Path::new("other/app.log")));
}
#[test]
fn test_multiple_patterns() {
let mut filter = IncludeExclude::new();
filter.include("*.rs");
filter.include("*.go");
filter.include("*.py");
filter.exclude("*_test.*");
assert_eq!(filter.check(Path::new("main.rs")), FilterResult::Include);
assert_eq!(filter.check(Path::new("server.go")), FilterResult::Include);
assert_eq!(filter.check(Path::new("main_test.rs")), FilterResult::Include); }
#[test]
fn test_brace_expansion() {
let mut filter = IncludeExclude::new();
filter.include("*.{rs,go,py}");
assert_eq!(filter.check(Path::new("main.rs")), FilterResult::Include);
assert_eq!(filter.check(Path::new("main.go")), FilterResult::Include);
assert_eq!(filter.check(Path::new("main.py")), FilterResult::Include);
assert_eq!(filter.check(Path::new("main.js")), FilterResult::NoMatch);
}
}