semifold_core/
versioning.rs1use semver::{Prerelease, Version};
2use serde::Serialize;
3
4use crate::BumpLevel;
5
6#[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
15pub 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 ¤t,
167 BumpLevel::Unchanged,
168 &ReleaseChannel::Named("alpha".to_string())
169 )
170 .unwrap(),
171 current
172 );
173 }
174}