changepacks_java/
package.rs

1use anyhow::Result;
2use async_trait::async_trait;
3use changepacks_core::{Language, Package, UpdateType};
4use changepacks_utils::next_version;
5use regex::Regex;
6use std::collections::HashSet;
7use std::path::{Path, PathBuf};
8use tokio::fs::{read_to_string, write};
9
10#[derive(Debug)]
11pub struct GradlePackage {
12    name: Option<String>,
13    version: Option<String>,
14    path: PathBuf,
15    relative_path: PathBuf,
16    is_changed: bool,
17    dependencies: HashSet<String>,
18}
19
20impl GradlePackage {
21    pub fn new(
22        name: Option<String>,
23        version: Option<String>,
24        path: PathBuf,
25        relative_path: PathBuf,
26    ) -> Self {
27        Self {
28            name,
29            version,
30            path,
31            relative_path,
32            is_changed: false,
33            dependencies: HashSet::new(),
34        }
35    }
36}
37
38/// Update version in build.gradle.kts content
39fn update_version_kts(content: &str, new_version: &str) -> String {
40    // Pattern 1: version = "1.0.0"
41    let simple_pattern = Regex::new(r#"(?m)^(version\s*=\s*)"[^"]+""#).unwrap();
42    if simple_pattern.is_match(content) {
43        return simple_pattern
44            .replace(content, format!(r#"${{1}}"{}""#, new_version))
45            .to_string();
46    }
47
48    // Pattern 2: version = project.findProperty("...") ?: "1.0.0"
49    let fallback_pattern =
50        Regex::new(r#"(?m)^(version\s*=\s*project\.findProperty\([^)]+\)\s*\?:\s*)"[^"]+""#)
51            .unwrap();
52    if fallback_pattern.is_match(content) {
53        return fallback_pattern
54            .replace(content, format!(r#"${{1}}"{}""#, new_version))
55            .to_string();
56    }
57
58    content.to_string()
59}
60
61/// Update version in build.gradle (Groovy) content
62fn update_version_groovy(content: &str, new_version: &str) -> String {
63    // Pattern 1: version = '1.0.0' or version = "1.0.0"
64    let assign_pattern = Regex::new(r#"(?m)^(version\s*=\s*)['"][^'"]+['"]"#).unwrap();
65    if assign_pattern.is_match(content) {
66        return assign_pattern
67            .replace(content, format!(r#"${{1}}'{}'"#, new_version))
68            .to_string();
69    }
70
71    // Pattern 2: version '1.0.0' or version "1.0.0"
72    let space_pattern = Regex::new(r#"(?m)^(version\s+)['"][^'"]+['"]"#).unwrap();
73    if space_pattern.is_match(content) {
74        return space_pattern
75            .replace(content, format!(r#"${{1}}'{}'"#, new_version))
76            .to_string();
77    }
78
79    content.to_string()
80}
81
82#[async_trait]
83impl Package for GradlePackage {
84    fn name(&self) -> Option<&str> {
85        self.name.as_deref()
86    }
87
88    fn version(&self) -> Option<&str> {
89        self.version.as_deref()
90    }
91
92    fn path(&self) -> &Path {
93        &self.path
94    }
95
96    fn relative_path(&self) -> &Path {
97        &self.relative_path
98    }
99
100    async fn update_version(&mut self, update_type: UpdateType) -> Result<()> {
101        let current_version = self.version.as_deref().unwrap_or("0.0.0");
102        let new_version = next_version(current_version, update_type)?;
103
104        let content = read_to_string(&self.path).await?;
105        let file_name = self
106            .path
107            .file_name()
108            .and_then(|f| f.to_str())
109            .unwrap_or_default();
110        let is_kts = file_name.ends_with(".kts");
111
112        let updated_content = if is_kts {
113            update_version_kts(&content, &new_version)
114        } else {
115            update_version_groovy(&content, &new_version)
116        };
117
118        write(&self.path, updated_content).await?;
119        self.version = Some(new_version);
120        Ok(())
121    }
122
123    fn language(&self) -> Language {
124        Language::Java
125    }
126
127    fn set_changed(&mut self, changed: bool) {
128        self.is_changed = changed;
129    }
130
131    fn is_changed(&self) -> bool {
132        self.is_changed
133    }
134
135    fn default_publish_command(&self) -> String {
136        "./gradlew publish".to_string()
137    }
138
139    fn dependencies(&self) -> &HashSet<String> {
140        &self.dependencies
141    }
142
143    fn add_dependency(&mut self, dependency: &str) {
144        self.dependencies.insert(dependency.to_string());
145    }
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151    use changepacks_core::UpdateType;
152    use std::fs;
153    use tempfile::TempDir;
154    use tokio::fs::read_to_string;
155
156    #[tokio::test]
157    async fn test_gradle_package_new() {
158        let package = GradlePackage::new(
159            Some("test-package".to_string()),
160            Some("1.0.0".to_string()),
161            PathBuf::from("/test/build.gradle.kts"),
162            PathBuf::from("test/build.gradle.kts"),
163        );
164
165        assert_eq!(package.name(), Some("test-package"));
166        assert_eq!(package.version(), Some("1.0.0"));
167        assert_eq!(package.path(), PathBuf::from("/test/build.gradle.kts"));
168        assert_eq!(
169            package.relative_path(),
170            PathBuf::from("test/build.gradle.kts")
171        );
172        assert_eq!(package.language(), Language::Java);
173        assert!(!package.is_changed());
174        assert_eq!(package.default_publish_command(), "./gradlew publish");
175    }
176
177    #[tokio::test]
178    async fn test_gradle_package_set_changed() {
179        let mut package = GradlePackage::new(
180            Some("test-package".to_string()),
181            Some("1.0.0".to_string()),
182            PathBuf::from("/test/build.gradle.kts"),
183            PathBuf::from("test/build.gradle.kts"),
184        );
185
186        assert!(!package.is_changed());
187        package.set_changed(true);
188        assert!(package.is_changed());
189        package.set_changed(false);
190        assert!(!package.is_changed());
191    }
192
193    #[test]
194    fn test_update_version_kts_simple() {
195        let content = r#"
196plugins {
197    id("java")
198}
199
200group = "com.example"
201version = "1.0.0"
202"#;
203        let updated = update_version_kts(content, "1.0.1");
204        assert!(updated.contains(r#"version = "1.0.1""#));
205    }
206
207    #[test]
208    fn test_update_version_kts_with_fallback() {
209        let content = r#"
210group = "com.devfive"
211version = project.findProperty("releaseVersion") ?: "1.0.11"
212"#;
213        let updated = update_version_kts(content, "1.0.12");
214        assert!(
215            updated.contains(r#"version = project.findProperty("releaseVersion") ?: "1.0.12""#)
216        );
217    }
218
219    #[test]
220    fn test_update_version_groovy_assign() {
221        let content = r#"
222group = 'com.example'
223version = '2.0.0'
224"#;
225        let updated = update_version_groovy(content, "2.0.1");
226        assert!(updated.contains("version = '2.0.1'"));
227    }
228
229    #[test]
230    fn test_update_version_groovy_space() {
231        let content = r#"
232group 'com.example'
233version '3.0.0'
234"#;
235        let updated = update_version_groovy(content, "3.0.1");
236        assert!(updated.contains("version '3.0.1'"));
237    }
238
239    #[tokio::test]
240    async fn test_gradle_package_update_version_kts_patch() {
241        let temp_dir = TempDir::new().unwrap();
242        let project_dir = temp_dir.path().join("myproject");
243        fs::create_dir_all(&project_dir).unwrap();
244
245        let build_gradle = project_dir.join("build.gradle.kts");
246        fs::write(
247            &build_gradle,
248            r#"
249plugins {
250    id("java")
251}
252
253group = "com.example"
254version = "1.0.0"
255"#,
256        )
257        .unwrap();
258
259        let mut package = GradlePackage::new(
260            Some("myproject".to_string()),
261            Some("1.0.0".to_string()),
262            build_gradle.clone(),
263            PathBuf::from("myproject/build.gradle.kts"),
264        );
265
266        package.update_version(UpdateType::Patch).await.unwrap();
267
268        let content = read_to_string(&build_gradle).await.unwrap();
269        assert!(content.contains(r#"version = "1.0.1""#));
270
271        temp_dir.close().unwrap();
272    }
273
274    #[tokio::test]
275    async fn test_gradle_package_update_version_kts_minor() {
276        let temp_dir = TempDir::new().unwrap();
277        let project_dir = temp_dir.path().join("myproject");
278        fs::create_dir_all(&project_dir).unwrap();
279
280        let build_gradle = project_dir.join("build.gradle.kts");
281        fs::write(
282            &build_gradle,
283            r#"
284plugins {
285    id("java")
286}
287
288group = "com.example"
289version = "1.0.0"
290"#,
291        )
292        .unwrap();
293
294        let mut package = GradlePackage::new(
295            Some("myproject".to_string()),
296            Some("1.0.0".to_string()),
297            build_gradle.clone(),
298            PathBuf::from("myproject/build.gradle.kts"),
299        );
300
301        package.update_version(UpdateType::Minor).await.unwrap();
302
303        let content = read_to_string(&build_gradle).await.unwrap();
304        assert!(content.contains(r#"version = "1.1.0""#));
305
306        temp_dir.close().unwrap();
307    }
308
309    #[tokio::test]
310    async fn test_gradle_package_update_version_kts_major() {
311        let temp_dir = TempDir::new().unwrap();
312        let project_dir = temp_dir.path().join("myproject");
313        fs::create_dir_all(&project_dir).unwrap();
314
315        let build_gradle = project_dir.join("build.gradle.kts");
316        fs::write(
317            &build_gradle,
318            r#"
319plugins {
320    id("java")
321}
322
323group = "com.example"
324version = "1.0.0"
325"#,
326        )
327        .unwrap();
328
329        let mut package = GradlePackage::new(
330            Some("myproject".to_string()),
331            Some("1.0.0".to_string()),
332            build_gradle.clone(),
333            PathBuf::from("myproject/build.gradle.kts"),
334        );
335
336        package.update_version(UpdateType::Major).await.unwrap();
337
338        let content = read_to_string(&build_gradle).await.unwrap();
339        assert!(content.contains(r#"version = "2.0.0""#));
340
341        temp_dir.close().unwrap();
342    }
343
344    #[tokio::test]
345    async fn test_gradle_package_update_version_groovy() {
346        let temp_dir = TempDir::new().unwrap();
347        let project_dir = temp_dir.path().join("myproject");
348        fs::create_dir_all(&project_dir).unwrap();
349
350        let build_gradle = project_dir.join("build.gradle");
351        fs::write(
352            &build_gradle,
353            r#"
354plugins {
355    id 'java'
356}
357
358group = 'com.example'
359version = '1.0.0'
360"#,
361        )
362        .unwrap();
363
364        let mut package = GradlePackage::new(
365            Some("myproject".to_string()),
366            Some("1.0.0".to_string()),
367            build_gradle.clone(),
368            PathBuf::from("myproject/build.gradle"),
369        );
370
371        package.update_version(UpdateType::Patch).await.unwrap();
372
373        let content = read_to_string(&build_gradle).await.unwrap();
374        assert!(content.contains("version = '1.0.1'"));
375
376        temp_dir.close().unwrap();
377    }
378
379    #[tokio::test]
380    async fn test_gradle_package_update_version_with_fallback() {
381        let temp_dir = TempDir::new().unwrap();
382        let project_dir = temp_dir.path().join("myproject");
383        fs::create_dir_all(&project_dir).unwrap();
384
385        let build_gradle = project_dir.join("build.gradle.kts");
386        fs::write(
387            &build_gradle,
388            r#"
389group = "com.devfive"
390version = project.findProperty("releaseVersion") ?: "1.0.11"
391"#,
392        )
393        .unwrap();
394
395        let mut package = GradlePackage::new(
396            Some("myproject".to_string()),
397            Some("1.0.11".to_string()),
398            build_gradle.clone(),
399            PathBuf::from("myproject/build.gradle.kts"),
400        );
401
402        package.update_version(UpdateType::Patch).await.unwrap();
403
404        let content = read_to_string(&build_gradle).await.unwrap();
405        assert!(content.contains(r#"?: "1.0.12""#));
406
407        temp_dir.close().unwrap();
408    }
409
410    #[test]
411    fn test_gradle_package_dependencies() {
412        let mut package = GradlePackage::new(
413            Some("test-package".to_string()),
414            Some("1.0.0".to_string()),
415            PathBuf::from("/test/build.gradle.kts"),
416            PathBuf::from("test/build.gradle.kts"),
417        );
418
419        // Initially empty
420        assert!(package.dependencies().is_empty());
421
422        // Add dependencies
423        package.add_dependency("core");
424        package.add_dependency("utils");
425
426        let deps = package.dependencies();
427        assert_eq!(deps.len(), 2);
428        assert!(deps.contains("core"));
429        assert!(deps.contains("utils"));
430
431        // Adding duplicate should not increase count
432        package.add_dependency("core");
433        assert_eq!(package.dependencies().len(), 2);
434    }
435}