1use std::cmp::Ordering;
10use std::fmt;
11use std::str::FromStr;
12
13use serde::{Deserialize, Serialize};
14
15use crate::errors::Error;
16
17#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
18pub enum Prerelease {
19 Alpha(String),
20 Beta(String),
21 Rc(String),
22}
23
24impl Prerelease {
25 pub fn is_alpha(&self) -> bool {
26 matches!(self, Prerelease::Alpha(_))
27 }
28}
29
30impl fmt::Display for Prerelease {
31 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
32 match self {
33 Prerelease::Alpha(s) => write!(f, "alpha.{}", s),
34 Prerelease::Beta(s) => write!(f, "beta.{}", s),
35 Prerelease::Rc(s) => write!(f, "rc.{}", s),
36 }
37 }
38}
39
40fn compare_prerelease_identifiers(a: &str, b: &str) -> Ordering {
41 match (a.parse::<u32>(), b.parse::<u32>()) {
42 (Ok(na), Ok(nb)) => na.cmp(&nb),
43 _ => a.cmp(b),
44 }
45}
46
47impl Ord for Prerelease {
48 fn cmp(&self, other: &Self) -> Ordering {
49 match (self, other) {
50 (Prerelease::Alpha(a), Prerelease::Alpha(b)) => compare_prerelease_identifiers(a, b),
51 (Prerelease::Alpha(_), _) => Ordering::Less,
52 (_, Prerelease::Alpha(_)) => Ordering::Greater,
53 (Prerelease::Beta(a), Prerelease::Beta(b)) => compare_prerelease_identifiers(a, b),
54 (Prerelease::Beta(_), Prerelease::Rc(_)) => Ordering::Less,
55 (Prerelease::Rc(_), Prerelease::Beta(_)) => Ordering::Greater,
56 (Prerelease::Rc(a), Prerelease::Rc(b)) => compare_prerelease_identifiers(a, b),
57 }
58 }
59}
60
61impl PartialOrd for Prerelease {
62 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
63 Some(self.cmp(other))
64 }
65}
66
67#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
68pub struct Version {
69 pub major: u32,
70 pub minor: u32,
71 pub patch: u32,
72 #[serde(skip_serializing_if = "Option::is_none")]
73 pub prerelease: Option<Prerelease>,
74}
75
76impl Version {
77 pub fn new(major: u32, minor: u32, patch: u32) -> Self {
78 Self {
79 major,
80 minor,
81 patch,
82 prerelease: None,
83 }
84 }
85
86 pub fn with_prerelease(major: u32, minor: u32, patch: u32, prerelease: Prerelease) -> Self {
87 Self {
88 major,
89 minor,
90 patch,
91 prerelease: Some(prerelease),
92 }
93 }
94
95 pub fn dir_name(&self) -> String {
96 self.to_string()
97 }
98
99 pub fn is_distributed_via_server_packages_repository(&self) -> bool {
100 self.prerelease.as_ref().is_some_and(|p| p.is_alpha())
101 }
102
103 pub fn is_ga(&self) -> bool {
104 self.prerelease.is_none()
105 }
106
107 pub fn download_url(&self) -> String {
108 format!(
109 "https://github.com/rabbitmq/rabbitmq-server/releases/download/v{v}/rabbitmq-server-generic-unix-{v}.tar.xz",
110 v = self
111 )
112 }
113
114 pub fn download_url_with_tag(&self, tag: &str) -> String {
115 format!(
116 "https://github.com/rabbitmq/server-packages/releases/download/{tag}/rabbitmq-server-generic-unix-{v}.tar.xz",
117 tag = tag,
118 v = self
119 )
120 }
121
122 pub fn archive_name(&self) -> String {
123 format!("rabbitmq-server-generic-unix-{}.tar.xz", self)
124 }
125
126 pub fn extracted_dir_name(&self) -> String {
127 format!("rabbitmq_server-{}", self)
128 }
129}
130
131impl fmt::Display for Version {
132 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
133 write!(f, "{}.{}.{}", self.major, self.minor, self.patch)?;
134 if let Some(ref pre) = self.prerelease {
135 write!(f, "-{}", pre)?;
136 }
137 Ok(())
138 }
139}
140
141impl FromStr for Version {
142 type Err = Error;
143
144 fn from_str(s: &str) -> Result<Self, Self::Err> {
145 let s = s.trim().trim_start_matches('v');
146
147 let (version_part, prerelease) = if let Some(idx) = s.find('-') {
148 let (ver, pre) = s.split_at(idx);
149 let pre = &pre[1..];
150 (ver, Some(parse_prerelease(pre, s)?))
151 } else {
152 (s, None)
153 };
154
155 let parts: Vec<&str> = version_part.split('.').collect();
156
157 if parts.len() != 3 {
158 return Err(Error::InvalidVersion(s.to_string()));
159 }
160
161 let major = parts[0]
162 .parse()
163 .map_err(|_| Error::InvalidVersion(s.to_string()))?;
164 let minor = parts[1]
165 .parse()
166 .map_err(|_| Error::InvalidVersion(s.to_string()))?;
167 let patch = parts[2]
168 .parse()
169 .map_err(|_| Error::InvalidVersion(s.to_string()))?;
170
171 Ok(Version {
172 major,
173 minor,
174 patch,
175 prerelease,
176 })
177 }
178}
179
180fn parse_prerelease(s: &str, full: &str) -> Result<Prerelease, Error> {
181 let parts: Vec<&str> = s.split('.').collect();
182 if parts.len() != 2 {
183 return Err(Error::InvalidVersion(full.to_string()));
184 }
185
186 let identifier = parts[1];
187 if identifier.is_empty() {
188 return Err(Error::InvalidVersion(full.to_string()));
189 }
190
191 match parts[0].to_lowercase().as_str() {
192 "alpha" => Ok(Prerelease::Alpha(identifier.to_string())),
193 "beta" => Ok(Prerelease::Beta(identifier.to_string())),
194 "rc" => Ok(Prerelease::Rc(identifier.to_string())),
195 _ => Err(Error::InvalidVersion(full.to_string())),
196 }
197}
198
199impl Ord for Version {
200 fn cmp(&self, other: &Self) -> Ordering {
201 let base =
202 (self.major, self.minor, self.patch).cmp(&(other.major, other.minor, other.patch));
203 if base != Ordering::Equal {
204 return base;
205 }
206
207 match (&self.prerelease, &other.prerelease) {
208 (None, None) => Ordering::Equal,
209 (Some(_), None) => Ordering::Less,
210 (None, Some(_)) => Ordering::Greater,
211 (Some(a), Some(b)) => a.cmp(b),
212 }
213 }
214}
215
216impl PartialOrd for Version {
217 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
218 Some(self.cmp(other))
219 }
220}