Skip to main content

standard_version/
pubspec.rs

1//! `pubspec.yaml` version file engine.
2//!
3//! Implements [`VersionFile`] for Flutter/Dart's `pubspec.yaml` manifest,
4//! detecting and rewriting the top-level `version:` field. When the existing
5//! version carries a `+N` build number suffix, the suffix is incremented
6//! automatically.
7
8use crate::version_file::{VersionFile, VersionFileError};
9
10/// Version file engine for `pubspec.yaml`.
11#[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                    // Existing build number — increment it.
50                    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        // 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
78// ---------------------------------------------------------------------------
79// Tests
80// ---------------------------------------------------------------------------
81
82#[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    // --- detect ---
99
100    #[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    // --- read_version ---
123
124    #[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    // --- write_version ---
147
148    #[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    // --- integration ---
183
184    #[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}