1use std::fmt;
2use std::str::FromStr;
3use std::{cmp::Ordering, sync::LazyLock};
4
5use regex::Regex;
6
7use super::{Version, VersionError};
8
9#[derive(Copy, Clone, Debug)]
18pub enum PartialVersion {
19 Pre10m(u32, u32),
23 Pre10mm(u32, u32, u32),
27 Post10m(u32),
31 Post10mm(u32, u32),
35}
36
37impl From<&PartialVersion> for Version {
44 fn from(partial: &PartialVersion) -> Self {
45 use PartialVersion::*;
46 match *partial {
47 Pre10m(a, b) => Version::Pre10(a, b, 0),
48 Pre10mm(a, b, c) => Version::Pre10(a, b, c),
49 Post10m(a) => Version::Post10(a, 0),
50 Post10mm(a, b) => Version::Post10(a, b),
51 }
52 }
53}
54
55impl From<PartialVersion> for Version {
57 fn from(partial: PartialVersion) -> Self {
58 (&partial).into()
59 }
60}
61
62impl From<&Version> for PartialVersion {
64 fn from(version: &Version) -> Self {
65 use Version::*;
66 match *version {
67 Pre10(a, b, c) => PartialVersion::Pre10mm(a, b, c),
68 Post10(a, b) => PartialVersion::Post10mm(a, b),
69 }
70 }
71}
72
73impl From<Version> for PartialVersion {
75 fn from(version: Version) -> Self {
76 (&version).into()
77 }
78}
79
80impl PartialVersion {
81 pub fn checked(self) -> Result<Self, VersionError> {
89 use PartialVersion::*;
90 match self {
91 Pre10m(a, ..) | Pre10mm(a, ..) if a < 10 => Ok(self),
92 Post10m(a) | Post10mm(a, ..) if a >= 10 => Ok(self),
93 _ => Err(VersionError::BadlyFormed { text: Some(self.to_string()) }),
94 }
95 }
96
97 #[allow(dead_code)]
119 pub fn compatible(&self, version: Version) -> bool {
120 use PartialVersion::*;
121 match (*self, version) {
122 (Pre10m(a, b), Version::Pre10(x, y, _)) => a == x && b == y,
123 (Pre10mm(a, b, c), Version::Pre10(x, y, z)) => a == x && b == y && c <= z,
124 (Post10m(a), Version::Post10(x, _)) => a == x,
125 (Post10mm(a, b), Version::Post10(x, y)) => a == x && b <= y,
126 _ => false,
127 }
128 }
129
130 #[must_use]
132 pub fn widened(&self) -> PartialVersion {
133 use PartialVersion::*;
134 match self {
135 Pre10mm(a, b, _) => Pre10m(*a, *b),
136 Post10mm(a, _) => Post10m(*a),
137 _ => *self,
138 }
139 }
140
141 #[allow(dead_code)]
148 pub fn sort_key(&self) -> (u32, Option<u32>, Option<u32>) {
149 use PartialVersion::*;
150 match *self {
151 Pre10m(a, b) => (a, Some(b), None),
152 Pre10mm(a, b, c) => (a, Some(b), Some(c)),
153 Post10m(a) => (a, None, None),
154 Post10mm(a, b) => (a, Some(b), None),
155 }
156 }
157}
158
159impl PartialEq for PartialVersion {
160 fn eq(&self, other: &Self) -> bool {
161 self.partial_cmp(other) == Some(Ordering::Equal)
162 }
163}
164
165impl PartialOrd for PartialVersion {
166 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
167 use PartialVersion::*;
168 match (*self, *other) {
169 (Pre10m(a, b), Pre10m(x, y)) => Some((a, b).cmp(&(x, y))),
170 (Pre10m(a, b), Pre10mm(x, y, _)) => Some((a, b).cmp(&(x, y))),
171 (Pre10mm(a, b, _), Pre10m(x, y)) => Some((a, b).cmp(&(x, y))),
172 (Pre10mm(a, b, c), Pre10mm(x, y, z)) => Some((a, b, c).cmp(&(x, y, z))),
173
174 (Post10m(a), Post10m(x)) => Some(a.cmp(&x)),
175 (Post10m(a), Post10mm(x, _)) => Some(a.cmp(&x)),
176 (Post10mm(a, _), Post10m(x)) => Some(a.cmp(&x)),
177 (Post10mm(a, b), Post10mm(x, y)) => Some((a, b).cmp(&(x, y))),
178
179 (Pre10m(..), Post10m(..)) => Some(Ordering::Less),
180 (Pre10m(..), Post10mm(..)) => Some(Ordering::Less),
181 (Pre10mm(..), Post10m(..)) => Some(Ordering::Less),
182 (Pre10mm(..), Post10mm(..)) => Some(Ordering::Less),
183
184 (Post10m(..), Pre10m(..)) => Some(Ordering::Greater),
185 (Post10m(..), Pre10mm(..)) => Some(Ordering::Greater),
186 (Post10mm(..), Pre10m(..)) => Some(Ordering::Greater),
187 (Post10mm(..), Pre10mm(..)) => Some(Ordering::Greater),
188 }
189 }
190}
191
192impl fmt::Display for PartialVersion {
193 fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
194 match *self {
195 Self::Pre10m(a, b) => fmt.pad(&format!("{a}.{b}")),
196 Self::Pre10mm(a, b, c) => fmt.pad(&format!("{a}.{b}.{c}")),
197 Self::Post10m(a) => fmt.pad(&format!("{a}")),
198 Self::Post10mm(a, b) => fmt.pad(&format!("{a}.{b}")),
199 }
200 }
201}
202
203impl FromStr for PartialVersion {
204 type Err = VersionError;
205
206 fn from_str(s: &str) -> Result<Self, Self::Err> {
207 static RE: LazyLock<Regex> = LazyLock::new(|| {
208 Regex::new(r"(?x) \b (\d+) (?: [.] (\d+) (?: [.] (\d+) )? )? \b")
209 .expect("invalid regex (for matching partial PostgreSQL versions)")
210 });
211 match RE.captures(s) {
212 Some(caps) => match (
213 caps.get(1).and_then(|n| n.as_str().parse::<u32>().ok()),
214 caps.get(2).and_then(|n| n.as_str().parse::<u32>().ok()),
215 caps.get(3).and_then(|n| n.as_str().parse::<u32>().ok()),
216 ) {
217 (Some(a), Some(b), None) if a < 10 => Ok(Self::Pre10m(a, b)),
218 (Some(a), Some(b), Some(c)) if a < 10 => Ok(Self::Pre10mm(a, b, c)),
219 (Some(a), None, None) if a >= 10 => Ok(Self::Post10m(a)),
220 (Some(a), Some(b), None) if a >= 10 => Ok(Self::Post10mm(a, b)),
221 _ => Err(VersionError::BadlyFormed { text: Some(s.into()) }),
222 },
223 None => Err(VersionError::NotFound { text: Some(s.into()) }),
224 }
225 }
226}
227
228#[cfg(test)]
229mod tests {
230 use super::super::{Version, VersionError::*};
231 use super::{PartialVersion, PartialVersion::*};
232
233 use rand::rng;
234 use rand::seq::SliceRandom;
235
236 #[test]
237 fn parses_version_below_10() {
238 assert_eq!(Ok(Pre10mm(9, 6, 17)), "9.6.17".parse());
239 assert_eq!(Ok(Pre10m(9, 6)), "9.6".parse());
240 }
241
242 #[test]
243 fn parses_version_above_10() {
244 assert_eq!(Ok(Post10mm(12, 2)), "12.2".parse());
245 assert_eq!(Ok(Post10m(12)), "12".parse());
246 }
247
248 #[test]
249 fn parse_returns_error_when_version_is_invalid() {
250 assert!(matches!(
252 "4294967296.0".parse::<PartialVersion>(),
253 Err(BadlyFormed { .. })
254 ));
255 assert!(matches!(
257 "9".parse::<PartialVersion>(),
258 Err(BadlyFormed { .. })
259 ));
260 assert!(matches!(
262 "10.10.10".parse::<PartialVersion>(),
263 Err(BadlyFormed { .. })
264 ));
265 }
266
267 #[test]
268 fn parse_returns_error_when_version_not_found() {
269 assert!(matches!(
270 "foo".parse::<PartialVersion>(),
271 Err(NotFound { .. })
272 ));
273 }
274
275 #[test]
276 fn checked_returns_self_when_variant_is_valid() {
277 use PartialVersion::*;
278 assert_eq!(Ok(Pre10m(9, 0)), Pre10m(9, 0).checked());
279 assert_eq!(Ok(Pre10mm(9, 0, 0)), Pre10mm(9, 0, 0).checked());
280 assert_eq!(Ok(Post10m(10)), Post10m(10).checked());
281 assert_eq!(Ok(Post10mm(10, 0)), Post10mm(10, 0).checked());
282 }
283
284 #[test]
285 fn checked_returns_error_when_variant_is_invalid() {
286 use PartialVersion::*;
287 assert!(matches!(Pre10m(10, 0).checked(), Err(BadlyFormed { .. })));
288 assert!(matches!(
289 Pre10mm(10, 0, 0).checked(),
290 Err(BadlyFormed { .. })
291 ));
292 assert!(matches!(Post10m(9).checked(), Err(BadlyFormed { .. })));
293 assert!(matches!(Post10mm(9, 0).checked(), Err(BadlyFormed { .. })));
294 }
295
296 #[test]
297 fn displays_version_below_10() {
298 assert_eq!("9.6.17", format!("{}", Pre10mm(9, 6, 17)));
299 assert_eq!("9.6", format!("{}", Pre10m(9, 6)));
300 }
301
302 #[test]
303 fn displays_version_above_10() {
304 assert_eq!("12.2", format!("{}", Post10mm(12, 2)));
305 assert_eq!("12", format!("{}", Post10m(12)));
306 }
307
308 #[test]
309 fn converts_partial_version_to_version() {
310 assert_eq!(Version::Pre10(9, 1, 2), Pre10mm(9, 1, 2).into());
311 assert_eq!(Version::Pre10(9, 1, 0), Pre10m(9, 1).into());
312 assert_eq!(Version::Post10(14, 2), Post10mm(14, 2).into());
313 assert_eq!(Version::Post10(14, 0), Post10m(14).into());
314 }
315
316 #[test]
317 fn compatible_below_10() {
318 let version = "9.6.16".parse().unwrap();
319 assert!(Pre10mm(9, 6, 16).compatible(version));
320 assert!(Pre10m(9, 6).compatible(version));
321 }
322
323 #[test]
324 fn not_compatible_below_10() {
325 let version = "9.6.16".parse().unwrap();
326 assert!(!Pre10mm(9, 6, 17).compatible(version));
327 assert!(!Pre10m(9, 7).compatible(version));
328 assert!(!Pre10mm(8, 6, 16).compatible(version));
329 assert!(!Pre10m(8, 6).compatible(version));
330 }
331
332 #[test]
333 fn compatible_above_10() {
334 let version = "12.6".parse().unwrap();
335 assert!(Post10mm(12, 6).compatible(version));
336 assert!(Post10m(12).compatible(version));
337 }
338
339 #[test]
340 fn not_compatible_above_10() {
341 let version = "12.6".parse().unwrap();
342 assert!(!Post10mm(12, 7).compatible(version));
343 assert!(!Post10m(13).compatible(version));
344 assert!(!Post10mm(11, 6).compatible(version));
345 assert!(!Post10m(11).compatible(version));
346 }
347
348 #[test]
349 fn not_compatible_below_10_with_above_10() {
350 let version = "12.6".parse().unwrap();
351 assert!(!Pre10m(9, 1).compatible(version));
352 assert!(!Pre10mm(9, 1, 2).compatible(version));
353 let version = "9.1.2".parse().unwrap();
354 assert!(!Post10m(12).compatible(version));
355 assert!(!Post10mm(12, 6).compatible(version));
356 }
357
358 #[test]
359 fn widened_removes_minor_or_patch_number() {
360 assert_eq!(Pre10mm(9, 1, 2), Pre10m(9, 1));
361 assert_eq!(Post10mm(12, 9), Post10m(12));
362 assert_eq!(Pre10m(9, 1), Pre10m(9, 1));
363 assert_eq!(Post10m(12), Post10m(12));
364 }
365
366 #[test]
367 fn partial_ord_works_as_expected() {
368 let mut versions = vec![
369 Pre10mm(9, 10, 11),
370 Pre10mm(9, 10, 12),
371 Pre10m(8, 11),
372 Pre10m(9, 11),
373 Pre10m(9, 12),
374 Post10mm(10, 11),
375 Post10m(11),
376 ];
377 let mut rng = rng();
378 for _ in 0..1000 {
379 versions.shuffle(&mut rng);
380 versions.sort_by(|a, b| a.partial_cmp(b).unwrap());
381 assert_eq!(
382 versions,
383 vec![
384 Pre10m(8, 11),
385 Pre10mm(9, 10, 11),
386 Pre10mm(9, 10, 12),
387 Pre10m(9, 11),
388 Pre10m(9, 12),
389 Post10mm(10, 11),
390 Post10m(11),
391 ]
392 );
393 }
394 }
395
396 #[test]
397 fn sort_key_works_as_expected() {
398 let mut versions = vec![
399 Pre10mm(9, 0, 0),
400 Pre10mm(9, 10, 11),
401 Pre10mm(9, 10, 12),
402 Pre10m(9, 0),
403 Pre10m(8, 11),
404 Pre10m(9, 11),
405 Pre10m(9, 12),
406 Post10mm(10, 11),
407 Post10m(11),
408 ];
409 let mut rng = rng();
410 for _ in 0..1000 {
411 versions.shuffle(&mut rng);
412 versions.sort_by_key(PartialVersion::sort_key);
413 assert_eq!(
414 versions,
415 vec![
416 Pre10m(8, 11),
417 Pre10m(9, 0),
418 Pre10mm(9, 0, 0),
419 Pre10mm(9, 10, 11),
420 Pre10mm(9, 10, 12),
421 Pre10m(9, 11),
422 Pre10m(9, 12),
423 Post10mm(10, 11),
424 Post10m(11),
425 ]
426 );
427 }
428 }
429}