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 fn default_publish_command(&self) -> String {
100 if cfg!(windows) {
101 ".\\gradlew.bat publish".to_string()
102 } else {
103 "./gradlew publish".to_string()
104 }
105 }
106
107 fn dependencies(&self) -> &HashSet<String> {
108 &self.dependencies
109 }
110
111 fn add_dependency(&mut self, dependency: &str) {
112 self.dependencies.insert(dependency.to_string());
113 }
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119 use changepacks_core::UpdateType;
120 use std::fs;
121 use tempfile::TempDir;
122 use tokio::fs::read_to_string;
123
124 #[tokio::test]
125 async fn test_gradle_workspace_new() {
126 let workspace = GradleWorkspace::new(
127 Some("test-workspace".to_string()),
128 Some("1.0.0".to_string()),
129 PathBuf::from("/test/build.gradle.kts"),
130 PathBuf::from("test/build.gradle.kts"),
131 );
132
133 assert_eq!(workspace.name(), Some("test-workspace"));
134 assert_eq!(workspace.version(), Some("1.0.0"));
135 assert_eq!(workspace.path(), PathBuf::from("/test/build.gradle.kts"));
136 assert_eq!(
137 workspace.relative_path(),
138 PathBuf::from("test/build.gradle.kts")
139 );
140 assert_eq!(workspace.language(), Language::Java);
141 assert!(!workspace.is_changed());
142 if cfg!(windows) {
143 assert_eq!(
144 workspace.default_publish_command(),
145 ".\\gradlew.bat publish"
146 );
147 } else {
148 assert_eq!(workspace.default_publish_command(), "./gradlew publish");
149 }
150 }
151
152 #[tokio::test]
153 async fn test_gradle_workspace_new_without_name_and_version() {
154 let workspace = GradleWorkspace::new(
155 None,
156 None,
157 PathBuf::from("/test/build.gradle.kts"),
158 PathBuf::from("test/build.gradle.kts"),
159 );
160
161 assert_eq!(workspace.name(), None);
162 assert_eq!(workspace.version(), None);
163 }
164
165 #[tokio::test]
166 async fn test_gradle_workspace_set_changed() {
167 let mut workspace = GradleWorkspace::new(
168 Some("test-workspace".to_string()),
169 Some("1.0.0".to_string()),
170 PathBuf::from("/test/build.gradle.kts"),
171 PathBuf::from("test/build.gradle.kts"),
172 );
173
174 assert!(!workspace.is_changed());
175 workspace.set_changed(true);
176 assert!(workspace.is_changed());
177 workspace.set_changed(false);
178 assert!(!workspace.is_changed());
179 }
180
181 #[tokio::test]
182 async fn test_gradle_workspace_update_version_kts_patch() {
183 let temp_dir = TempDir::new().unwrap();
184 let project_dir = temp_dir.path().join("multiproject");
185 fs::create_dir_all(&project_dir).unwrap();
186
187 let build_gradle = project_dir.join("build.gradle.kts");
188 fs::write(
189 &build_gradle,
190 r#"
191plugins {
192 id("java")
193}
194
195group = "com.example"
196version = "1.0.0"
197"#,
198 )
199 .unwrap();
200
201 let mut workspace = GradleWorkspace::new(
202 Some("multiproject".to_string()),
203 Some("1.0.0".to_string()),
204 build_gradle.clone(),
205 PathBuf::from("multiproject/build.gradle.kts"),
206 );
207
208 workspace.update_version(UpdateType::Patch).await.unwrap();
209
210 let content = read_to_string(&build_gradle).await.unwrap();
211 assert!(content.contains(r#"version = "1.0.1""#));
212
213 temp_dir.close().unwrap();
214 }
215
216 #[tokio::test]
217 async fn test_gradle_workspace_update_version_kts_minor() {
218 let temp_dir = TempDir::new().unwrap();
219 let project_dir = temp_dir.path().join("multiproject");
220 fs::create_dir_all(&project_dir).unwrap();
221
222 let build_gradle = project_dir.join("build.gradle.kts");
223 fs::write(
224 &build_gradle,
225 r#"
226plugins {
227 id("java")
228}
229
230group = "com.example"
231version = "1.0.0"
232"#,
233 )
234 .unwrap();
235
236 let mut workspace = GradleWorkspace::new(
237 Some("multiproject".to_string()),
238 Some("1.0.0".to_string()),
239 build_gradle.clone(),
240 PathBuf::from("multiproject/build.gradle.kts"),
241 );
242
243 workspace.update_version(UpdateType::Minor).await.unwrap();
244
245 let content = read_to_string(&build_gradle).await.unwrap();
246 assert!(content.contains(r#"version = "1.1.0""#));
247
248 temp_dir.close().unwrap();
249 }
250
251 #[tokio::test]
252 async fn test_gradle_workspace_update_version_kts_major() {
253 let temp_dir = TempDir::new().unwrap();
254 let project_dir = temp_dir.path().join("multiproject");
255 fs::create_dir_all(&project_dir).unwrap();
256
257 let build_gradle = project_dir.join("build.gradle.kts");
258 fs::write(
259 &build_gradle,
260 r#"
261plugins {
262 id("java")
263}
264
265group = "com.example"
266version = "1.0.0"
267"#,
268 )
269 .unwrap();
270
271 let mut workspace = GradleWorkspace::new(
272 Some("multiproject".to_string()),
273 Some("1.0.0".to_string()),
274 build_gradle.clone(),
275 PathBuf::from("multiproject/build.gradle.kts"),
276 );
277
278 workspace.update_version(UpdateType::Major).await.unwrap();
279
280 let content = read_to_string(&build_gradle).await.unwrap();
281 assert!(content.contains(r#"version = "2.0.0""#));
282
283 temp_dir.close().unwrap();
284 }
285
286 #[tokio::test]
287 async fn test_gradle_workspace_update_version_groovy() {
288 let temp_dir = TempDir::new().unwrap();
289 let project_dir = temp_dir.path().join("multiproject");
290 fs::create_dir_all(&project_dir).unwrap();
291
292 let build_gradle = project_dir.join("build.gradle");
293 fs::write(
294 &build_gradle,
295 r#"
296plugins {
297 id 'java'
298}
299
300group = 'com.example'
301version = '1.0.0'
302"#,
303 )
304 .unwrap();
305
306 let mut workspace = GradleWorkspace::new(
307 Some("multiproject".to_string()),
308 Some("1.0.0".to_string()),
309 build_gradle.clone(),
310 PathBuf::from("multiproject/build.gradle"),
311 );
312
313 workspace.update_version(UpdateType::Patch).await.unwrap();
314
315 let content = read_to_string(&build_gradle).await.unwrap();
316 assert!(content.contains("version = '1.0.1'"));
317
318 temp_dir.close().unwrap();
319 }
320
321 #[tokio::test]
322 async fn test_gradle_workspace_update_version_without_version() {
323 let temp_dir = TempDir::new().unwrap();
324 let project_dir = temp_dir.path().join("multiproject");
325 fs::create_dir_all(&project_dir).unwrap();
326
327 let build_gradle = project_dir.join("build.gradle.kts");
328 fs::write(
329 &build_gradle,
330 r#"
331plugins {
332 id("java")
333}
334
335group = "com.example"
336version = "0.0.0"
337"#,
338 )
339 .unwrap();
340
341 let mut workspace = GradleWorkspace::new(
342 Some("multiproject".to_string()),
343 None,
344 build_gradle.clone(),
345 PathBuf::from("multiproject/build.gradle.kts"),
346 );
347
348 workspace.update_version(UpdateType::Patch).await.unwrap();
349
350 let content = read_to_string(&build_gradle).await.unwrap();
351 assert!(content.contains(r#"version = "0.0.1""#));
352
353 temp_dir.close().unwrap();
354 }
355
356 #[test]
357 fn test_gradle_workspace_dependencies() {
358 let mut workspace = GradleWorkspace::new(
359 Some("test-workspace".to_string()),
360 Some("1.0.0".to_string()),
361 PathBuf::from("/test/build.gradle.kts"),
362 PathBuf::from("test/build.gradle.kts"),
363 );
364
365 assert!(workspace.dependencies().is_empty());
367
368 workspace.add_dependency("core");
370 workspace.add_dependency("utils");
371
372 let deps = workspace.dependencies();
373 assert_eq!(deps.len(), 2);
374 assert!(deps.contains("core"));
375 assert!(deps.contains("utils"));
376
377 workspace.add_dependency("core");
379 assert_eq!(workspace.dependencies().len(), 2);
380 }
381
382 #[test]
383 fn test_set_name() {
384 let mut workspace = GradleWorkspace::new(
385 None,
386 Some("1.0.0".to_string()),
387 PathBuf::from("/test/build.gradle.kts"),
388 PathBuf::from("build.gradle.kts"),
389 );
390 assert_eq!(workspace.name(), None);
391 workspace.set_name("my-project".to_string());
392 assert_eq!(workspace.name(), Some("my-project"));
393 }
394}