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