Skip to main content

kellnr_common/
version.rs

1use std::convert::TryFrom;
2use std::fmt;
3use std::str::FromStr;
4
5use sea_orm::Value;
6use thiserror::Error;
7use utoipa::ToSchema;
8
9/// A crate version. It is always a valid `SemVer` version and is compared,
10/// ordered and hashed like one, including build metadata. It is displayed,
11/// serialized and stored as the version string it was parsed from.
12#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, ToSchema)]
13#[schema(value_type = String)]
14pub struct Version(semver::Version);
15
16impl serde::Serialize for Version {
17    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
18    where
19        S: serde::Serializer,
20    {
21        serializer.collect_str(&self.0)
22    }
23}
24
25impl<'de> serde::Deserialize<'de> for Version {
26    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
27    where
28        D: serde::Deserializer<'de>,
29    {
30        let s = <String as serde::Deserialize>::deserialize(deserializer)?;
31        Version::try_from(s.as_str()).map_err(serde::de::Error::custom)
32    }
33}
34
35#[derive(Debug, Eq, PartialEq, Error)]
36pub enum VersionError {
37    #[error("Invalid SemVer")]
38    InvalidSemVer,
39}
40
41impl Version {
42    /// Whether both versions are equal when build metadata is ignored. Cargo
43    /// does not distinguish `1.0.0+a` from `1.0.0+b`, so a registry must not
44    /// host both.
45    pub fn eq_ignoring_build(&self, other: &Version) -> bool {
46        let (a, b) = (&self.0, &other.0);
47        (a.major, a.minor, a.patch, &a.pre) == (b.major, b.minor, b.patch, &b.pre)
48    }
49}
50
51impl From<Version> for Value {
52    fn from(value: Version) -> Self {
53        Value::String(Some(value.to_string()))
54    }
55}
56
57impl From<&Version> for Value {
58    fn from(value: &Version) -> Self {
59        Value::String(Some(value.to_string()))
60    }
61}
62
63impl TryFrom<&str> for Version {
64    type Error = VersionError;
65
66    fn try_from(version: &str) -> Result<Self, Self::Error> {
67        semver::Version::parse(version)
68            .map(Version)
69            .map_err(|_| VersionError::InvalidSemVer)
70    }
71}
72
73impl TryFrom<&String> for Version {
74    type Error = VersionError;
75
76    fn try_from(version: &String) -> Result<Self, Self::Error> {
77        Version::try_from(version.as_str())
78    }
79}
80
81impl TryFrom<String> for Version {
82    type Error = VersionError;
83
84    fn try_from(version: String) -> Result<Self, Self::Error> {
85        Version::try_from(version.as_str())
86    }
87}
88
89impl FromStr for Version {
90    type Err = VersionError;
91
92    fn from_str(version: &str) -> Result<Self, Self::Err> {
93        Version::try_from(version)
94    }
95}
96
97impl AsRef<Version> for Version {
98    fn as_ref(&self) -> &Version {
99        self
100    }
101}
102
103impl Default for Version {
104    fn default() -> Self {
105        Version(semver::Version::new(0, 0, 0))
106    }
107}
108
109impl fmt::Debug for Version {
110    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
111        f.debug_tuple("Version").field(&self.to_string()).finish()
112    }
113}
114
115impl fmt::Display for Version {
116    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
117        fmt::Display::fmt(&self.0, f)
118    }
119}
120
121/// The latest of `versions`. Stable versions win over pre-releases, like on
122/// crates.io, so a pre-release is only the latest if there is no stable one.
123/// Versions that cannot be parsed are ignored.
124pub fn latest_version<'a>(versions: impl IntoIterator<Item = &'a str>) -> Option<String> {
125    versions
126        .into_iter()
127        .filter_map(|v| semver::Version::parse(v).ok())
128        .max_by(|a, b| (a.pre.is_empty(), a).cmp(&(b.pre.is_empty(), b)))
129        .map(|v| v.to_string())
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135
136    #[test]
137    fn latest_version_prefers_stable_versions() {
138        assert_eq!(
139            Some("2.0.0".to_string()),
140            latest_version(["1.0.0", "2.0.0", "1.5.0", "3.0.0-beta.1"])
141        );
142        assert_eq!(
143            Some("3.0.0-beta.2".to_string()),
144            latest_version(["3.0.0-beta.1", "3.0.0-beta.2"])
145        );
146        assert_eq!(
147            Some("1.0.0".to_string()),
148            latest_version(["1.0.0", "invalid"])
149        );
150        assert_eq!(None, latest_version([]));
151    }
152
153    #[test]
154    fn valid_package_versions() {
155        assert_eq!(Version::try_from("0.0.0").unwrap().to_string(), "0.0.0");
156        assert_eq!(Version::try_from("1.0.1").unwrap().to_string(), "1.0.1");
157        assert_eq!(
158            Version::try_from("23.123.343").unwrap().to_string(),
159            "23.123.343"
160        );
161        assert_eq!(
162            Version::try_from("2.43.3-alpha34").unwrap().to_string(),
163            "2.43.3-alpha34"
164        );
165        assert_eq!(
166            Version::try_from("0.1.1-45rdfsd-45").unwrap().to_string(),
167            "0.1.1-45rdfsd-45"
168        );
169    }
170
171    #[test]
172    fn eq_ignoring_build_ignores_only_build_metadata() {
173        let v = |s: &str| Version::try_from(s).unwrap();
174
175        assert!(v("1.0.0+a").eq_ignoring_build(&v("1.0.0+b")));
176        assert!(v("1.0.0").eq_ignoring_build(&v("1.0.0+b")));
177        assert!(v("1.0.0-rc.1+a").eq_ignoring_build(&v("1.0.0-rc.1")));
178        assert!(!v("1.0.0-rc.1").eq_ignoring_build(&v("1.0.0")));
179        assert!(!v("1.0.1+a").eq_ignoring_build(&v("1.0.0+a")));
180    }
181
182    #[test]
183    fn invalid_package_versions() {
184        assert_eq!(
185            Version::try_from("a.1.2").unwrap_err(),
186            VersionError::InvalidSemVer
187        );
188        assert_eq!(
189            Version::try_from("002.23.1").unwrap_err(),
190            VersionError::InvalidSemVer
191        );
192        assert_eq!(
193            Version::try_from("3.2fg.3").unwrap_err(),
194            VersionError::InvalidSemVer
195        );
196        assert_eq!(
197            Version::try_from("5.3.2.3").unwrap_err(),
198            VersionError::InvalidSemVer
199        );
200    }
201
202    #[test]
203    fn versions_are_ordered_like_semver() {
204        let v = |s: &str| Version::try_from(s).unwrap();
205
206        assert!(v("1.0.0-alpha") < v("1.0.0-alpha.1"));
207        assert!(v("1.0.0-alpha.1") < v("1.0.0-beta"));
208        assert!(v("1.0.0-rc.1") < v("1.0.0"));
209        assert!(v("1.0.0") < v("1.0.1"));
210        assert!(v("1.9.0") < v("1.10.0"));
211        assert!(v("1.0.0") < v("1.0.0+build"));
212        assert!(v("1.0.0+a") < v("1.0.0+b"));
213    }
214
215    #[test]
216    fn versions_with_different_build_metadata_are_not_equal() {
217        let v = |s: &str| Version::try_from(s).unwrap();
218
219        assert_eq!(v("1.0.0+a"), v("1.0.0+a"));
220        assert_ne!(v("1.0.0+a"), v("1.0.0+b"));
221        assert_ne!(v("1.0.0"), v("1.0.0+a"));
222        assert_ne!(v("1.0.0"), v("1.0.0-rc.1"));
223    }
224
225    #[test]
226    fn equal_versions_have_equal_hashes() {
227        use std::collections::HashSet;
228
229        let v = |s: &str| Version::try_from(s).unwrap();
230        let set: HashSet<Version> = [v("1.0.0"), v("1.0.0"), v("1.0.0+a"), v("1.0.0-rc.1")]
231            .into_iter()
232            .collect();
233
234        assert_eq!(set.len(), 3);
235        assert!(set.contains(&v("1.0.0+a")));
236        assert!(!set.contains(&v("1.0.0+b")));
237    }
238
239    #[test]
240    fn version_keeps_its_string_form() {
241        for s in [
242            "0.0.0",
243            "1.2.3",
244            "1.0.0-alpha.1",
245            "1.0.0+build",
246            "1.0.0-rc.1+build.5",
247            "2.43.3-alpha34",
248            "1.0.0+001",
249        ] {
250            let version = Version::try_from(s).unwrap();
251            assert_eq!(version.to_string(), s);
252            let json = serde_json::to_string(&version).unwrap();
253            assert_eq!(json, format!("\"{s}\""));
254            assert_eq!(serde_json::from_str::<Version>(&json).unwrap(), version);
255            assert_eq!(Value::from(&version), Value::String(Some(s.to_string())));
256        }
257    }
258
259    #[test]
260    fn deserializing_an_invalid_version_fails() {
261        assert!(serde_json::from_str::<Version>("\"1.0\"").is_err());
262        assert!(serde_json::from_str::<Version>("\"invalid\"").is_err());
263    }
264}