Skip to main content

standard_version/
gradle.rs

1//! `gradle.properties` version file engine.
2//!
3//! Implements [`VersionFile`] for Android/Gradle projects that store version
4//! information as `VERSION_NAME=` in `gradle.properties`. When a
5//! `VERSION_CODE=N` line is also present, the integer value is incremented
6//! automatically.
7
8use crate::version_file::{VersionFile, VersionFileError};
9
10/// Version file engine for `gradle.properties`.
11#[derive(Debug, Clone, Copy)]
12pub struct GradleVersionFile;
13
14impl VersionFile for GradleVersionFile {
15    fn name(&self) -> &str {
16        "gradle.properties"
17    }
18
19    fn filenames(&self) -> &[&str] {
20        &["gradle.properties"]
21    }
22
23    fn detect(&self, content: &str) -> bool {
24        content
25            .lines()
26            .any(|line| line.starts_with("VERSION_NAME="))
27    }
28
29    fn read_version(&self, content: &str) -> Option<String> {
30        for line in content.lines() {
31            if let Some(value) = line.strip_prefix("VERSION_NAME=") {
32                let trimmed = value.trim();
33                if !trimmed.is_empty() {
34                    return Some(trimmed.to_string());
35                }
36            }
37        }
38        None
39    }
40
41    fn write_version(&self, content: &str, new_version: &str) -> Result<String, VersionFileError> {
42        let mut result = String::new();
43        let mut replaced = false;
44
45        for line in content.lines() {
46            if !replaced && line.starts_with("VERSION_NAME=") {
47                result.push_str(&format!("VERSION_NAME={new_version}"));
48                result.push('\n');
49                replaced = true;
50                continue;
51            }
52
53            if let Some(old_code) = line.strip_prefix("VERSION_CODE=") {
54                let old_code = old_code.trim();
55                let code_num: u64 = old_code.parse().unwrap_or(0);
56                result.push_str(&format!("VERSION_CODE={}", code_num + 1));
57                result.push('\n');
58                continue;
59            }
60
61            result.push_str(line);
62            result.push('\n');
63        }
64
65        if !replaced {
66            return Err(VersionFileError::NoVersionField);
67        }
68
69        // Preserve original trailing-newline behaviour.
70        if !content.ends_with('\n') && result.ends_with('\n') {
71            result.pop();
72        }
73
74        Ok(result)
75    }
76
77    fn extra_info(&self, old_content: &str, new_content: &str) -> Option<String> {
78        let old_code = extract_version_code(old_content);
79        let new_code = extract_version_code(new_content);
80
81        match (old_code, new_code) {
82            (Some(old), Some(new)) => Some(format!("VERSION_CODE: {old} \u{2192} {new}")),
83            _ => None,
84        }
85    }
86}
87
88/// Extract the `VERSION_CODE` integer value from file content.
89fn extract_version_code(content: &str) -> Option<u64> {
90    for line in content.lines() {
91        if let Some(value) = line.strip_prefix("VERSION_CODE=") {
92            return value.trim().parse().ok();
93        }
94    }
95    None
96}
97
98// ---------------------------------------------------------------------------
99// Tests
100// ---------------------------------------------------------------------------
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105
106    const BASIC_GRADLE: &str = "\
107VERSION_NAME=1.2.3
108VERSION_CODE=42
109org.gradle.jvmargs=-Xmx2048m
110";
111
112    const GRADLE_NO_CODE: &str = "\
113VERSION_NAME=1.2.3
114org.gradle.jvmargs=-Xmx2048m
115";
116
117    // --- detect ---
118
119    #[test]
120    fn detect_positive() {
121        assert!(GradleVersionFile.detect(BASIC_GRADLE));
122    }
123
124    #[test]
125    fn detect_negative() {
126        let content = "org.gradle.jvmargs=-Xmx2048m\n";
127        assert!(!GradleVersionFile.detect(content));
128    }
129
130    #[test]
131    fn detect_negative_version_name_in_comment() {
132        let content = "# VERSION_NAME=1.0.0\norg.gradle.jvmargs=-Xmx2048m\n";
133        assert!(!GradleVersionFile.detect(content));
134    }
135
136    // --- read_version ---
137
138    #[test]
139    fn read_version_basic() {
140        assert_eq!(
141            GradleVersionFile.read_version(BASIC_GRADLE),
142            Some("1.2.3".to_string()),
143        );
144    }
145
146    #[test]
147    fn read_version_missing() {
148        let content = "org.gradle.jvmargs=-Xmx2048m\n";
149        assert_eq!(GradleVersionFile.read_version(content), None);
150    }
151
152    // --- write_version ---
153
154    #[test]
155    fn write_version_updates_name_and_code() {
156        let result = GradleVersionFile
157            .write_version(BASIC_GRADLE, "2.0.0")
158            .unwrap();
159        assert!(result.contains("VERSION_NAME=2.0.0"));
160        assert!(result.contains("VERSION_CODE=43"));
161        assert!(result.contains("org.gradle.jvmargs=-Xmx2048m"));
162    }
163
164    #[test]
165    fn write_version_no_code_stays_without() {
166        let result = GradleVersionFile
167            .write_version(GRADLE_NO_CODE, "2.0.0")
168            .unwrap();
169        assert!(result.contains("VERSION_NAME=2.0.0"));
170        assert!(!result.contains("VERSION_CODE"));
171    }
172
173    #[test]
174    fn write_version_no_field_returns_error() {
175        let content = "org.gradle.jvmargs=-Xmx2048m\n";
176        let err = GradleVersionFile.write_version(content, "1.0.0");
177        assert!(err.is_err());
178    }
179
180    #[test]
181    fn write_version_preserves_no_trailing_newline() {
182        let content = "VERSION_NAME=0.1.0";
183        let result = GradleVersionFile.write_version(content, "0.2.0").unwrap();
184        assert!(!result.ends_with('\n'));
185        assert!(result.contains("VERSION_NAME=0.2.0"));
186    }
187
188    // --- extra_info ---
189
190    #[test]
191    fn extra_info_reports_version_code_change() {
192        let old = BASIC_GRADLE;
193        let new_content = GradleVersionFile.write_version(old, "2.0.0").unwrap();
194        let info = GradleVersionFile.extra_info(old, &new_content);
195        assert_eq!(info, Some("VERSION_CODE: 42 \u{2192} 43".to_string()));
196    }
197
198    #[test]
199    fn extra_info_none_when_no_version_code() {
200        let old = GRADLE_NO_CODE;
201        let new_content = GradleVersionFile.write_version(old, "2.0.0").unwrap();
202        let info = GradleVersionFile.extra_info(old, &new_content);
203        assert_eq!(info, None);
204    }
205
206    // --- integration ---
207
208    #[test]
209    fn integration_roundtrip() {
210        let dir = tempfile::tempdir().unwrap();
211        let path = dir.path().join("gradle.properties");
212        std::fs::write(&path, BASIC_GRADLE).unwrap();
213
214        let content = std::fs::read_to_string(&path).unwrap();
215        assert!(GradleVersionFile.detect(&content));
216        assert_eq!(
217            GradleVersionFile.read_version(&content),
218            Some("1.2.3".to_string()),
219        );
220
221        let updated = GradleVersionFile.write_version(&content, "3.0.0").unwrap();
222        std::fs::write(&path, &updated).unwrap();
223
224        let final_content = std::fs::read_to_string(&path).unwrap();
225        assert_eq!(
226            GradleVersionFile.read_version(&final_content),
227            Some("3.0.0".to_string()),
228        );
229        assert!(final_content.contains("VERSION_CODE=43"));
230    }
231
232    #[test]
233    fn integration_roundtrip_no_code() {
234        let dir = tempfile::tempdir().unwrap();
235        let path = dir.path().join("gradle.properties");
236        std::fs::write(&path, GRADLE_NO_CODE).unwrap();
237
238        let content = std::fs::read_to_string(&path).unwrap();
239        let updated = GradleVersionFile.write_version(&content, "3.0.0").unwrap();
240        std::fs::write(&path, &updated).unwrap();
241
242        let final_content = std::fs::read_to_string(&path).unwrap();
243        assert_eq!(
244            GradleVersionFile.read_version(&final_content),
245            Some("3.0.0".to_string()),
246        );
247        assert!(!final_content.contains("VERSION_CODE"));
248    }
249}