ricecoder_orchestration/analyzers/
project_detector.rs1use crate::error::{OrchestrationError, Result};
4use crate::models::{Project, ProjectStatus};
5use std::path::PathBuf;
6use tracing::{debug, warn};
7
8pub struct ProjectDetector;
10
11impl ProjectDetector {
12 pub fn detect_project_type(path: &std::path::Path) -> Option<String> {
22 if path.join("Cargo.toml").exists() {
24 return Some("rust".to_string());
25 }
26
27 if path.join("package.json").exists() {
29 return Some("nodejs".to_string());
30 }
31
32 if path.join("pyproject.toml").exists() {
34 return Some("python".to_string());
35 }
36
37 if path.join("go.mod").exists() {
39 return Some("go".to_string());
40 }
41
42 if path.join("pom.xml").exists() {
44 return Some("java".to_string());
45 }
46
47 if path.join("build.gradle").exists() || path.join("build.gradle.kts").exists() {
49 return Some("gradle".to_string());
50 }
51
52 None
53 }
54
55 pub fn extract_metadata(path: &PathBuf) -> Result<Project> {
65 let project_type = Self::detect_project_type(path)
66 .ok_or_else(|| OrchestrationError::ProjectNotFound(
67 format!("No project manifest found in {:?}", path),
68 ))?;
69
70 let name = path
71 .file_name()
72 .and_then(|n| n.to_str())
73 .map(|s| s.to_string())
74 .ok_or_else(|| OrchestrationError::ProjectNotFound(
75 format!("Invalid project path: {:?}", path),
76 ))?;
77
78 let version = Self::extract_version(path, &project_type);
79
80 Ok(Project {
81 path: path.clone(),
82 name,
83 project_type,
84 version,
85 status: ProjectStatus::Unknown,
86 })
87 }
88
89 fn extract_version(path: &std::path::Path, project_type: &str) -> String {
100 match project_type {
101 "rust" => Self::extract_rust_version(path),
102 "nodejs" => Self::extract_nodejs_version(path),
103 "python" => Self::extract_python_version(path),
104 "go" => Self::extract_go_version(path),
105 "java" => Self::extract_java_version(path),
106 "gradle" => Self::extract_gradle_version(path),
107 _ => "0.1.0".to_string(),
108 }
109 }
110
111 fn extract_rust_version(path: &std::path::Path) -> String {
113 let cargo_toml = path.join("Cargo.toml");
114 match std::fs::read_to_string(&cargo_toml) {
115 Ok(content) => {
116 for line in content.lines() {
117 if line.contains("version") && line.contains("=") {
118 if let Some(version_part) = line.split('=').nth(1) {
119 let version = version_part
120 .trim()
121 .trim_matches('"')
122 .trim_matches('\'')
123 .to_string();
124 if !version.is_empty() {
125 debug!("Extracted Rust version: {}", version);
126 return version;
127 }
128 }
129 }
130 }
131 "0.1.0".to_string()
132 }
133 Err(e) => {
134 warn!("Failed to read Cargo.toml: {}", e);
135 "0.1.0".to_string()
136 }
137 }
138 }
139
140 fn extract_nodejs_version(path: &std::path::Path) -> String {
142 let package_json = path.join("package.json");
143 match std::fs::read_to_string(&package_json) {
144 Ok(content) => {
145 if let Ok(json) = serde_json::from_str::<serde_json::Value>(&content) {
147 if let Some(version) = json.get("version").and_then(|v| v.as_str()) {
148 debug!("Extracted Node.js version: {}", version);
149 return version.to_string();
150 }
151 }
152
153 for line in content.lines() {
155 if line.contains("\"version\"") && line.contains(":") {
156 if let Some(version_part) = line.split(':').nth(1) {
157 let version = version_part
158 .trim()
159 .trim_matches(',')
160 .trim_matches('"')
161 .to_string();
162 if !version.is_empty() && !version.contains('{') && !version.contains('}') {
163 debug!("Extracted Node.js version: {}", version);
164 return version;
165 }
166 }
167 }
168 }
169 "0.1.0".to_string()
170 }
171 Err(e) => {
172 warn!("Failed to read package.json: {}", e);
173 "0.1.0".to_string()
174 }
175 }
176 }
177
178 fn extract_python_version(path: &std::path::Path) -> String {
180 let pyproject_toml = path.join("pyproject.toml");
181 match std::fs::read_to_string(&pyproject_toml) {
182 Ok(content) => {
183 for line in content.lines() {
184 if line.contains("version") && line.contains("=") {
185 if let Some(version_part) = line.split('=').nth(1) {
186 let version = version_part
187 .trim()
188 .trim_matches('"')
189 .trim_matches('\'')
190 .to_string();
191 if !version.is_empty() {
192 debug!("Extracted Python version: {}", version);
193 return version;
194 }
195 }
196 }
197 }
198 "0.1.0".to_string()
199 }
200 Err(e) => {
201 warn!("Failed to read pyproject.toml: {}", e);
202 "0.1.0".to_string()
203 }
204 }
205 }
206
207 fn extract_go_version(path: &std::path::Path) -> String {
209 let go_mod = path.join("go.mod");
210 match std::fs::read_to_string(&go_mod) {
211 Ok(content) => {
212 for line in content.lines() {
214 if line.starts_with("module ") {
215 debug!("Found Go module: {}", line);
216 return "0.1.0".to_string();
217 }
218 }
219 "0.1.0".to_string()
220 }
221 Err(e) => {
222 warn!("Failed to read go.mod: {}", e);
223 "0.1.0".to_string()
224 }
225 }
226 }
227
228 fn extract_java_version(path: &std::path::Path) -> String {
230 let pom_xml = path.join("pom.xml");
231 match std::fs::read_to_string(&pom_xml) {
232 Ok(content) => {
233 for line in content.lines() {
234 if line.contains("<version>") {
235 if let Some(version_part) = line.split("<version>").nth(1) {
236 if let Some(version) = version_part.split("</version>").next() {
237 let version = version.trim().to_string();
238 if !version.is_empty() {
239 debug!("Extracted Java version: {}", version);
240 return version;
241 }
242 }
243 }
244 }
245 }
246 "0.1.0".to_string()
247 }
248 Err(e) => {
249 warn!("Failed to read pom.xml: {}", e);
250 "0.1.0".to_string()
251 }
252 }
253 }
254
255 fn extract_gradle_version(path: &std::path::Path) -> String {
257 let gradle_file = if path.join("build.gradle.kts").exists() {
258 path.join("build.gradle.kts")
259 } else {
260 path.join("build.gradle")
261 };
262
263 match std::fs::read_to_string(&gradle_file) {
264 Ok(content) => {
265 for line in content.lines() {
266 if line.contains("version") && line.contains("=") {
267 if let Some(version_part) = line.split('=').nth(1) {
268 let version = version_part
269 .trim()
270 .trim_matches('"')
271 .trim_matches('\'')
272 .to_string();
273 if !version.is_empty() {
274 debug!("Extracted Gradle version: {}", version);
275 return version;
276 }
277 }
278 }
279 }
280 "0.1.0".to_string()
281 }
282 Err(e) => {
283 warn!("Failed to read build.gradle: {}", e);
284 "0.1.0".to_string()
285 }
286 }
287 }
288}
289
290#[cfg(test)]
291mod tests {
292 use super::*;
293 use tempfile::TempDir;
294
295 #[test]
296 fn test_detect_rust_project() {
297 let temp_dir = TempDir::new().expect("failed to create temp dir");
298 let project_dir = temp_dir.path().to_path_buf();
299 std::fs::write(project_dir.join("Cargo.toml"), "[package]")
300 .expect("failed to write Cargo.toml");
301
302 let project_type = ProjectDetector::detect_project_type(&project_dir);
303 assert_eq!(project_type, Some("rust".to_string()));
304 }
305
306 #[test]
307 fn test_detect_nodejs_project() {
308 let temp_dir = TempDir::new().expect("failed to create temp dir");
309 let project_dir = temp_dir.path().to_path_buf();
310 std::fs::write(project_dir.join("package.json"), "{}")
311 .expect("failed to write package.json");
312
313 let project_type = ProjectDetector::detect_project_type(&project_dir);
314 assert_eq!(project_type, Some("nodejs".to_string()));
315 }
316
317 #[test]
318 fn test_detect_python_project() {
319 let temp_dir = TempDir::new().expect("failed to create temp dir");
320 let project_dir = temp_dir.path().to_path_buf();
321 std::fs::write(project_dir.join("pyproject.toml"), "[build-system]")
322 .expect("failed to write pyproject.toml");
323
324 let project_type = ProjectDetector::detect_project_type(&project_dir);
325 assert_eq!(project_type, Some("python".to_string()));
326 }
327
328 #[test]
329 fn test_detect_go_project() {
330 let temp_dir = TempDir::new().expect("failed to create temp dir");
331 let project_dir = temp_dir.path().to_path_buf();
332 std::fs::write(project_dir.join("go.mod"), "module example.com/test")
333 .expect("failed to write go.mod");
334
335 let project_type = ProjectDetector::detect_project_type(&project_dir);
336 assert_eq!(project_type, Some("go".to_string()));
337 }
338
339 #[test]
340 fn test_detect_java_project() {
341 let temp_dir = TempDir::new().expect("failed to create temp dir");
342 let project_dir = temp_dir.path().to_path_buf();
343 std::fs::write(project_dir.join("pom.xml"), "<project>")
344 .expect("failed to write pom.xml");
345
346 let project_type = ProjectDetector::detect_project_type(&project_dir);
347 assert_eq!(project_type, Some("java".to_string()));
348 }
349
350 #[test]
351 fn test_detect_gradle_project() {
352 let temp_dir = TempDir::new().expect("failed to create temp dir");
353 let project_dir = temp_dir.path().to_path_buf();
354 std::fs::write(project_dir.join("build.gradle"), "plugins {}")
355 .expect("failed to write build.gradle");
356
357 let project_type = ProjectDetector::detect_project_type(&project_dir);
358 assert_eq!(project_type, Some("gradle".to_string()));
359 }
360
361 #[test]
362 fn test_detect_unknown_project() {
363 let temp_dir = TempDir::new().expect("failed to create temp dir");
364 let project_dir = temp_dir.path().to_path_buf();
365
366 let project_type = ProjectDetector::detect_project_type(&project_dir);
367 assert_eq!(project_type, None);
368 }
369
370 #[test]
371 fn test_extract_metadata_rust() {
372 let temp_dir = TempDir::new().expect("failed to create temp dir");
373 let project_dir = temp_dir.path().to_path_buf();
374 std::fs::write(
375 project_dir.join("Cargo.toml"),
376 "[package]\nname = \"test\"\nversion = \"0.2.0\"\n",
377 )
378 .expect("failed to write Cargo.toml");
379
380 let project = ProjectDetector::extract_metadata(&project_dir)
381 .expect("failed to extract metadata");
382
383 assert_eq!(project.project_type, "rust");
384 assert_eq!(project.version, "0.2.0");
385 }
386
387 #[test]
388 fn test_extract_metadata_nodejs() {
389 let temp_dir = TempDir::new().expect("failed to create temp dir");
390 let project_dir = temp_dir.path().to_path_buf();
391 std::fs::write(
392 project_dir.join("package.json"),
393 r#"{"name": "test", "version": "1.0.0"}"#,
394 )
395 .expect("failed to write package.json");
396
397 let project = ProjectDetector::extract_metadata(&project_dir)
398 .expect("failed to extract metadata");
399
400 assert_eq!(project.project_type, "nodejs");
401 assert_eq!(project.version, "1.0.0");
402 }
403
404 #[test]
405 fn test_extract_metadata_python() {
406 let temp_dir = TempDir::new().expect("failed to create temp dir");
407 let project_dir = temp_dir.path().to_path_buf();
408 std::fs::write(
409 project_dir.join("pyproject.toml"),
410 "[project]\nname = \"test\"\nversion = \"2.1.0\"\n",
411 )
412 .expect("failed to write pyproject.toml");
413
414 let project = ProjectDetector::extract_metadata(&project_dir)
415 .expect("failed to extract metadata");
416
417 assert_eq!(project.project_type, "python");
418 assert_eq!(project.version, "2.1.0");
419 }
420
421 #[test]
422 fn test_extract_metadata_missing_manifest() {
423 let temp_dir = TempDir::new().expect("failed to create temp dir");
424 let project_dir = temp_dir.path().to_path_buf();
425
426 let result = ProjectDetector::extract_metadata(&project_dir);
427 assert!(result.is_err());
428 }
429
430 #[test]
431 fn test_extract_rust_version_with_quotes() {
432 let temp_dir = TempDir::new().expect("failed to create temp dir");
433 let project_dir = temp_dir.path().to_path_buf();
434 std::fs::write(
435 project_dir.join("Cargo.toml"),
436 "[package]\nversion = \"0.3.0\"\n",
437 )
438 .expect("failed to write Cargo.toml");
439
440 let version = ProjectDetector::extract_rust_version(&project_dir);
441 assert_eq!(version, "0.3.0");
442 }
443
444 #[test]
445 fn test_extract_version_missing_field() {
446 let temp_dir = TempDir::new().expect("failed to create temp dir");
447 let project_dir = temp_dir.path().to_path_buf();
448 std::fs::write(project_dir.join("Cargo.toml"), "[package]\nname = \"test\"\n")
449 .expect("failed to write Cargo.toml");
450
451 let version = ProjectDetector::extract_rust_version(&project_dir);
452 assert_eq!(version, "0.1.0");
453 }
454
455 #[test]
456 fn test_extract_java_version() {
457 let temp_dir = TempDir::new().expect("failed to create temp dir");
458 let project_dir = temp_dir.path().to_path_buf();
459 std::fs::write(
460 project_dir.join("pom.xml"),
461 "<project><version>1.2.3</version></project>",
462 )
463 .expect("failed to write pom.xml");
464
465 let version = ProjectDetector::extract_java_version(&project_dir);
466 assert_eq!(version, "1.2.3");
467 }
468
469 #[test]
470 fn test_extract_gradle_version() {
471 let temp_dir = TempDir::new().expect("failed to create temp dir");
472 let project_dir = temp_dir.path().to_path_buf();
473 std::fs::write(
474 project_dir.join("build.gradle"),
475 "plugins {}\nversion = \"3.0.0\"\n",
476 )
477 .expect("failed to write build.gradle");
478
479 let version = ProjectDetector::extract_gradle_version(&project_dir);
480 assert_eq!(version, "3.0.0");
481 }
482}