Skip to main content

semifold_core/
versioning.rs

1use semver::{Prerelease, Version};
2use serde::Serialize;
3
4use crate::BumpLevel;
5
6/// Release channel selected for a package.
7#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize)]
8#[serde(tag = "kind", content = "name", rename_all = "snake_case")]
9pub enum ReleaseChannel {
10    #[default]
11    Stable,
12    Named(String),
13}
14
15/// Computes one package version without reading configuration or manifests.
16pub fn bump_version(
17    current: &Version,
18    bump: BumpLevel,
19    channel: &ReleaseChannel,
20) -> Result<Version, VersioningError> {
21    let mut next = current.clone();
22    if bump == BumpLevel::Unchanged {
23        return Ok(next);
24    }
25
26    match channel {
27        ReleaseChannel::Stable => {
28            if next.pre.is_empty() {
29                bump_stable_base(&mut next, bump);
30            } else {
31                next.pre = Prerelease::EMPTY;
32            }
33        }
34        ReleaseChannel::Named(name) => {
35            if next.pre.is_empty() {
36                bump_stable_base(&mut next, bump);
37                set_named_channel(&mut next, name, 0)?;
38            } else {
39                advance_named_channel(&mut next, name)?;
40            }
41        }
42    }
43    Ok(next)
44}
45
46fn bump_stable_base(version: &mut Version, bump: BumpLevel) {
47    match bump {
48        BumpLevel::Major => {
49            version.major += 1;
50            version.minor = 0;
51            version.patch = 0;
52        }
53        BumpLevel::Minor => {
54            version.minor += 1;
55            version.patch = 0;
56        }
57        BumpLevel::Patch => version.patch += 1,
58        BumpLevel::Unchanged => {}
59    }
60}
61
62fn set_named_channel(
63    version: &mut Version,
64    channel: &str,
65    sequence: u64,
66) -> Result<(), VersioningError> {
67    version.pre = Prerelease::new(&format!("{channel}.{sequence}")).map_err(|error| {
68        VersioningError::InvalidChannel {
69            channel: channel.to_string(),
70            reason: error.to_string(),
71        }
72    })?;
73    Ok(())
74}
75
76fn advance_named_channel(version: &mut Version, channel: &str) -> Result<(), VersioningError> {
77    let prefix = format!("{channel}.");
78    let sequence = version
79        .pre
80        .as_str()
81        .strip_prefix(&prefix)
82        .map(|value| {
83            value
84                .parse::<u64>()
85                .map(|sequence| sequence + 1)
86                .map_err(|error| VersioningError::InvalidSequence {
87                    version: version.clone(),
88                    channel: channel.to_string(),
89                    reason: error.to_string(),
90                })
91        })
92        .transpose()?
93        .unwrap_or(0);
94    set_named_channel(version, channel, sequence)
95}
96
97#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
98pub enum VersioningError {
99    #[error("invalid release channel {channel}: {reason}")]
100    InvalidChannel { channel: String, reason: String },
101    #[error("invalid {channel} sequence in version {version}: {reason}")]
102    InvalidSequence {
103        version: Version,
104        channel: String,
105        reason: String,
106    },
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112
113    fn version(value: &str) -> Version {
114        Version::parse(value).unwrap()
115    }
116
117    #[test]
118    fn computes_stable_and_named_channel_transitions() {
119        assert_eq!(
120            bump_version(&version("1.2.3"), BumpLevel::Minor, &ReleaseChannel::Stable).unwrap(),
121            version("1.3.0")
122        );
123        assert_eq!(
124            bump_version(
125                &version("1.2.3"),
126                BumpLevel::Minor,
127                &ReleaseChannel::Named("alpha".to_string())
128            )
129            .unwrap(),
130            version("1.3.0-alpha.0")
131        );
132        assert_eq!(
133            bump_version(
134                &version("1.3.0-alpha.2"),
135                BumpLevel::Major,
136                &ReleaseChannel::Named("alpha".to_string())
137            )
138            .unwrap(),
139            version("1.3.0-alpha.3")
140        );
141        assert_eq!(
142            bump_version(
143                &version("1.3.0-alpha.2"),
144                BumpLevel::Patch,
145                &ReleaseChannel::Named("beta".to_string())
146            )
147            .unwrap(),
148            version("1.3.0-beta.0")
149        );
150        assert_eq!(
151            bump_version(
152                &version("1.3.0-alpha.2"),
153                BumpLevel::Patch,
154                &ReleaseChannel::Stable
155            )
156            .unwrap(),
157            version("1.3.0")
158        );
159    }
160
161    #[test]
162    fn unchanged_never_advances_a_channel() {
163        let current = version("1.3.0-alpha.2");
164        assert_eq!(
165            bump_version(
166                &current,
167                BumpLevel::Unchanged,
168                &ReleaseChannel::Named("alpha".to_string())
169            )
170            .unwrap(),
171            current
172        );
173    }
174}