ricecoder_research/dependency_analyzer/
ruby_parser.rs

1//! Ruby dependency parser for Gemfile
2
3use crate::error::ResearchError;
4use crate::models::Dependency;
5use std::path::Path;
6use tracing::debug;
7
8/// Parses Ruby dependencies from Gemfile
9#[derive(Debug)]
10pub struct RubyParser;
11
12impl RubyParser {
13    /// Creates a new RubyParser
14    pub fn new() -> Self {
15        RubyParser
16    }
17
18    /// Parses dependencies from Gemfile
19    pub fn parse(&self, root: &Path) -> Result<Vec<Dependency>, ResearchError> {
20        let gemfile_path = root.join("Gemfile");
21
22        if !gemfile_path.exists() {
23            return Ok(Vec::new());
24        }
25
26        debug!("Parsing Ruby dependencies from {:?}", gemfile_path);
27
28        let content = std::fs::read_to_string(&gemfile_path).map_err(|e| {
29            ResearchError::DependencyParsingFailed {
30                language: "Ruby".to_string(),
31                path: Some(gemfile_path.clone()),
32                reason: format!("Failed to read Gemfile: {}", e),
33            }
34        })?;
35
36        let mut dependencies = Vec::new();
37
38        // Parse gem declarations
39        // Patterns: gem 'name', 'version'
40        //          gem 'name', '~> 1.0'
41        //          gem 'name'
42        let gem_pattern =
43            regex::Regex::new(r#"gem\s+['"]([a-zA-Z0-9_\-\.]+)['"](?:\s*,\s*['"]([^'"]+)['"])?"#)
44                .unwrap();
45
46        for cap in gem_pattern.captures_iter(&content) {
47            let name = cap.get(1).map(|m| m.as_str()).unwrap_or("");
48            let version = cap.get(2).map(|m| m.as_str()).unwrap_or("*");
49
50            dependencies.push(Dependency {
51                name: name.to_string(),
52                version: version.to_string(),
53                constraints: Some(version.to_string()),
54                is_dev: false,
55            });
56        }
57
58        Ok(dependencies)
59    }
60
61    /// Checks if Gemfile exists
62    pub fn has_manifest(&self, root: &Path) -> bool {
63        root.join("Gemfile").exists()
64    }
65}
66
67impl Default for RubyParser {
68    fn default() -> Self {
69        Self::new()
70    }
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76    use std::fs;
77    use tempfile::TempDir;
78
79    #[test]
80    fn test_ruby_parser_creation() {
81        let parser = RubyParser::new();
82        assert!(true);
83    }
84
85    #[test]
86    fn test_ruby_parser_no_manifest() {
87        let parser = RubyParser::new();
88        let temp_dir = TempDir::new().unwrap();
89        let result = parser.parse(temp_dir.path()).unwrap();
90        assert!(result.is_empty());
91    }
92
93    #[test]
94    fn test_ruby_parser_simple_dependencies() {
95        let parser = RubyParser::new();
96        let temp_dir = TempDir::new().unwrap();
97
98        let gemfile = r#"
99source 'https://rubygems.org'
100
101gem 'rails', '~> 7.0'
102gem 'pg', '~> 1.1'
103gem 'puma', '~> 5.0'
104
105group :development, :test do
106  gem 'rspec-rails', '~> 5.0'
107end
108"#;
109
110        fs::write(temp_dir.path().join("Gemfile"), gemfile).unwrap();
111
112        let deps = parser.parse(temp_dir.path()).unwrap();
113        assert!(deps.len() >= 3);
114
115        let rails = deps.iter().find(|d| d.name == "rails").unwrap();
116        assert_eq!(rails.version, "~> 7.0");
117    }
118
119    #[test]
120    fn test_ruby_parser_has_manifest() {
121        let parser = RubyParser::new();
122        let temp_dir = TempDir::new().unwrap();
123
124        assert!(!parser.has_manifest(temp_dir.path()));
125
126        fs::write(temp_dir.path().join("Gemfile"), "").unwrap();
127        assert!(parser.has_manifest(temp_dir.path()));
128    }
129}