use std::path::{Path, PathBuf};
use walkdir::WalkDir;
use super::error::{ParseError, ParseResult};
#[derive(Debug, Clone)]
pub struct WalkerConfig {
pub follow_symlinks: bool,
pub max_depth: Option<usize>,
pub skip_dirs: Vec<String>,
pub include_patterns: Vec<String>,
}
impl Default for WalkerConfig {
fn default() -> Self {
Self {
follow_symlinks: false,
max_depth: None,
skip_dirs: vec![
"target".to_string(),
".git".to_string(),
"node_modules".to_string(),
".cargo".to_string(),
],
include_patterns: vec!["*.rs".to_string()],
}
}
}
impl WalkerConfig {
pub fn new() -> Self {
Self::default()
}
pub fn follow_symlinks(mut self, follow: bool) -> Self {
self.follow_symlinks = follow;
self
}
pub fn max_depth(mut self, depth: Option<usize>) -> Self {
self.max_depth = depth;
self
}
pub fn skip_dir(mut self, dir: impl Into<String>) -> Self {
self.skip_dirs.push(dir.into());
self
}
pub fn skip_dirs(mut self, dirs: Vec<String>) -> Self {
self.skip_dirs = dirs;
self
}
}
pub struct DirectoryWalker {
config: WalkerConfig,
}
impl DirectoryWalker {
pub fn new(config: WalkerConfig) -> Self {
Self { config }
}
pub fn with_defaults() -> Self {
Self::new(WalkerConfig::default())
}
pub fn discover_rust_files<P: AsRef<Path>>(&self, root: P) -> ParseResult<Vec<PathBuf>> {
let root = root.as_ref();
if !root.exists() {
return Err(ParseError::DirectoryNotFound(root.to_path_buf()));
}
let mut walker = WalkDir::new(root).follow_links(self.config.follow_symlinks);
if let Some(depth) = self.config.max_depth {
walker = walker.max_depth(depth);
}
let mut rust_files = Vec::new();
let skip_dirs = &self.config.skip_dirs;
let walker_iter = walker.into_iter().filter_entry(move |entry| {
if entry.depth() == 0 {
return true;
}
if let Some(name) = entry.file_name().to_str() {
if name.starts_with('.') || name.starts_with('_') {
return false;
}
if entry.file_type().is_dir() && skip_dirs.contains(&name.to_string()) {
return false;
}
}
true
});
for entry in walker_iter {
let entry = entry.map_err(|e| ParseError::WalkError {
path: root.to_path_buf(),
source: e,
})?;
let path = entry.path();
if entry.file_type().is_file() {
if let Some(extension) = path.extension() {
if extension == "rs" {
rust_files.push(path.to_path_buf());
}
}
}
}
rust_files.sort();
Ok(rust_files)
}
pub fn config(&self) -> &WalkerConfig {
&self.config
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::tempdir;
#[test]
fn test_walker_config_default() {
let config = WalkerConfig::default();
assert!(!config.follow_symlinks);
assert!(config.max_depth.is_none());
assert!(config.skip_dirs.contains(&"target".to_string()));
}
#[test]
fn test_walker_config_builder() {
let config = WalkerConfig::new()
.follow_symlinks(true)
.max_depth(Some(5))
.skip_dir("custom_dir");
assert!(config.follow_symlinks);
assert_eq!(config.max_depth, Some(5));
assert!(config.skip_dirs.contains(&"custom_dir".to_string()));
}
#[test]
fn test_discover_rust_files() {
let dir = tempdir().unwrap();
let dir_path = dir.path();
fs::write(dir_path.join("main.rs"), "fn main() {}").unwrap();
fs::write(dir_path.join("lib.rs"), "pub fn lib() {}").unwrap();
let subdir = dir_path.join("src");
fs::create_dir(&subdir).unwrap();
fs::write(subdir.join("module.rs"), "pub mod module;").unwrap();
fs::write(dir_path.join("readme.md"), "# Readme").unwrap();
let walker = DirectoryWalker::with_defaults();
let files = walker.discover_rust_files(dir_path).unwrap();
assert_eq!(files.len(), 3);
assert!(files.iter().all(|f| f.extension().unwrap() == "rs"));
}
#[test]
fn test_skip_directories() {
let dir = tempdir().unwrap();
let dir_path = dir.path();
let target_dir = dir_path.join("target");
fs::create_dir(&target_dir).unwrap();
fs::write(target_dir.join("generated.rs"), "// generated").unwrap();
fs::write(dir_path.join("main.rs"), "fn main() {}").unwrap();
let walker = DirectoryWalker::with_defaults();
let files = walker.discover_rust_files(dir_path).unwrap();
assert_eq!(files.len(), 1);
assert!(files[0].ends_with("main.rs"));
}
#[test]
fn test_directory_not_found() {
let walker = DirectoryWalker::with_defaults();
let result = walker.discover_rust_files("/nonexistent/path");
assert!(matches!(result, Err(ParseError::DirectoryNotFound(_))));
}
}