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#[cfg(test)]
141mod tests {
142    use super::*;
143
144    #[test]
145    fn valid_package_versions() {
146        assert_eq!(Version::try_from("0.0.0").unwrap().to_string(), "0.0.0");
147        assert_eq!(Version::try_from("1.0.1").unwrap().to_string(), "1.0.1");
148        assert_eq!(
149            Version::try_from("23.123.343").unwrap().to_string(),
150            "23.123.343"
151        );
152        assert_eq!(
153            Version::try_from("2.43.3-alpha34").unwrap().to_string(),
154            "2.43.3-alpha34"
155        );
156        assert_eq!(
157            Version::try_from("0.1.1-45rdfsd-45").unwrap().to_string(),
158            "0.1.1-45rdfsd-45"
159        );
160    }
161
162    #[test]
163    fn eq_ignoring_build_ignores_only_build_metadata() {
164        let v = |s: &str| Version::try_from(s).unwrap();
165
166        assert!(v("1.0.0+a").eq_ignoring_build(&v("1.0.0+b")));
167        assert!(v("1.0.0").eq_ignoring_build(&v("1.0.0+b")));
168        assert!(v("1.0.0-rc.1+a").eq_ignoring_build(&v("1.0.0-rc.1")));
169        assert!(!v("1.0.0-rc.1").eq_ignoring_build(&v("1.0.0")));
170        assert!(!v("1.0.1+a").eq_ignoring_build(&v("1.0.0+a")));
171    }
172
173    #[test]
174    fn invalid_package_versions() {
175        assert_eq!(
176            Version::try_from("a.1.2").unwrap_err(),
177            VersionError::InvalidSemVer
178        );
179        assert_eq!(
180            Version::try_from("002.23.1").unwrap_err(),
181            VersionError::InvalidSemVer
182        );
183        assert_eq!(
184            Version::try_from("3.2fg.3").unwrap_err(),
185            VersionError::InvalidSemVer
186        );
187        assert_eq!(
188            Version::try_from("5.3.2.3").unwrap_err(),
189            VersionError::InvalidSemVer
190        );
191    }
192}