Skip to main content

alef_core/
version.rs

1//! Cross-ecosystem version-format conversions.
2//!
3//! Cargo and the polyglot package registries disagree on prerelease syntax;
4//! these helpers normalize between them so each binding manifest receives a
5//! version string that its tooling will accept.
6
7/// Convert a semver pre-release version to R / CRAN-compatible version format.
8///
9/// R's `package_version()` rejects SemVer dash-form prereleases like
10/// `4.10.0-rc.15`. CRAN convention is to encode development versions as a
11/// fourth component with a high value (9000+). This helper maps:
12///   `4.10.0`      → `4.10.0`        (unchanged)
13///   `4.10.0-rc.1` → `4.10.0.9001`
14///   `4.10.0-rc.15`→ `4.10.0.9015`
15///
16/// The numeric suffix preserves ordering: rc.1 < rc.15 < release.
17///
18/// # Examples
19///
20/// ```
21/// use alef_core::version::to_r_version;
22/// assert_eq!(to_r_version("1.8.0"), "1.8.0");
23/// assert_eq!(to_r_version("4.10.0-rc.1"), "4.10.0.9001");
24/// assert_eq!(to_r_version("4.10.0-rc.15"), "4.10.0.9015");
25/// assert_eq!(to_r_version("0.1.0-alpha.2"), "0.1.0.9000");
26/// ```
27pub fn to_r_version(version: &str) -> String {
28    let Some((base, pre)) = version.split_once('-') else {
29        return version.to_string();
30    };
31
32    // For rc (release candidate) prereleases, encode the RC number as an offset
33    // from 9000 so that ordering is preserved within a series (rc.1 → 9001, rc.15 → 9015).
34    // All other prerelease identifiers (alpha, beta, dev, …) map to the base value 9000.
35    let numeric_offset: u32 = if pre.starts_with("rc") {
36        pre.split('.')
37            .filter_map(|part| part.parse::<u32>().ok())
38            .next_back()
39            .unwrap_or(0)
40    } else {
41        0
42    };
43
44    format!("{base}.{}", 9000 + numeric_offset)
45}
46
47/// Convert a semver pre-release version to RubyGems canonical prerelease format.
48///
49/// RubyGems rejects the dash-form prerelease syntax that cargo uses
50/// (`Gem::Version.new("1.8.0-rc.2")` raises) and requires the `.pre.` form.
51///
52/// # Examples
53///
54/// ```
55/// use alef_core::version::to_rubygems_prerelease;
56/// assert_eq!(to_rubygems_prerelease("1.8.0"), "1.8.0");
57/// assert_eq!(to_rubygems_prerelease("1.8.0-rc.2"), "1.8.0.pre.rc.2");
58/// assert_eq!(to_rubygems_prerelease("0.1.0-alpha.2"), "0.1.0.pre.alpha.2");
59/// ```
60pub fn to_rubygems_prerelease(version: &str) -> String {
61    if let Some((base, pre)) = version.split_once('-') {
62        let normalized_pre = pre.replace(['-', '_'], ".");
63        format!("{base}.pre.{normalized_pre}")
64    } else {
65        version.to_string()
66    }
67}
68
69#[cfg(test)]
70mod tests {
71    use super::*;
72
73    #[test]
74    fn release_version_is_unchanged() {
75        assert_eq!(to_rubygems_prerelease("1.8.0"), "1.8.0");
76        assert_eq!(to_rubygems_prerelease("0.1.0"), "0.1.0");
77    }
78
79    #[test]
80    fn rc_prerelease_uses_pre_dot_form() {
81        assert_eq!(to_rubygems_prerelease("1.8.0-rc.2"), "1.8.0.pre.rc.2");
82    }
83
84    #[test]
85    fn alpha_and_beta_prereleases_normalize() {
86        assert_eq!(to_rubygems_prerelease("0.1.0-alpha.2"), "0.1.0.pre.alpha.2");
87        assert_eq!(to_rubygems_prerelease("0.1.0-beta.3"), "0.1.0.pre.beta.3");
88    }
89
90    #[test]
91    fn dashes_in_prerelease_become_dots() {
92        assert_eq!(to_rubygems_prerelease("1.0.0-pre-rc-2"), "1.0.0.pre.pre.rc.2");
93    }
94
95    // --- to_r_version tests ---
96
97    #[test]
98    fn r_release_version_is_unchanged() {
99        assert_eq!(to_r_version("1.8.0"), "1.8.0");
100        assert_eq!(to_r_version("0.1.0"), "0.1.0");
101        assert_eq!(to_r_version("4.10.0"), "4.10.0");
102    }
103
104    #[test]
105    fn r_rc_prerelease_gets_9000_offset() {
106        assert_eq!(to_r_version("4.10.0-rc.1"), "4.10.0.9001");
107        assert_eq!(to_r_version("4.10.0-rc.15"), "4.10.0.9015");
108        assert_eq!(to_r_version("1.8.0-rc.2"), "1.8.0.9002");
109    }
110
111    #[test]
112    fn r_alpha_without_number_gets_9000() {
113        assert_eq!(to_r_version("0.1.0-alpha"), "0.1.0.9000");
114    }
115
116    #[test]
117    fn r_alpha_with_number_uses_offset() {
118        assert_eq!(to_r_version("0.1.0-alpha.2"), "0.1.0.9000");
119    }
120}