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 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_package_new() {
126 let package = GradlePackage::new(
127 Some("test-package".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!(package.name(), Some("test-package"));
134 assert_eq!(package.version(), Some("1.0.0"));
135 assert_eq!(package.path(), PathBuf::from("/test/build.gradle.kts"));
136 assert_eq!(
137 package.relative_path(),
138 PathBuf::from("test/build.gradle.kts")
139 );
140 assert_eq!(package.language(), Language::Java);
141 assert!(!package.is_changed());
142 if cfg!(windows) {
143 assert_eq!(package.default_publish_command(), ".\\gradlew.bat publish");
144 } else {
145 assert_eq!(package.default_publish_command(), "./gradlew publish");
146 }
147 }
148
149 #[tokio::test]
150 async fn test_gradle_package_set_changed() {
151 let mut package = GradlePackage::new(
152 Some("test-package".to_string()),
153 Some("1.0.0".to_string()),
154 PathBuf::from("/test/build.gradle.kts"),
155 PathBuf::from("test/build.gradle.kts"),
156 );
157
158 assert!(!package.is_changed());
159 package.set_changed(true);
160 assert!(package.is_changed());
161 package.set_changed(false);
162 assert!(!package.is_changed());
163 }
164
165 #[tokio::test]
166 async fn test_gradle_package_update_version_kts_patch() {
167 let temp_dir = TempDir::new().unwrap();
168 let project_dir = temp_dir.path().join("myproject");
169 fs::create_dir_all(&project_dir).unwrap();
170
171 let build_gradle = project_dir.join("build.gradle.kts");
172 fs::write(
173 &build_gradle,
174 r#"
175plugins {
176 id("java")
177}
178
179group = "com.example"
180version = "1.0.0"
181"#,
182 )
183 .unwrap();
184
185 let mut package = GradlePackage::new(
186 Some("myproject".to_string()),
187 Some("1.0.0".to_string()),
188 build_gradle.clone(),
189 PathBuf::from("myproject/build.gradle.kts"),
190 );
191
192 package.update_version(UpdateType::Patch).await.unwrap();
193
194 let content = read_to_string(&build_gradle).await.unwrap();
195 assert!(content.contains(r#"version = "1.0.1""#));
196
197 temp_dir.close().unwrap();
198 }
199
200 #[tokio::test]
201 async fn test_gradle_package_update_version_kts_minor() {
202 let temp_dir = TempDir::new().unwrap();
203 let project_dir = temp_dir.path().join("myproject");
204 fs::create_dir_all(&project_dir).unwrap();
205
206 let build_gradle = project_dir.join("build.gradle.kts");
207 fs::write(
208 &build_gradle,
209 r#"
210plugins {
211 id("java")
212}
213
214group = "com.example"
215version = "1.0.0"
216"#,
217 )
218 .unwrap();
219
220 let mut package = GradlePackage::new(
221 Some("myproject".to_string()),
222 Some("1.0.0".to_string()),
223 build_gradle.clone(),
224 PathBuf::from("myproject/build.gradle.kts"),
225 );
226
227 package.update_version(UpdateType::Minor).await.unwrap();
228
229 let content = read_to_string(&build_gradle).await.unwrap();
230 assert!(content.contains(r#"version = "1.1.0""#));
231
232 temp_dir.close().unwrap();
233 }
234
235 #[tokio::test]
236 async fn test_gradle_package_update_version_kts_major() {
237 let temp_dir = TempDir::new().unwrap();
238 let project_dir = temp_dir.path().join("myproject");
239 fs::create_dir_all(&project_dir).unwrap();
240
241 let build_gradle = project_dir.join("build.gradle.kts");
242 fs::write(
243 &build_gradle,
244 r#"
245plugins {
246 id("java")
247}
248
249group = "com.example"
250version = "1.0.0"
251"#,
252 )
253 .unwrap();
254
255 let mut package = GradlePackage::new(
256 Some("myproject".to_string()),
257 Some("1.0.0".to_string()),
258 build_gradle.clone(),
259 PathBuf::from("myproject/build.gradle.kts"),
260 );
261
262 package.update_version(UpdateType::Major).await.unwrap();
263
264 let content = read_to_string(&build_gradle).await.unwrap();
265 assert!(content.contains(r#"version = "2.0.0""#));
266
267 temp_dir.close().unwrap();
268 }
269
270 #[tokio::test]
271 async fn test_gradle_package_update_version_groovy() {
272 let temp_dir = TempDir::new().unwrap();
273 let project_dir = temp_dir.path().join("myproject");
274 fs::create_dir_all(&project_dir).unwrap();
275
276 let build_gradle = project_dir.join("build.gradle");
277 fs::write(
278 &build_gradle,
279 r#"
280plugins {
281 id 'java'
282}
283
284group = 'com.example'
285version = '1.0.0'
286"#,
287 )
288 .unwrap();
289
290 let mut package = GradlePackage::new(
291 Some("myproject".to_string()),
292 Some("1.0.0".to_string()),
293 build_gradle.clone(),
294 PathBuf::from("myproject/build.gradle"),
295 );
296
297 package.update_version(UpdateType::Patch).await.unwrap();
298
299 let content = read_to_string(&build_gradle).await.unwrap();
300 assert!(content.contains("version = '1.0.1'"));
301
302 temp_dir.close().unwrap();
303 }
304
305 #[tokio::test]
306 async fn test_gradle_package_update_version_with_fallback() {
307 let temp_dir = TempDir::new().unwrap();
308 let project_dir = temp_dir.path().join("myproject");
309 fs::create_dir_all(&project_dir).unwrap();
310
311 let build_gradle = project_dir.join("build.gradle.kts");
312 fs::write(
313 &build_gradle,
314 r#"
315group = "com.devfive"
316version = project.findProperty("releaseVersion") ?: "1.0.11"
317"#,
318 )
319 .unwrap();
320
321 let mut package = GradlePackage::new(
322 Some("myproject".to_string()),
323 Some("1.0.11".to_string()),
324 build_gradle.clone(),
325 PathBuf::from("myproject/build.gradle.kts"),
326 );
327
328 package.update_version(UpdateType::Patch).await.unwrap();
329
330 let content = read_to_string(&build_gradle).await.unwrap();
331 assert!(content.contains(r#"?: "1.0.12""#));
332
333 temp_dir.close().unwrap();
334 }
335
336 #[test]
337 fn test_gradle_package_dependencies() {
338 let mut package = GradlePackage::new(
339 Some("test-package".to_string()),
340 Some("1.0.0".to_string()),
341 PathBuf::from("/test/build.gradle.kts"),
342 PathBuf::from("test/build.gradle.kts"),
343 );
344
345 assert!(package.dependencies().is_empty());
347
348 package.add_dependency("core");
350 package.add_dependency("utils");
351
352 let deps = package.dependencies();
353 assert_eq!(deps.len(), 2);
354 assert!(deps.contains("core"));
355 assert!(deps.contains("utils"));
356
357 package.add_dependency("core");
359 assert_eq!(package.dependencies().len(), 2);
360 }
361
362 #[test]
363 fn test_set_name() {
364 let mut package = GradlePackage::new(
365 None,
366 Some("1.0.0".to_string()),
367 PathBuf::from("/test/build.gradle.kts"),
368 PathBuf::from("build.gradle.kts"),
369 );
370 assert_eq!(package.name(), None);
371 package.set_name("my-project".to_string());
372 assert_eq!(package.name(), Some("my-project"));
373 }
374}