openapi_from_source/
scanner.rs

1use anyhow::Result;
2use log::warn;
3use std::path::PathBuf;
4use walkdir::WalkDir;
5
6/// File scanner for traversing project directories.
7///
8/// The `FileScanner` recursively walks through a project directory to find all Rust source files.
9/// It automatically skips common directories that should be ignored, such as `target` and hidden
10/// directories (those starting with `.`).
11///
12/// # Example
13///
14/// ```no_run
15/// use openapi_from_source::scanner::FileScanner;
16/// use std::path::PathBuf;
17///
18/// let scanner = FileScanner::new(PathBuf::from("./my-project"));
19/// let result = scanner.scan().unwrap();
20/// println!("Found {} Rust files", result.rust_files.len());
21/// ```
22pub struct FileScanner {
23    root_path: PathBuf,
24}
25
26/// Result of directory scanning operation.
27///
28/// Contains the list of discovered Rust files and any warnings encountered during scanning.
29pub struct ScanResult {
30    /// List of paths to all discovered `.rs` files
31    pub rust_files: Vec<PathBuf>,
32    /// Warning messages for any issues encountered (e.g., inaccessible directories)
33    pub warnings: Vec<String>,
34}
35
36impl FileScanner {
37    /// Creates a new `FileScanner` for the specified root directory.
38    ///
39    /// # Arguments
40    ///
41    /// * `root_path` - The root directory to scan for Rust files
42    pub fn new(root_path: PathBuf) -> Self {
43        Self { root_path }
44    }
45
46    /// Scans the directory tree and collects all `.rs` files.
47    ///
48    /// This method recursively traverses the directory tree starting from the root path,
49    /// collecting all files with the `.rs` extension. It automatically skips:
50    /// - The `target` directory (build artifacts)
51    /// - Hidden directories (starting with `.`)
52    ///
53    /// If any directories or files cannot be accessed, warnings are logged and added to
54    /// the result, but scanning continues.
55    ///
56    /// # Returns
57    ///
58    /// Returns a `ScanResult` containing the list of discovered files and any warnings.
59    ///
60    /// # Errors
61    ///
62    /// Returns an error if the root directory cannot be accessed.
63    pub fn scan(&self) -> Result<ScanResult> {
64        let mut rust_files = Vec::new();
65        let mut warnings = Vec::new();
66
67        for entry in WalkDir::new(&self.root_path)
68            .into_iter()
69            .filter_entry(|e| {
70                // Don't filter the root directory itself
71                if e.path() == self.root_path {
72                    return true;
73                }
74                
75                // Skip target directory and hidden directories
76                let file_name = e.file_name().to_string_lossy();
77                let is_hidden = file_name.starts_with('.');
78                let is_target = file_name == "target";
79                
80                !is_hidden && !is_target
81            })
82        {
83            match entry {
84                Ok(entry) => {
85                    let path = entry.path();
86                    
87                    // Check if it's a .rs file
88                    if path.is_file() && path.extension().and_then(|s| s.to_str()) == Some("rs") {
89                        rust_files.push(path.to_path_buf());
90                    }
91                }
92                Err(e) => {
93                    // Record warning for inaccessible directories/files
94                    let warning = format!("Failed to access path: {}", e);
95                    warn!("{}", warning);
96                    warnings.push(warning);
97                }
98            }
99        }
100
101        Ok(ScanResult {
102            rust_files,
103            warnings,
104        })
105    }
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111    use std::fs;
112    use tempfile::TempDir;
113
114    #[test]
115    fn test_scan_normal_directory() {
116        // Create temporary test directory structure
117        let temp_dir = TempDir::new().unwrap();
118        let root = temp_dir.path();
119
120        // Create test files
121        fs::write(root.join("main.rs"), "fn main() {}").unwrap();
122        fs::write(root.join("lib.rs"), "pub fn test() {}").unwrap();
123        fs::write(root.join("readme.md"), "# README").unwrap();
124
125        // Scan directory
126        let scanner = FileScanner::new(root.to_path_buf());
127        let result = scanner.scan().unwrap();
128
129        // Verify results
130        assert_eq!(result.rust_files.len(), 2);
131        assert!(result.warnings.is_empty());
132        
133        let file_names: Vec<String> = result
134            .rust_files
135            .iter()
136            .map(|p| p.file_name().unwrap().to_string_lossy().to_string())
137            .collect();
138        
139        assert!(file_names.contains(&"main.rs".to_string()));
140        assert!(file_names.contains(&"lib.rs".to_string()));
141    }
142
143    #[test]
144    fn test_scan_empty_directory() {
145        // Create empty temporary directory
146        let temp_dir = TempDir::new().unwrap();
147        let root = temp_dir.path();
148
149        // Scan directory
150        let scanner = FileScanner::new(root.to_path_buf());
151        let result = scanner.scan().unwrap();
152
153        // Verify results
154        assert_eq!(result.rust_files.len(), 0);
155        assert!(result.warnings.is_empty());
156    }
157
158    #[test]
159    fn test_scan_nested_directories() {
160        // Create temporary test directory structure with nested directories
161        let temp_dir = TempDir::new().unwrap();
162        let root = temp_dir.path();
163
164        // Create nested structure
165        fs::create_dir(root.join("src")).unwrap();
166        fs::create_dir(root.join("src/models")).unwrap();
167        fs::create_dir(root.join("tests")).unwrap();
168
169        // Create test files
170        fs::write(root.join("main.rs"), "fn main() {}").unwrap();
171        fs::write(root.join("src/lib.rs"), "pub fn test() {}").unwrap();
172        fs::write(root.join("src/models/user.rs"), "struct User {}").unwrap();
173        fs::write(root.join("tests/integration.rs"), "#[test] fn test() {}").unwrap();
174
175        // Scan directory
176        let scanner = FileScanner::new(root.to_path_buf());
177        let result = scanner.scan().unwrap();
178
179        // Verify results
180        assert_eq!(result.rust_files.len(), 4);
181        assert!(result.warnings.is_empty());
182    }
183
184    #[test]
185    fn test_scan_skips_target_directory() {
186        // Create temporary test directory structure
187        let temp_dir = TempDir::new().unwrap();
188        let root = temp_dir.path();
189
190        // Create target directory with files
191        fs::create_dir(root.join("target")).unwrap();
192        fs::write(root.join("target/build.rs"), "fn main() {}").unwrap();
193        
194        // Create normal file
195        fs::write(root.join("main.rs"), "fn main() {}").unwrap();
196
197        // Scan directory
198        let scanner = FileScanner::new(root.to_path_buf());
199        let result = scanner.scan().unwrap();
200
201        // Verify results - should only find main.rs, not target/build.rs
202        assert_eq!(result.rust_files.len(), 1);
203        assert!(result.warnings.is_empty());
204        assert_eq!(
205            result.rust_files[0].file_name().unwrap().to_string_lossy(),
206            "main.rs"
207        );
208    }
209
210    #[test]
211    fn test_scan_skips_hidden_directories() {
212        // Create temporary test directory structure
213        let temp_dir = TempDir::new().unwrap();
214        let root = temp_dir.path();
215
216        // Create hidden directory with files
217        fs::create_dir(root.join(".git")).unwrap();
218        fs::write(root.join(".git/config.rs"), "// config").unwrap();
219        
220        // Create normal file
221        fs::write(root.join("main.rs"), "fn main() {}").unwrap();
222
223        // Scan directory
224        let scanner = FileScanner::new(root.to_path_buf());
225        let result = scanner.scan().unwrap();
226
227        // Verify results - should only find main.rs, not .git/config.rs
228        assert_eq!(result.rust_files.len(), 1);
229        assert!(result.warnings.is_empty());
230        assert_eq!(
231            result.rust_files[0].file_name().unwrap().to_string_lossy(),
232            "main.rs"
233        );
234    }
235
236    #[test]
237    fn test_scan_filters_non_rust_files() {
238        // Create temporary test directory structure
239        let temp_dir = TempDir::new().unwrap();
240        let root = temp_dir.path();
241
242        // Create various file types
243        fs::write(root.join("main.rs"), "fn main() {}").unwrap();
244        fs::write(root.join("readme.md"), "# README").unwrap();
245        fs::write(root.join("config.toml"), "[package]").unwrap();
246        fs::write(root.join("script.sh"), "#!/bin/bash").unwrap();
247
248        // Scan directory
249        let scanner = FileScanner::new(root.to_path_buf());
250        let result = scanner.scan().unwrap();
251
252        // Verify results - should only find .rs files
253        assert_eq!(result.rust_files.len(), 1);
254        assert!(result.warnings.is_empty());
255        assert_eq!(
256            result.rust_files[0].file_name().unwrap().to_string_lossy(),
257            "main.rs"
258        );
259    }
260}