standard_version/
pubspec.rs1use crate::version_file::{VersionFile, VersionFileError};
9
10#[derive(Debug, Clone, Copy)]
12pub struct PubspecVersionFile;
13
14impl VersionFile for PubspecVersionFile {
15 fn name(&self) -> &str {
16 "pubspec.yaml"
17 }
18
19 fn filenames(&self) -> &[&str] {
20 &["pubspec.yaml"]
21 }
22
23 fn detect(&self, content: &str) -> bool {
24 content
25 .lines()
26 .any(|line| line.starts_with("version:") && line.len() > "version:".len())
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:") {
32 let value = value.trim();
33 if !value.is_empty() {
34 return Some(value.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 && let Some(old_value) = line.strip_prefix("version:") {
47 let old_value = old_value.trim();
48 let new_value = if let Some(pos) = old_value.find('+') {
49 let build_str = &old_value[pos + 1..];
51 let build_num: u64 = build_str.parse().unwrap_or(0);
52 format!("{new_version}+{}", build_num + 1)
53 } else {
54 new_version.to_string()
55 };
56 result.push_str(&format!("version: {new_value}"));
57 result.push('\n');
58 replaced = true;
59 continue;
60 }
61 result.push_str(line);
62 result.push('\n');
63 }
64
65 if !replaced {
66 return Err(VersionFileError::NoVersionField);
67 }
68
69 if !content.ends_with('\n') && result.ends_with('\n') {
71 result.pop();
72 }
73
74 Ok(result)
75 }
76}
77
78#[cfg(test)]
83mod tests {
84 use super::*;
85
86 const BASIC_PUBSPEC: &str = "\
87name: my_app
88version: 1.2.3
89description: A sample app
90";
91
92 const PUBSPEC_WITH_BUILD: &str = "\
93name: my_app
94version: 1.2.3+42
95description: A sample app
96";
97
98 #[test]
101 fn detect_positive() {
102 assert!(PubspecVersionFile.detect(BASIC_PUBSPEC));
103 }
104
105 #[test]
106 fn detect_positive_with_build_number() {
107 assert!(PubspecVersionFile.detect(PUBSPEC_WITH_BUILD));
108 }
109
110 #[test]
111 fn detect_negative_no_version() {
112 let content = "name: my_app\ndescription: A sample app\n";
113 assert!(!PubspecVersionFile.detect(content));
114 }
115
116 #[test]
117 fn detect_negative_version_in_middle_of_line() {
118 let content = "name: my_app\n# version: 1.0.0\ndescription: foo\n";
119 assert!(!PubspecVersionFile.detect(content));
120 }
121
122 #[test]
125 fn read_version_basic() {
126 assert_eq!(
127 PubspecVersionFile.read_version(BASIC_PUBSPEC),
128 Some("1.2.3".to_string()),
129 );
130 }
131
132 #[test]
133 fn read_version_with_build_number() {
134 assert_eq!(
135 PubspecVersionFile.read_version(PUBSPEC_WITH_BUILD),
136 Some("1.2.3+42".to_string()),
137 );
138 }
139
140 #[test]
141 fn read_version_missing() {
142 let content = "name: my_app\n";
143 assert_eq!(PubspecVersionFile.read_version(content), None);
144 }
145
146 #[test]
149 fn write_version_basic() {
150 let result = PubspecVersionFile
151 .write_version(BASIC_PUBSPEC, "2.0.0")
152 .unwrap();
153 assert!(result.contains("version: 2.0.0"));
154 assert!(!result.contains('+'));
155 assert!(result.contains("name: my_app"));
156 }
157
158 #[test]
159 fn write_version_increments_build_number() {
160 let result = PubspecVersionFile
161 .write_version(PUBSPEC_WITH_BUILD, "2.0.0")
162 .unwrap();
163 assert!(result.contains("version: 2.0.0+43"));
164 assert!(result.contains("name: my_app"));
165 }
166
167 #[test]
168 fn write_version_no_field_returns_error() {
169 let content = "name: my_app\n";
170 let err = PubspecVersionFile.write_version(content, "1.0.0");
171 assert!(err.is_err());
172 }
173
174 #[test]
175 fn write_version_preserves_no_trailing_newline() {
176 let content = "name: my_app\nversion: 0.1.0";
177 let result = PubspecVersionFile.write_version(content, "0.2.0").unwrap();
178 assert!(!result.ends_with('\n'));
179 assert!(result.contains("version: 0.2.0"));
180 }
181
182 #[test]
185 fn integration_roundtrip() {
186 let dir = tempfile::tempdir().unwrap();
187 let path = dir.path().join("pubspec.yaml");
188 std::fs::write(&path, BASIC_PUBSPEC).unwrap();
189
190 let content = std::fs::read_to_string(&path).unwrap();
191 assert!(PubspecVersionFile.detect(&content));
192 assert_eq!(
193 PubspecVersionFile.read_version(&content),
194 Some("1.2.3".to_string()),
195 );
196
197 let updated = PubspecVersionFile.write_version(&content, "3.0.0").unwrap();
198 std::fs::write(&path, &updated).unwrap();
199
200 let final_content = std::fs::read_to_string(&path).unwrap();
201 assert_eq!(
202 PubspecVersionFile.read_version(&final_content),
203 Some("3.0.0".to_string()),
204 );
205 }
206
207 #[test]
208 fn integration_roundtrip_with_build_number() {
209 let dir = tempfile::tempdir().unwrap();
210 let path = dir.path().join("pubspec.yaml");
211 std::fs::write(&path, PUBSPEC_WITH_BUILD).unwrap();
212
213 let content = std::fs::read_to_string(&path).unwrap();
214 let updated = PubspecVersionFile.write_version(&content, "3.0.0").unwrap();
215 std::fs::write(&path, &updated).unwrap();
216
217 let final_content = std::fs::read_to_string(&path).unwrap();
218 assert_eq!(
219 PubspecVersionFile.read_version(&final_content),
220 Some("3.0.0+43".to_string()),
221 );
222 }
223}