Skip to main content

kellnr_common/
version.rs

1use std::cmp::Ordering;
2use std::convert::TryFrom;
3use std::fmt;
4use std::hash::{Hash, Hasher};
5use std::ops::Deref;
6
7use sea_orm::Value;
8use thiserror::Error;
9use utoipa::ToSchema;
10
11#[derive(Debug, Eq, Clone, serde::Serialize, ToSchema)]
12#[schema(value_type = String)]
13pub struct Version(String);
14
15impl<'de> serde::Deserialize<'de> for Version {
16    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
17    where
18        D: serde::Deserializer<'de>,
19    {
20        let s = <String as serde::Deserialize>::deserialize(deserializer)?;
21        semver::Version::parse(&s)
22            .map(|sv| Version(sv.to_string()))
23            .map_err(|e| serde::de::Error::custom(e.to_string()))
24    }
25}
26
27#[derive(Debug, Eq, PartialEq, Error)]
28pub enum VersionError {
29    #[error("Invalid SemVer")]
30    InvalidSemVer,
31}
32
33impl Version {
34    pub fn from_unchecked_str(version: &str) -> Self {
35        Self(version.to_string())
36    }
37    pub fn into_inner(self) -> String {
38        self.0
39    }
40
41    /// Whether both versions are equal when build metadata is ignored. Cargo
42    /// does not distinguish `1.0.0+a` from `1.0.0+b`, so a registry must not
43    /// host both.
44    pub fn eq_ignoring_build(&self, other: &Version) -> bool {
45        match (semver::Version::parse(self), semver::Version::parse(other)) {
46            (Ok(a), Ok(b)) => {
47                (a.major, a.minor, a.patch, a.pre) == (b.major, b.minor, b.patch, b.pre)
48            }
49            _ => self.0 == other.0,
50        }
51    }
52}
53
54impl From<Version> for Value {
55    fn from(value: Version) -> Self {
56        Value::String(Some(value.0))
57    }
58}
59
60impl From<&Version> for Value {
61    fn from(value: &Version) -> Self {
62        Value::String(Some(value.0.clone()))
63    }
64}
65
66impl TryFrom<&str> for Version {
67    type Error = VersionError;
68
69    fn try_from(version: &str) -> Result<Self, Self::Error> {
70        Version::try_from(&version.to_string())
71    }
72}
73
74impl TryFrom<&String> for Version {
75    type Error = VersionError;
76
77    fn try_from(version: &String) -> Result<Self, Self::Error> {
78        match semver::Version::parse(version) {
79            Ok(sv) => Ok(Version(sv.to_string())),
80            Err(_) => Err(VersionError::InvalidSemVer),
81        }
82    }
83}
84
85impl AsRef<Version> for Version {
86    fn as_ref(&self) -> &Version {
87        self
88    }
89}
90
91impl Deref for Version {
92    type Target = String;
93
94    fn deref(&self) -> &Self::Target {
95        &self.0
96    }
97}
98
99impl Default for Version {
100    fn default() -> Self {
101        Version(semver::Version::new(0, 0, 0).to_string())
102    }
103}
104
105impl Ord for Version {
106    fn cmp(&self, other: &Self) -> Ordering {
107        let sv1 = semver::Version::parse(&self.to_string()).unwrap();
108        let sv2 = semver::Version::parse(&other.to_string()).unwrap();
109        sv1.cmp(&sv2)
110    }
111}
112
113impl PartialOrd for Version {
114    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
115        Some(self.cmp(other))
116    }
117}
118
119impl PartialEq for Version {
120    fn eq(&self, other: &Self) -> bool {
121        let sv1 = semver::Version::parse(&self.to_string()).unwrap();
122        let sv2 = semver::Version::parse(&other.to_string()).unwrap();
123        sv1 == sv2
124    }
125}
126
127impl Hash for Version {
128    fn hash<H: Hasher>(&self, state: &mut H) {
129        let sv = semver::Version::parse(&self.to_string()).unwrap();
130        sv.hash(state);
131    }
132}
133
134impl fmt::Display for Version {
135    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
136        write!(f, "{}", self.0)
137    }
138}
139
140/// The latest of `versions`. Stable versions win over pre-releases, like on
141/// crates.io, so a pre-release is only the latest if there is no stable one.
142/// Versions that cannot be parsed are ignored.
143pub fn latest_version<'a>(versions: impl IntoIterator<Item = &'a str>) -> Option<String> {
144    versions
145        .into_iter()
146        .filter_map(|v| semver::Version::parse(v).ok())
147        .max_by(|a, b| (a.pre.is_empty(), a).cmp(&(b.pre.is_empty(), b)))
148        .map(|v| v.to_string())
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154
155    #[test]
156    fn latest_version_prefers_stable_versions() {
157        assert_eq!(
158            Some("2.0.0".to_string()),
159            latest_version(["1.0.0", "2.0.0", "1.5.0", "3.0.0-beta.1"])
160        );
161        assert_eq!(
162            Some("3.0.0-beta.2".to_string()),
163            latest_version(["3.0.0-beta.1", "3.0.0-beta.2"])
164        );
165        assert_eq!(
166            Some("1.0.0".to_string()),
167            latest_version(["1.0.0", "invalid"])
168        );
169        assert_eq!(None, latest_version([]));
170    }
171
172    #[test]
173    fn valid_package_versions() {
174        assert_eq!(Version::try_from("0.0.0").unwrap().to_string(), "0.0.0");
175        assert_eq!(Version::try_from("1.0.1").unwrap().to_string(), "1.0.1");
176        assert_eq!(
177            Version::try_from("23.123.343").unwrap().to_string(),
178            "23.123.343"
179        );
180        assert_eq!(
181            Version::try_from("2.43.3-alpha34").unwrap().to_string(),
182            "2.43.3-alpha34"
183        );
184        assert_eq!(
185            Version::try_from("0.1.1-45rdfsd-45").unwrap().to_string(),
186            "0.1.1-45rdfsd-45"
187        );
188    }
189
190    #[test]
191    fn eq_ignoring_build_ignores_only_build_metadata() {
192        let v = |s: &str| Version::try_from(s).unwrap();
193
194        assert!(v("1.0.0+a").eq_ignoring_build(&v("1.0.0+b")));
195        assert!(v("1.0.0").eq_ignoring_build(&v("1.0.0+b")));
196        assert!(v("1.0.0-rc.1+a").eq_ignoring_build(&v("1.0.0-rc.1")));
197        assert!(!v("1.0.0-rc.1").eq_ignoring_build(&v("1.0.0")));
198        assert!(!v("1.0.1+a").eq_ignoring_build(&v("1.0.0+a")));
199    }
200
201    #[test]
202    fn invalid_package_versions() {
203        assert_eq!(
204            Version::try_from("a.1.2").unwrap_err(),
205            VersionError::InvalidSemVer
206        );
207        assert_eq!(
208            Version::try_from("002.23.1").unwrap_err(),
209            VersionError::InvalidSemVer
210        );
211        assert_eq!(
212            Version::try_from("3.2fg.3").unwrap_err(),
213            VersionError::InvalidSemVer
214        );
215        assert_eq!(
216            Version::try_from("5.3.2.3").unwrap_err(),
217            VersionError::InvalidSemVer
218        );
219    }
220}