Skip to main content

standard_version/
cargo.rs

1//! Cargo.toml version file engine.
2//!
3//! Implements [`VersionFile`] for Rust's `Cargo.toml` manifest, detecting and
4//! rewriting the `version` field inside the `[package]` section (regular crate)
5//! or the `[workspace.package]` section (workspace manifest) while preserving
6//! formatting.
7
8use crate::toml_helpers;
9use crate::version_file::{VersionFile, VersionFileError};
10
11/// TOML section header for a regular crate manifest.
12const PACKAGE_SECTION: &str = "[package]";
13
14/// TOML section header for a Cargo workspace manifest.
15const WORKSPACE_PACKAGE_SECTION: &str = "[workspace.package]";
16
17/// Version file engine for `Cargo.toml`.
18///
19/// Handles both regular crates (`[package]`) and workspace manifests
20/// (`[workspace.package]`), trying `[package]` first.
21#[derive(Debug, Clone, Copy)]
22pub struct CargoVersionFile;
23
24impl CargoVersionFile {
25    /// Return the section that contains the version field, if any.
26    fn active_section(content: &str) -> Option<&'static str> {
27        if toml_helpers::detect_version_in_section(content, PACKAGE_SECTION) {
28            Some(PACKAGE_SECTION)
29        } else if toml_helpers::detect_version_in_section(content, WORKSPACE_PACKAGE_SECTION) {
30            Some(WORKSPACE_PACKAGE_SECTION)
31        } else {
32            None
33        }
34    }
35}
36
37impl VersionFile for CargoVersionFile {
38    fn name(&self) -> &str {
39        "Cargo.toml"
40    }
41
42    fn filenames(&self) -> &[&str] {
43        &["Cargo.toml"]
44    }
45
46    fn detect(&self, content: &str) -> bool {
47        Self::active_section(content).is_some()
48    }
49
50    fn read_version(&self, content: &str) -> Option<String> {
51        let section = Self::active_section(content)?;
52        toml_helpers::read_version_in_section(content, section)
53    }
54
55    fn write_version(&self, content: &str, new_version: &str) -> Result<String, VersionFileError> {
56        let section = Self::active_section(content).ok_or(VersionFileError::NoVersionField)?;
57        toml_helpers::write_version_in_section(content, section, new_version)
58    }
59}
60
61// ---------------------------------------------------------------------------
62// Tests
63// ---------------------------------------------------------------------------
64
65#[cfg(test)]
66mod tests {
67    use super::*;
68
69    const BASIC_TOML: &str = r#"[package]
70name = "my-crate"
71version = "0.1.0"
72edition = "2021"
73"#;
74
75    const MULTI_SECTION_TOML: &str = r#"[package]
76name = "my-crate"
77version = "0.1.0"
78
79[dependencies]
80foo = { version = "1.0" }
81"#;
82
83    // --- detect ---
84
85    #[test]
86    fn detect_with_package_version() {
87        assert!(CargoVersionFile.detect(BASIC_TOML));
88    }
89
90    #[test]
91    fn detect_without_package_section() {
92        let content = "[dependencies]\nfoo = \"1\"\n";
93        assert!(!CargoVersionFile.detect(content));
94    }
95
96    #[test]
97    fn detect_version_only_in_deps() {
98        let content = "[package]\nname = \"x\"\n\n[dependencies]\nfoo = { version = \"1\" }\n";
99        assert!(!CargoVersionFile.detect(content));
100    }
101
102    // --- read_version ---
103
104    #[test]
105    fn read_version_basic() {
106        assert_eq!(
107            CargoVersionFile.read_version(BASIC_TOML),
108            Some("0.1.0".to_string()),
109        );
110    }
111
112    #[test]
113    fn read_version_no_package() {
114        let content = "[dependencies]\nfoo = \"1\"\n";
115        assert_eq!(CargoVersionFile.read_version(content), None);
116    }
117
118    // --- write_version ---
119
120    #[test]
121    fn write_version_basic() {
122        let result = CargoVersionFile.write_version(BASIC_TOML, "1.0.0").unwrap();
123        assert!(result.contains("version = \"1.0.0\""));
124        assert!(result.contains("name = \"my-crate\""));
125        assert!(result.contains("edition = \"2021\""));
126    }
127
128    #[test]
129    fn write_version_only_in_package_section() {
130        let result = CargoVersionFile
131            .write_version(MULTI_SECTION_TOML, "2.0.0")
132            .unwrap();
133        assert!(result.contains("version = \"2.0.0\""));
134        // Dependency version untouched.
135        assert!(result.contains("foo = { version = \"1.0\" }"));
136    }
137
138    #[test]
139    fn write_version_no_field_returns_error() {
140        let content = "[package]\nname = \"x\"\n";
141        let err = CargoVersionFile.write_version(content, "1.0.0");
142        assert!(err.is_err());
143    }
144
145    #[test]
146    fn write_version_preserves_no_trailing_newline() {
147        let content = "[package]\nname = \"x\"\nversion = \"0.1.0\"";
148        let result = CargoVersionFile.write_version(content, "0.2.0").unwrap();
149        assert!(!result.ends_with('\n'));
150        assert!(result.contains("version = \"0.2.0\""));
151    }
152
153    // --- workspace.package ---
154
155    const WORKSPACE_TOML: &str = r#"[workspace]
156members = ["app"]
157resolver = "2"
158
159[workspace.package]
160version = "0.10.0"
161edition = "2021"
162
163[workspace.dependencies]
164anyhow = "1"
165"#;
166
167    #[test]
168    fn detect_workspace_package_version() {
169        assert!(CargoVersionFile.detect(WORKSPACE_TOML));
170    }
171
172    #[test]
173    fn read_version_workspace_package() {
174        assert_eq!(
175            CargoVersionFile.read_version(WORKSPACE_TOML),
176            Some("0.10.0".to_string()),
177        );
178    }
179
180    #[test]
181    fn write_version_workspace_package() {
182        let result = CargoVersionFile
183            .write_version(WORKSPACE_TOML, "0.11.0")
184            .unwrap();
185        assert!(result.contains("version = \"0.11.0\""));
186        // [workspace.dependencies] versions untouched.
187        assert!(result.contains("anyhow = \"1\""));
188        // members and resolver untouched.
189        assert!(result.contains("members = [\"app\"]"));
190    }
191
192    #[test]
193    fn package_section_takes_priority_over_workspace_package() {
194        // A crate Cargo.toml that happens to also mention [workspace.package]
195        // should use [package] version, not [workspace.package].
196        let content = "[package]\nname = \"x\"\nversion = \"1.0.0\"\n\n[workspace.package]\nversion = \"2.0.0\"\n";
197        assert_eq!(
198            CargoVersionFile.read_version(content),
199            Some("1.0.0".to_string()),
200        );
201    }
202}