Skip to main content

changepacks_java/
workspace.rs

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