1use std::{fmt::Display, num::NonZeroUsize};
2
3use serde::{de::Visitor, Deserialize, Serialize};
4use thiserror::Error;
5
6#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
7pub struct Version {
8 major: u32,
9 minor: u32,
10 patch: u32,
11 suffix: VersionSuffix,
12}
13impl Version {
14 pub fn new(major: u32, minor: u32, patch: u32, suffix: VersionSuffix) -> Self {
15 Self {
16 major,
17 minor,
18 patch,
19 suffix,
20 }
21 }
22
23 pub fn new_from_str(id: &str) -> Result<Self, VersionError> {
24 if id.trim().is_empty() {
25 return Err(VersionError::TooFewComponents);
26 }
27
28 let mut segments = id.split('.');
29 let major = segments
30 .next()
31 .ok_or(VersionError::TooFewComponents)?
32 .parse()?;
33 let minor = segments.next().map(|s| s.parse()).transpose()?.unwrap_or(0);
34 let (patch, suffix) = if let Some(patch) = segments.next() {
36 let (patch, suffix) = match patch.split_once('-') {
37 Some((patch, suffix)) => (patch, Some(suffix)),
38 None => (patch, None),
39 };
40
41 (
42 patch.parse()?,
43 VersionSuffix::new_from_str(suffix.unwrap_or_default())?,
44 )
45 } else {
46 (0, VersionSuffix::Final)
47 };
48
49 if segments.next().is_some() {
50 return Err(VersionError::TooManyComponents);
51 }
52
53 if [major, minor, patch].iter().all(|v| *v == 0) {
54 return Err(VersionError::AllZero);
55 }
56
57 Ok(Self {
58 major,
59 minor,
60 patch,
61 suffix,
62 })
63 }
64}
65impl Display for Version {
66 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67 match &self.suffix {
68 VersionSuffix::Final => write!(f, "{}.{}.{}", self.major, self.minor, self.patch),
69 suf => write!(f, "{}.{}.{}-{}", self.major, self.minor, self.patch, suf),
70 }
71 }
72}
73impl Serialize for Version {
74 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
75 where
76 S: serde::Serializer,
77 {
78 self.to_string().serialize(serializer)
79 }
80}
81impl<'de> Deserialize<'de> for Version {
82 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
83 where
84 D: serde::Deserializer<'de>,
85 {
86 deserializer.deserialize_str(VersionVisitor)
87 }
88}
89
90struct VersionVisitor;
91impl<'de> Visitor<'de> for VersionVisitor {
92 type Value = Version;
93
94 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
95 formatter.write_str(
96 "a semantic dot-separated version with up to three components and an optional prefix",
97 )
98 }
99
100 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
101 where
102 E: serde::de::Error,
103 {
104 Version::new_from_str(v).map_err(serde::de::Error::custom)
105 }
106}
107
108#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
109pub enum VersionSuffix {
110 Other(String),
111 Dev,
112 Alpha(Option<NonZeroUsize>),
113 Beta(Option<NonZeroUsize>),
114 ReleaseCandidate(Option<NonZeroUsize>),
115 #[default]
116 Final,
117}
118impl VersionSuffix {
119 const RELEASE_CANDIDATE: &str = "rc";
120 const BETA: &str = "beta";
121 const ALPHA: &str = "alpha";
122 const DEV: &str = "dev";
123
124 pub fn new_from_str(id: &str) -> Result<Self, VersionError> {
125 if id.is_empty() {
126 Ok(Self::Final)
127 } else if let Some(version) = id.strip_prefix(Self::RELEASE_CANDIDATE) {
128 Ok(Self::ReleaseCandidate(NonZeroUsize::new(version.parse()?)))
129 } else if let Some(version) = id.strip_prefix(Self::BETA) {
130 Ok(Self::Beta(NonZeroUsize::new(version.parse()?)))
131 } else if let Some(version) = id.strip_prefix(Self::ALPHA) {
132 Ok(Self::Alpha(NonZeroUsize::new(version.parse()?)))
133 } else if id == Self::DEV {
134 Ok(Self::Dev)
135 } else {
136 Ok(Self::Other(id.to_string()))
137 }
138 }
139}
140impl Display for VersionSuffix {
141 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
142 let (prefix, suffix) = match self {
143 VersionSuffix::Final => ("", None),
144 VersionSuffix::ReleaseCandidate(v) => (Self::RELEASE_CANDIDATE, *v),
145 VersionSuffix::Beta(v) => (Self::BETA, *v),
146 VersionSuffix::Alpha(v) => (Self::ALPHA, *v),
147 VersionSuffix::Dev => (Self::DEV, None),
148 VersionSuffix::Other(v) => (v.as_str(), None),
149 };
150
151 write!(f, "{prefix}")?;
152 if let Some(v) = suffix {
153 write!(f, "{v}")?;
154 }
155
156 Ok(())
157 }
158}
159
160#[derive(Error, Debug, PartialEq)]
161pub enum VersionError {
162 #[error("invalid number in version segment")]
163 InvalidNumber(#[from] std::num::ParseIntError),
164 #[error("too few components in version (at least one required)")]
165 TooFewComponents,
166 #[error("too many components (at most three required)")]
167 TooManyComponents,
168 #[error("all components were zero")]
169 AllZero,
170}
171
172#[cfg(test)]
173mod tests {
174 use std::num::NonZeroUsize;
175
176 use crate::{Version, VersionError, VersionSuffix};
177
178 #[test]
179 fn can_parse_versions() {
180 use Version as V;
181 use VersionSuffix as VS;
182
183 assert_eq!(V::new_from_str("1"), Ok(V::new(1, 0, 0, VS::Final)));
184 assert_eq!(V::new_from_str("1.0"), Ok(V::new(1, 0, 0, VS::Final)));
185 assert_eq!(V::new_from_str("1.0.0"), Ok(V::new(1, 0, 0, VS::Final)));
186 assert_eq!(V::new_from_str("1.2.3"), Ok(V::new(1, 2, 3, VS::Final)));
187 assert_eq!(
188 V::new_from_str("1.2.3-rc1"),
189 Ok(V::new(1, 2, 3, VS::ReleaseCandidate(NonZeroUsize::new(1))))
190 );
191
192 assert_eq!(V::new_from_str(""), Err(VersionError::TooFewComponents));
193 assert_eq!(V::new_from_str("0.0.0"), Err(VersionError::AllZero));
194 assert!(matches!(
195 V::new_from_str("1.2.3patch"),
196 Err(VersionError::InvalidNumber(_))
197 ));
198 assert_eq!(
199 V::new_from_str("1.2.3.4"),
200 Err(VersionError::TooManyComponents)
201 );
202 }
203
204 #[test]
205 fn can_roundtrip_serialize_versions() {
206 use Version as V;
207 use VersionSuffix as VS;
208
209 let versions = [
210 V::new(1, 0, 0, VS::Final),
211 V::new(1, 0, 0, VS::Dev),
212 V::new(1, 0, 0, VS::ReleaseCandidate(NonZeroUsize::new(1))),
213 V::new(123, 456, 789, VS::ReleaseCandidate(NonZeroUsize::new(1))),
214 V::new(123, 456, 789, VS::Final),
215 ];
216
217 for version in versions {
218 assert_eq!(
219 version,
220 serde_json::from_str(&serde_json::to_string(&version).unwrap()).unwrap()
221 );
222 }
223 }
224
225 #[test]
226 fn can_sort_versions() {
227 use Version as V;
228 use VersionSuffix as VS;
229
230 let versions = [
231 V::new(0, 0, 1, VS::Final),
232 V::new(0, 1, 0, VS::Dev),
233 V::new(0, 1, 0, VS::Final),
234 V::new(0, 1, 1, VS::Final),
235 V::new(0, 1, 12, VS::Final),
236 V::new(1, 0, 0, VS::Other("pancakes".to_string())),
237 V::new(1, 0, 0, VS::Dev),
238 V::new(1, 0, 0, VS::Alpha(None)),
239 V::new(1, 0, 0, VS::Alpha(NonZeroUsize::new(1))),
240 V::new(1, 0, 0, VS::Beta(NonZeroUsize::new(1))),
241 V::new(1, 0, 0, VS::ReleaseCandidate(None)),
242 V::new(1, 0, 0, VS::ReleaseCandidate(NonZeroUsize::new(1))),
243 V::new(1, 0, 0, VS::Final),
244 V::new(123, 456, 789, VS::ReleaseCandidate(NonZeroUsize::new(1))),
245 V::new(123, 456, 789, VS::Final),
246 ];
247
248 for [v1, v2] in versions.windows(2).map(|w| [&w[0], &w[1]]) {
249 if *v1 >= *v2 {
250 panic!("failed comparison: {v1} is not less than {v2}");
251 }
252 }
253 }
254}