1use std::cmp::Ordering;
4use std::fmt;
5
6use semver::Prerelease;
7use sentry_release_parser::Version;
8use serde::de::Error;
9use serde::{Deserialize, Deserializer, Serialize, Serializer};
10
11#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct Release {
20 package: Option<String>,
21 quad: Quad,
22 pre: Prerelease,
23}
24
25type Quad = (u64, u64, u64, u64);
27
28impl Release {
29 pub fn parse(release: &str) -> Option<Self> {
33 let (package, quad, pre) = parse_parts(release)?;
34 Some(Self {
35 package: package.map(str::to_owned),
36 quad,
37 pre,
38 })
39 }
40
41 pub fn compare(&self, release: &str) -> Option<Ordering> {
50 let (package, quad, pre) = parse_parts(release)?;
51
52 if self.package.is_some() && self.package.as_deref() != package {
53 return None;
54 }
55
56 Some(quad.cmp(&self.quad).then_with(|| pre.cmp(&self.pre)))
57 }
58}
59
60impl fmt::Display for Release {
61 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62 if let Some(package) = &self.package {
63 write!(f, "{package}@")?;
64 }
65
66 let (major, minor, patch, revision) = self.quad;
67 write!(f, "{major}.{minor}.{patch}")?;
68 if revision != 0 {
69 write!(f, ".{revision}")?;
70 }
71 if !self.pre.is_empty() {
72 write!(f, "-{}", self.pre)?;
73 }
74
75 Ok(())
76 }
77}
78
79impl Serialize for Release {
80 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
81 serializer.collect_str(self)
82 }
83}
84
85impl<'de> Deserialize<'de> for Release {
86 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
87 let release = String::deserialize(deserializer)?;
88 Self::parse(&release).ok_or_else(|| D::Error::custom("release has no version"))
89 }
90}
91
92fn parse_parts(release: &str) -> Option<(Option<&str>, Quad, Prerelease)> {
94 let release = sentry_release_parser::Release::parse(release).ok()?;
95
96 let version = release.version_raw();
99 if release.build_hash() == Some(version) {
100 return None;
101 }
102 let version = Version::parse(version).ok()?;
103
104 Some((release.package(), version.quad(), version.as_semver1().pre))
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110
111 #[test]
112 fn test_releases_without_version() {
113 for release in ["a4b7e0f9c2d1", "myapp@a4b7e0f9c2d1", "123456789012", ""] {
114 assert_eq!(Release::parse(release), None, "{release}");
115 assert_eq!(
116 Release::parse("1.0.0").unwrap().compare(release),
117 None,
118 "{release}"
119 );
120 }
121 }
122
123 #[test]
124 fn test_compare_versions() {
125 let cases = [
126 ("1.9.0", "1.10.0", Ordering::Less),
127 ("1.2.3.4", "1.2.3.5", Ordering::Less),
128 ("1.2", "1.2.0", Ordering::Equal),
129 ("1.0.0-rc.1", "1.0.0", Ordering::Less),
130 ("1.0.0-beta.2", "1.0.0-beta.11", Ordering::Less),
131 ("1.0.0+build.2", "1.0.0+build.1", Ordering::Equal),
132 ];
133
134 for (release, other, expected) in cases {
135 let ordering = Release::parse(other).unwrap().compare(release);
136 assert_eq!(ordering, Some(expected), "{release} {other}");
137 }
138 }
139
140 #[test]
141 fn test_compare_packages() {
142 let cases = [
143 ("myapp@1.2.3", "1.2.3", Some(Ordering::Equal)),
144 ("1.2.3", "1.2.3", Some(Ordering::Equal)),
145 ("myapp@1.2.3", "myapp@1.2.3", Some(Ordering::Equal)),
146 ("myapp@1.2.3", "other@1.2.3", None),
147 ("1.2.3", "myapp@1.2.3", None),
148 ];
149
150 for (release, other, expected) in cases {
151 let ordering = Release::parse(other).unwrap().compare(release);
152 assert_eq!(ordering, expected, "{release} {other}");
153 }
154 }
155
156 #[test]
157 fn test_serde_normalizes_the_release() {
158 let cases = [
159 ("1.2.0", "1.2.0"),
160 ("1.2", "1.2.0"),
161 ("1.2.3.4", "1.2.3.4"),
162 ("myapp@1.2.0+build", "myapp@1.2.0"),
163 ("myapp@1.2.0-rc.1+build", "myapp@1.2.0-rc.1"),
164 ];
165
166 for (input, normalized) in cases {
167 let release: Release = serde_json::from_str(&format!("{input:?}")).unwrap();
168 assert_eq!(
169 serde_json::to_string(&release).unwrap(),
170 format!("{normalized:?}")
171 );
172 assert_eq!(Release::parse(normalized), Some(release), "{input}");
173 }
174 }
175
176 #[test]
177 fn test_deserialize_rejects_releases_without_version() {
178 assert!(serde_json::from_str::<Release>(r#""a4b7e0f9c2d1""#).is_err());
179 }
180}