use globset::GlobSet;
use std::path::PathBuf;
use walkdir::WalkDir;
pub enum SearchStrategy {
Directories(Vec<PathBuf>),
Recursive {
roots: Vec<PathBuf>,
max_depth: Option<usize>,
},
Current,
Custom(Box<dyn Fn() -> Vec<PathBuf> + Send + Sync>),
}
impl std::fmt::Debug for SearchStrategy {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SearchStrategy::Directories(dirs) => {
write!(f, "SearchStrategy::Directories({dirs:?})")
}
SearchStrategy::Recursive { roots, max_depth } => {
write!(
f,
"SearchStrategy::Recursive {{ roots: {roots:?}, max_depth: {max_depth:?} }}"
)
}
SearchStrategy::Current => write!(f, "SearchStrategy::Current"),
SearchStrategy::Custom(_) => write!(f, "SearchStrategy::Custom(<closure>)"),
}
}
}
impl Clone for SearchStrategy {
fn clone(&self) -> Self {
match self {
SearchStrategy::Directories(dirs) => SearchStrategy::Directories(dirs.clone()),
SearchStrategy::Recursive { roots, max_depth } => SearchStrategy::Recursive {
roots: roots.clone(),
max_depth: *max_depth,
},
SearchStrategy::Current => SearchStrategy::Current,
SearchStrategy::Custom(_) => {
panic!(
"Cannot clone SearchStrategy::Custom - use other variants for cloneable strategies"
)
}
}
}
}
impl Default for SearchStrategy {
fn default() -> Self {
Self::Current
}
}
impl SearchStrategy {
pub fn discover_files(&self, glob_set: &GlobSet) -> Vec<PathBuf> {
match self {
SearchStrategy::Directories(dirs) => discover_in_directories(dirs, glob_set),
SearchStrategy::Recursive { roots, max_depth } => {
discover_recursive(roots, glob_set, *max_depth)
}
SearchStrategy::Current => discover_in_directories(&[PathBuf::from(".")], glob_set),
SearchStrategy::Custom(discovery_fn) => {
let all_files = discovery_fn();
filter_files_by_globset(&all_files, glob_set)
}
}
}
}
fn discover_in_directories(directories: &[PathBuf], glob_set: &GlobSet) -> Vec<PathBuf> {
let mut files = Vec::new();
for dir in directories {
if let Ok(entries) = std::fs::read_dir(dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_file() && glob_set.is_match(&path) {
files.push(path);
}
}
}
}
files
}
fn discover_recursive(
roots: &[PathBuf],
glob_set: &GlobSet,
max_depth: Option<usize>,
) -> Vec<PathBuf> {
let mut files = Vec::new();
for root in roots {
let mut walker = WalkDir::new(root);
if let Some(depth) = max_depth {
walker = walker.max_depth(depth);
}
for entry in walker.into_iter().filter_map(|e| e.ok()) {
let path = entry.path();
if path.is_file() && glob_set.is_match(path) {
files.push(path.to_path_buf());
}
}
}
files
}
fn filter_files_by_globset(files: &[PathBuf], glob_set: &GlobSet) -> Vec<PathBuf> {
files
.iter()
.filter(|path| path.is_file() && glob_set.is_match(path))
.cloned()
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use globset::{Glob, GlobSetBuilder};
use std::fs;
use tempfile::TempDir;
fn create_test_globset() -> GlobSet {
let mut builder = GlobSetBuilder::new();
builder.add(Glob::new("*.toml").unwrap());
builder.add(Glob::new("*.yaml").unwrap());
builder.build().unwrap()
}
#[test]
fn test_discover_in_directories() {
let temp_dir = TempDir::new().unwrap();
let dir_path = temp_dir.path();
fs::write(dir_path.join("config.toml"), "test").unwrap();
fs::write(dir_path.join("app.yaml"), "test").unwrap();
fs::write(dir_path.join("readme.txt"), "test").unwrap();
let glob_set = create_test_globset();
let files = discover_in_directories(&[dir_path.to_path_buf()], &glob_set);
assert_eq!(files.len(), 2); assert!(
files
.iter()
.any(|p| p.file_name().unwrap() == "config.toml")
);
assert!(files.iter().any(|p| p.file_name().unwrap() == "app.yaml"));
}
#[test]
fn test_discover_recursive() {
let temp_dir = TempDir::new().unwrap();
let root_path = temp_dir.path();
let sub_dir = root_path.join("subdir");
fs::create_dir(&sub_dir).unwrap();
fs::write(root_path.join("root.toml"), "test").unwrap();
fs::write(sub_dir.join("sub.yaml"), "test").unwrap();
fs::write(root_path.join("ignore.txt"), "test").unwrap();
let glob_set = create_test_globset();
let files = discover_recursive(&[root_path.to_path_buf()], &glob_set, None);
assert_eq!(files.len(), 2); assert!(files.iter().any(|p| p.file_name().unwrap() == "root.toml"));
assert!(files.iter().any(|p| p.file_name().unwrap() == "sub.yaml"));
}
#[test]
fn test_discover_recursive_with_max_depth() {
let temp_dir = TempDir::new().unwrap();
let root_path = temp_dir.path();
let sub_dir = root_path.join("subdir");
let deep_dir = sub_dir.join("deep");
fs::create_dir_all(&deep_dir).unwrap();
fs::write(root_path.join("root.toml"), "test").unwrap(); fs::write(sub_dir.join("sub.toml"), "test").unwrap(); fs::write(deep_dir.join("deep.toml"), "test").unwrap();
let glob_set = create_test_globset();
let files = discover_recursive(&[root_path.to_path_buf()], &glob_set, Some(2));
assert_eq!(files.len(), 2);
assert!(files.iter().any(|p| p.file_name().unwrap() == "root.toml"));
assert!(files.iter().any(|p| p.file_name().unwrap() == "sub.toml"));
assert!(!files.iter().any(|p| p.file_name().unwrap() == "deep.toml"));
}
#[test]
fn test_custom_discovery() {
let custom_files = vec![
PathBuf::from("custom1.toml"),
PathBuf::from("custom2.yaml"),
PathBuf::from("custom3.txt"),
];
let strategy = SearchStrategy::Custom(Box::new(move || custom_files.clone()));
let glob_set = create_test_globset();
let _files = strategy.discover_files(&glob_set);
}
#[test]
fn test_current_directory_strategy() {
let strategy = SearchStrategy::Current;
let glob_set = create_test_globset();
let _files = strategy.discover_files(&glob_set);
}
}