Skip to main content

changepacks_java/
package.rs

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