hyperdb_api/
server_version.rs1use std::fmt;
22
23#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct ServerVersion {
34 major: u32,
35 minor: u32,
36 patch: u32,
37 suffix: Option<String>,
39 raw: String,
41}
42
43impl ServerVersion {
44 #[must_use]
46 pub fn new(major: u32, minor: u32, patch: u32) -> Self {
47 ServerVersion {
48 major,
49 minor,
50 patch,
51 suffix: None,
52 raw: format!("{major}.{minor}.{patch}"),
53 }
54 }
55
56 #[must_use]
69 pub fn parse(s: &str) -> Option<Self> {
70 let trimmed = s.trim();
71 let trimmed = trimmed
73 .strip_prefix('v')
74 .or_else(|| trimmed.strip_prefix('V'))
75 .unwrap_or(trimmed);
76
77 let mut parts = trimmed.splitn(4, '.');
78
79 let major: u32 = parts.next()?.parse().ok()?;
80 let minor: u32 = parts.next()?.parse().ok()?;
81
82 let (patch, suffix) = if let Some(patch_str) = parts.next() {
84 let num_end = patch_str
88 .find(|c: char| !c.is_ascii_digit())
89 .unwrap_or(patch_str.len());
90 if num_end == 0 {
91 return None;
93 }
94 let patch: u32 = patch_str[..num_end].parse().ok()?;
95 let patch_tail = &patch_str[num_end..];
98 let dot_tail = parts.next();
99 let suffix = match (patch_tail.is_empty(), dot_tail) {
100 (true, None) => None,
101 (true, Some(t)) => Some(t.to_string()),
102 (false, None) => Some(patch_tail.to_string()),
103 (false, Some(t)) => Some(format!("{patch_tail}.{t}")),
104 };
105 (patch, suffix)
106 } else {
107 (0, None)
108 };
109
110 Some(ServerVersion {
111 major,
112 minor,
113 patch,
114 suffix,
115 raw: s.trim().to_string(),
116 })
117 }
118
119 #[must_use]
121 pub fn major(&self) -> u32 {
122 self.major
123 }
124
125 #[must_use]
127 pub fn minor(&self) -> u32 {
128 self.minor
129 }
130
131 #[must_use]
133 pub fn patch(&self) -> u32 {
134 self.patch
135 }
136
137 #[must_use]
139 pub fn suffix(&self) -> Option<&str> {
140 self.suffix.as_deref()
141 }
142
143 #[must_use]
145 pub fn raw(&self) -> &str {
146 &self.raw
147 }
148}
149
150impl PartialOrd for ServerVersion {
151 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
152 Some(self.cmp(other))
153 }
154}
155
156impl Ord for ServerVersion {
157 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
158 (self.major, self.minor, self.patch).cmp(&(other.major, other.minor, other.patch))
159 }
160}
161
162impl fmt::Display for ServerVersion {
163 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
164 write!(f, "{}", self.raw)
165 }
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171
172 #[test]
173 fn test_parse_basic() {
174 let v = ServerVersion::parse("0.0.19038").unwrap();
175 assert_eq!(v.major(), 0);
176 assert_eq!(v.minor(), 0);
177 assert_eq!(v.patch(), 19038);
178 assert_eq!(v.suffix(), None);
179 }
180
181 #[test]
182 fn test_parse_with_suffix() {
183 let v = ServerVersion::parse("1.2.3.r456").unwrap();
184 assert_eq!(v.major(), 1);
185 assert_eq!(v.minor(), 2);
186 assert_eq!(v.patch(), 3);
187 assert_eq!(v.suffix(), Some("r456"));
188 }
189
190 #[test]
191 fn test_parse_two_parts() {
192 let v = ServerVersion::parse("1.2").unwrap();
193 assert_eq!(v.major(), 1);
194 assert_eq!(v.minor(), 2);
195 assert_eq!(v.patch(), 0);
196 }
197
198 #[test]
199 fn test_parse_invalid() {
200 assert!(ServerVersion::parse("").is_none());
201 assert!(ServerVersion::parse("abc").is_none());
202 assert!(ServerVersion::parse("1").is_none());
203 assert!(ServerVersion::parse("1.2.abc").is_none());
204 assert!(ServerVersion::parse("vabc").is_none());
205 assert!(ServerVersion::parse("v").is_none());
206 }
207
208 #[test]
209 fn test_parse_v_prefix() {
210 let v = ServerVersion::parse("v1.2.3").unwrap();
211 assert_eq!(v.major(), 1);
212 assert_eq!(v.minor(), 2);
213 assert_eq!(v.patch(), 3);
214 assert_eq!(v.suffix(), None);
215 }
216
217 #[test]
218 fn test_parse_uppercase_v_prefix() {
219 let v = ServerVersion::parse("V1.2.3").unwrap();
220 assert_eq!(v.major(), 1);
221 assert_eq!(v.patch(), 3);
222 }
223
224 #[test]
225 fn test_parse_hyphen_prerelease() {
226 let v = ServerVersion::parse("1.2.3-beta1").unwrap();
227 assert_eq!(v.major(), 1);
228 assert_eq!(v.minor(), 2);
229 assert_eq!(v.patch(), 3);
230 assert_eq!(v.suffix(), Some("-beta1"));
231 }
232
233 #[test]
234 fn test_parse_patch_with_rc_suffix() {
235 let v = ServerVersion::parse("0.0.19038rc1").unwrap();
236 assert_eq!(v.patch(), 19038);
237 assert_eq!(v.suffix(), Some("rc1"));
238 }
239
240 #[test]
241 fn test_parse_hyphen_prerelease_with_dot_suffix() {
242 let v = ServerVersion::parse("1.2.3-beta.1").unwrap();
243 assert_eq!(v.patch(), 3);
244 assert_eq!(v.suffix(), Some("-beta.1"));
246 }
247
248 #[test]
249 fn test_parse_whitespace() {
250 let v = ServerVersion::parse(" 1.2.3 ").unwrap();
251 assert_eq!(v.major(), 1);
252 assert_eq!(v.patch(), 3);
253 }
254
255 #[test]
256 fn test_comparison() {
257 let v1 = ServerVersion::new(1, 0, 0);
258 let v2 = ServerVersion::new(1, 0, 1);
259 let v3 = ServerVersion::new(1, 1, 0);
260 let v4 = ServerVersion::new(2, 0, 0);
261
262 assert!(v1 < v2);
263 assert!(v2 < v3);
264 assert!(v3 < v4);
265 assert!(v1 == ServerVersion::new(1, 0, 0));
266 }
267
268 #[test]
269 fn test_comparison_ignores_suffix() {
270 let v1 = ServerVersion::parse("1.2.3").unwrap();
271 let v2 = ServerVersion::parse("1.2.3-beta1").unwrap();
272 assert_eq!(v1.cmp(&v2), std::cmp::Ordering::Equal);
274 assert!(v1 >= v2);
275 assert!(v2 >= v1);
276 }
277
278 #[test]
279 fn test_display() {
280 let v = ServerVersion::parse("0.0.19038").unwrap();
281 assert_eq!(format!("{v}"), "0.0.19038");
282 }
283
284 #[test]
285 fn test_display_preserves_original() {
286 let v = ServerVersion::parse("v1.2.3").unwrap();
288 assert_eq!(format!("{v}"), "v1.2.3");
289 }
290}