ricecoder_orchestration/analyzers/
workspace_scanner.rs1use crate::error::Result;
4use crate::models::{Project, ProjectStatus};
5use std::path::PathBuf;
6use tracing::{debug, info, warn};
7
8pub struct WorkspaceScanner {
14 workspace_root: PathBuf,
15}
16
17impl WorkspaceScanner {
18 pub fn new(workspace_root: PathBuf) -> Self {
28 debug!("Creating WorkspaceScanner for workspace: {:?}", workspace_root);
29 Self { workspace_root }
30 }
31
32 pub async fn scan_workspace(&self) -> Result<Vec<Project>> {
42 info!("Scanning workspace: {:?}", self.workspace_root);
43
44 let mut projects = Vec::new();
45
46 if !self.workspace_root.exists() {
48 debug!("Workspace root does not exist: {:?}", self.workspace_root);
49 return Ok(projects);
50 }
51
52 let projects_dir = self.workspace_root.join("projects");
54 if projects_dir.exists() {
55 debug!("Scanning projects directory: {:?}", projects_dir);
56 projects.extend(self.scan_directory(&projects_dir).await?);
57 }
58
59 let crates_dir = self.workspace_root.join("crates");
61 if crates_dir.exists() {
62 debug!("Scanning crates directory: {:?}", crates_dir);
63 projects.extend(self.scan_directory(&crates_dir).await?);
64 }
65
66 info!("Discovered {} projects", projects.len());
67 Ok(projects)
68 }
69
70 async fn scan_directory(&self, dir: &PathBuf) -> Result<Vec<Project>> {
80 let mut projects = Vec::new();
81
82 match std::fs::read_dir(dir) {
83 Ok(entries) => {
84 for entry in entries.flatten() {
85 let path = entry.path();
86
87 if path.is_dir() {
88 if let Some(project) = self.detect_project(&path).await {
90 projects.push(project);
91 }
92 }
93 }
94 }
95 Err(e) => {
96 debug!("Error reading directory {:?}: {}", dir, e);
97 }
98 }
99
100 Ok(projects)
101 }
102
103 async fn detect_project(&self, path: &std::path::Path) -> Option<Project> {
113 let cargo_toml = path.join("Cargo.toml");
115 if cargo_toml.exists() {
116 if let Some(name) = path.file_name() {
117 if let Some(name_str) = name.to_str() {
118 debug!("Detected Rust project: {}", name_str);
119 let version = self.extract_rust_version(&cargo_toml).await;
120 return Some(Project {
121 path: path.to_path_buf(),
122 name: name_str.to_string(),
123 project_type: "rust".to_string(),
124 version,
125 status: ProjectStatus::Unknown,
126 });
127 }
128 }
129 }
130
131 let package_json = path.join("package.json");
133 if package_json.exists() {
134 if let Some(name) = path.file_name() {
135 if let Some(name_str) = name.to_str() {
136 debug!("Detected Node.js project: {}", name_str);
137 let version = self.extract_nodejs_version(&package_json).await;
138 return Some(Project {
139 path: path.to_path_buf(),
140 name: name_str.to_string(),
141 project_type: "nodejs".to_string(),
142 version,
143 status: ProjectStatus::Unknown,
144 });
145 }
146 }
147 }
148
149 let pyproject_toml = path.join("pyproject.toml");
151 if pyproject_toml.exists() {
152 if let Some(name) = path.file_name() {
153 if let Some(name_str) = name.to_str() {
154 debug!("Detected Python project: {}", name_str);
155 let version = self.extract_python_version(&pyproject_toml).await;
156 return Some(Project {
157 path: path.to_path_buf(),
158 name: name_str.to_string(),
159 project_type: "python".to_string(),
160 version,
161 status: ProjectStatus::Unknown,
162 });
163 }
164 }
165 }
166
167 None
168 }
169
170 async fn extract_rust_version(&self, cargo_toml: &PathBuf) -> String {
172 match std::fs::read_to_string(cargo_toml) {
173 Ok(content) => {
174 for line in content.lines() {
176 if line.contains("version") && line.contains("=") {
177 if let Some(version_part) = line.split('=').nth(1) {
178 let version = version_part
179 .trim()
180 .trim_matches('"')
181 .trim_matches('\'')
182 .to_string();
183 if !version.is_empty() {
184 debug!("Extracted Rust version: {}", version);
185 return version;
186 }
187 }
188 }
189 }
190 "0.1.0".to_string()
191 }
192 Err(e) => {
193 warn!("Failed to read Cargo.toml: {}", e);
194 "0.1.0".to_string()
195 }
196 }
197 }
198
199 async fn extract_nodejs_version(&self, package_json: &PathBuf) -> String {
201 match std::fs::read_to_string(package_json) {
202 Ok(content) => {
203 if let Ok(json) = serde_json::from_str::<serde_json::Value>(&content) {
206 if let Some(version) = json.get("version").and_then(|v| v.as_str()) {
207 debug!("Extracted Node.js version: {}", version);
208 return version.to_string();
209 }
210 }
211
212 for line in content.lines() {
214 if line.contains("\"version\"") && line.contains(":") {
215 if let Some(version_part) = line.split(':').nth(1) {
216 let version = version_part
217 .trim()
218 .trim_matches(',')
219 .trim_matches('"')
220 .to_string();
221 if !version.is_empty() && !version.contains('{') && !version.contains('}') {
222 debug!("Extracted Node.js version: {}", version);
223 return version;
224 }
225 }
226 }
227 }
228 "0.1.0".to_string()
229 }
230 Err(e) => {
231 warn!("Failed to read package.json: {}", e);
232 "0.1.0".to_string()
233 }
234 }
235 }
236
237 async fn extract_python_version(&self, pyproject_toml: &PathBuf) -> String {
239 match std::fs::read_to_string(pyproject_toml) {
240 Ok(content) => {
241 for line in content.lines() {
243 if line.contains("version") && line.contains("=") {
244 if let Some(version_part) = line.split('=').nth(1) {
245 let version = version_part
246 .trim()
247 .trim_matches('"')
248 .trim_matches('\'')
249 .to_string();
250 if !version.is_empty() {
251 debug!("Extracted Python version: {}", version);
252 return version;
253 }
254 }
255 }
256 }
257 "0.1.0".to_string()
258 }
259 Err(e) => {
260 warn!("Failed to read pyproject.toml: {}", e);
261 "0.1.0".to_string()
262 }
263 }
264 }
265}
266
267#[cfg(test)]
268mod tests {
269 use super::*;
270 use tempfile::TempDir;
271
272 #[tokio::test]
273 async fn test_workspace_scanner_creation() {
274 let root = PathBuf::from("/test/workspace");
275 let scanner = WorkspaceScanner::new(root.clone());
276
277 assert_eq!(scanner.workspace_root, root);
278 }
279
280 #[tokio::test]
281 async fn test_scan_nonexistent_workspace() {
282 let root = PathBuf::from("/nonexistent/workspace");
283 let scanner = WorkspaceScanner::new(root);
284
285 let projects = scanner.scan_workspace().await.expect("scan failed");
286 assert_eq!(projects.len(), 0);
287 }
288
289 #[tokio::test]
290 async fn test_scan_empty_workspace() {
291 let temp_dir = TempDir::new().expect("failed to create temp dir");
292 let root = temp_dir.path().to_path_buf();
293
294 let scanner = WorkspaceScanner::new(root);
295 let projects = scanner.scan_workspace().await.expect("scan failed");
296
297 assert_eq!(projects.len(), 0);
298 }
299
300 #[tokio::test]
301 async fn test_detect_rust_project() {
302 let temp_dir = TempDir::new().expect("failed to create temp dir");
303 let project_dir = temp_dir.path().join("test-project");
304 std::fs::create_dir(&project_dir).expect("failed to create project dir");
305
306 std::fs::write(project_dir.join("Cargo.toml"), "[package]\nname = \"test\"")
308 .expect("failed to write Cargo.toml");
309
310 let scanner = WorkspaceScanner::new(temp_dir.path().to_path_buf());
311 let project = scanner.detect_project(&project_dir).await;
312
313 assert!(project.is_some());
314 let proj = project.unwrap();
315 assert_eq!(proj.name, "test-project");
316 assert_eq!(proj.project_type, "rust");
317 }
318
319 #[tokio::test]
320 async fn test_detect_nodejs_project() {
321 let temp_dir = TempDir::new().expect("failed to create temp dir");
322 let project_dir = temp_dir.path().join("node-project");
323 std::fs::create_dir(&project_dir).expect("failed to create project dir");
324
325 std::fs::write(project_dir.join("package.json"), "{}")
327 .expect("failed to write package.json");
328
329 let scanner = WorkspaceScanner::new(temp_dir.path().to_path_buf());
330 let project = scanner.detect_project(&project_dir).await;
331
332 assert!(project.is_some());
333 let proj = project.unwrap();
334 assert_eq!(proj.name, "node-project");
335 assert_eq!(proj.project_type, "nodejs");
336 }
337
338 #[tokio::test]
339 async fn test_detect_python_project() {
340 let temp_dir = TempDir::new().expect("failed to create temp dir");
341 let project_dir = temp_dir.path().join("python-project");
342 std::fs::create_dir(&project_dir).expect("failed to create project dir");
343
344 std::fs::write(project_dir.join("pyproject.toml"), "[build-system]")
346 .expect("failed to write pyproject.toml");
347
348 let scanner = WorkspaceScanner::new(temp_dir.path().to_path_buf());
349 let project = scanner.detect_project(&project_dir).await;
350
351 assert!(project.is_some());
352 let proj = project.unwrap();
353 assert_eq!(proj.name, "python-project");
354 assert_eq!(proj.project_type, "python");
355 }
356
357 #[tokio::test]
358 async fn test_scan_workspace_with_projects() {
359 let temp_dir = TempDir::new().expect("failed to create temp dir");
360 let projects_dir = temp_dir.path().join("projects");
361 std::fs::create_dir(&projects_dir).expect("failed to create projects dir");
362
363 let rust_project = projects_dir.join("rust-project");
365 std::fs::create_dir(&rust_project).expect("failed to create rust project");
366 std::fs::write(rust_project.join("Cargo.toml"), "[package]\nname = \"test\"")
367 .expect("failed to write Cargo.toml");
368
369 let scanner = WorkspaceScanner::new(temp_dir.path().to_path_buf());
370 let projects = scanner.scan_workspace().await.expect("scan failed");
371
372 assert_eq!(projects.len(), 1);
373 assert_eq!(projects[0].name, "rust-project");
374 assert_eq!(projects[0].project_type, "rust");
375 }
376
377 #[tokio::test]
378 async fn test_extract_rust_version() {
379 let temp_dir = TempDir::new().expect("failed to create temp dir");
380 let cargo_toml = temp_dir.path().join("Cargo.toml");
381 std::fs::write(
382 &cargo_toml,
383 "[package]\nname = \"test\"\nversion = \"0.2.5\"\n",
384 )
385 .expect("failed to write Cargo.toml");
386
387 let scanner = WorkspaceScanner::new(temp_dir.path().to_path_buf());
388 let version = scanner.extract_rust_version(&cargo_toml).await;
389
390 assert_eq!(version, "0.2.5");
391 }
392
393 #[tokio::test]
394 async fn test_extract_nodejs_version() {
395 let temp_dir = TempDir::new().expect("failed to create temp dir");
396 let package_json = temp_dir.path().join("package.json");
397 std::fs::write(
398 &package_json,
399 r#"{"name": "test", "version": "1.0.0"}"#,
400 )
401 .expect("failed to write package.json");
402
403 let scanner = WorkspaceScanner::new(temp_dir.path().to_path_buf());
404 let version = scanner.extract_nodejs_version(&package_json).await;
405
406 assert_eq!(version, "1.0.0");
407 }
408
409 #[tokio::test]
410 async fn test_extract_python_version() {
411 let temp_dir = TempDir::new().expect("failed to create temp dir");
412 let pyproject_toml = temp_dir.path().join("pyproject.toml");
413 std::fs::write(
414 &pyproject_toml,
415 "[project]\nname = \"test\"\nversion = \"2.1.0\"\n",
416 )
417 .expect("failed to write pyproject.toml");
418
419 let scanner = WorkspaceScanner::new(temp_dir.path().to_path_buf());
420 let version = scanner.extract_python_version(&pyproject_toml).await;
421
422 assert_eq!(version, "2.1.0");
423 }
424
425 #[tokio::test]
426 async fn test_extract_rust_version_with_quotes() {
427 let temp_dir = TempDir::new().expect("failed to create temp dir");
428 let cargo_toml = temp_dir.path().join("Cargo.toml");
429 std::fs::write(
430 &cargo_toml,
431 "[package]\nname = \"test\"\nversion = \"0.3.0\"\n",
432 )
433 .expect("failed to write Cargo.toml");
434
435 let scanner = WorkspaceScanner::new(temp_dir.path().to_path_buf());
436 let version = scanner.extract_rust_version(&cargo_toml).await;
437
438 assert_eq!(version, "0.3.0");
439 }
440
441 #[tokio::test]
442 async fn test_extract_version_from_malformed_file() {
443 let temp_dir = TempDir::new().expect("failed to create temp dir");
444 let cargo_toml = temp_dir.path().join("Cargo.toml");
445 std::fs::write(&cargo_toml, "[package]\nname = \"test\"\n")
446 .expect("failed to write Cargo.toml");
447
448 let scanner = WorkspaceScanner::new(temp_dir.path().to_path_buf());
449 let version = scanner.extract_rust_version(&cargo_toml).await;
450
451 assert_eq!(version, "0.1.0");
453 }
454
455 #[tokio::test]
456 async fn test_scan_workspace_with_multiple_projects() {
457 let temp_dir = TempDir::new().expect("failed to create temp dir");
458 let projects_dir = temp_dir.path().join("projects");
459 std::fs::create_dir(&projects_dir).expect("failed to create projects dir");
460
461 for i in 0..3 {
463 let project_dir = projects_dir.join(format!("project-{}", i));
464 std::fs::create_dir(&project_dir).expect("failed to create project dir");
465 std::fs::write(
466 project_dir.join("Cargo.toml"),
467 format!("[package]\nname = \"project-{}\"\nversion = \"0.{}.0\"\n", i, i),
468 )
469 .expect("failed to write Cargo.toml");
470 }
471
472 let scanner = WorkspaceScanner::new(temp_dir.path().to_path_buf());
473 let projects = scanner.scan_workspace().await.expect("scan failed");
474
475 assert_eq!(projects.len(), 3);
476 for (i, project) in projects.iter().enumerate() {
477 assert_eq!(project.name, format!("project-{}", i));
478 assert_eq!(project.project_type, "rust");
479 }
480 }
481
482 #[tokio::test]
483 async fn test_scan_workspace_with_mixed_projects() {
484 let temp_dir = TempDir::new().expect("failed to create temp dir");
485 let projects_dir = temp_dir.path().join("projects");
486 std::fs::create_dir(&projects_dir).expect("failed to create projects dir");
487
488 let rust_project = projects_dir.join("rust-project");
490 std::fs::create_dir(&rust_project).expect("failed to create rust project");
491 std::fs::write(rust_project.join("Cargo.toml"), "[package]\nname = \"test\"")
492 .expect("failed to write Cargo.toml");
493
494 let node_project = projects_dir.join("node-project");
496 std::fs::create_dir(&node_project).expect("failed to create node project");
497 std::fs::write(node_project.join("package.json"), "{}")
498 .expect("failed to write package.json");
499
500 let python_project = projects_dir.join("python-project");
502 std::fs::create_dir(&python_project).expect("failed to create python project");
503 std::fs::write(python_project.join("pyproject.toml"), "[build-system]")
504 .expect("failed to write pyproject.toml");
505
506 let scanner = WorkspaceScanner::new(temp_dir.path().to_path_buf());
507 let projects = scanner.scan_workspace().await.expect("scan failed");
508
509 assert_eq!(projects.len(), 3);
510
511 let project_types: Vec<_> = projects.iter().map(|p| p.project_type.as_str()).collect();
512 assert!(project_types.contains(&"rust"));
513 assert!(project_types.contains(&"nodejs"));
514 assert!(project_types.contains(&"python"));
515 }
516}