ambient_project/
version.rs

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        // We handle patch separately as it may have a suffix.
35        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}