1use std::num::ParseIntError;
2use std::str::FromStr;
3
4use serde::{Deserialize, Deserializer, de::Error};
5
6#[derive(Debug, PartialEq, PartialOrd, Clone, Copy)]
7pub struct Version {
8 major: i32,
9 minor: Option<i32>,
10 patch: Option<i32>,
11}
12
13impl Version {
14 #[must_use]
15 pub(crate) fn new(
16 major: i32,
17 minor: impl Into<Option<i32>>,
18 patch: impl Into<Option<i32>>,
19 ) -> Self {
20 Self {
21 major,
22 minor: minor.into(),
23 patch: patch.into(),
24 }
25 }
26}
27
28impl Default for Version {
29 fn default() -> Version {
30 Version::new(15, 0, 0)
31 }
32}
33
34impl<'de> Deserialize<'de> for Version {
37 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
38 where
39 D: Deserializer<'de>,
40 {
41 let s: &str = Deserialize::deserialize(deserializer)?;
42 Version::from_str(s).map_err(D::Error::custom)
43 }
44}
45
46#[derive(Debug, PartialEq)]
47pub struct InvalidNumber {
48 pub version: String,
49 pub e: ParseIntError,
50}
51
52#[derive(Debug, PartialEq)]
53pub struct EmptyVersion {
54 pub version: String,
55}
56
57#[derive(Debug, PartialEq)]
58pub enum ParseVersionError {
59 EmptyVersion(EmptyVersion),
60 InvalidNumber(InvalidNumber),
61}
62
63fn parse_int(s: &str) -> Result<i32, ParseVersionError> {
64 Ok(s.parse().map_err(|e| {
65 ParseVersionError::InvalidNumber(InvalidNumber {
66 version: s.to_string(),
67 e,
68 })
69 }))?
70}
71
72impl std::fmt::Display for ParseVersionError {
73 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
74 match *self {
75 Self::EmptyVersion(ref err) => {
76 write!(f, "Empty version number provided: {:?}", err.version)
77 }
78 Self::InvalidNumber(ref err) => {
79 write!(
80 f,
81 "Invalid number in version: {:?}. Parse error: {}",
82 err.version, err.e
83 )
84 }
85 }
86 }
87}
88
89impl std::error::Error for ParseVersionError {}
90
91impl FromStr for Version {
92 type Err = ParseVersionError;
93
94 fn from_str(s: &str) -> Result<Self, Self::Err> {
95 let version_pieces: Vec<&str> = s.split('.').collect();
96
97 if version_pieces.is_empty() {
98 return Err(ParseVersionError::EmptyVersion(EmptyVersion {
99 version: s.to_string(),
100 }));
101 }
102 let major = parse_int(version_pieces[0])?;
103
104 let minor: Option<i32> = if version_pieces.len() > 1 {
105 Some(parse_int(version_pieces[1])?)
106 } else {
107 None
108 };
109 let patch: Option<i32> = if version_pieces.len() > 2 {
110 Some(parse_int(version_pieces[2])?)
111 } else {
112 None
113 };
114
115 Ok(Version {
116 major,
117 minor,
118 patch,
119 })
120 }
121}
122
123#[cfg(test)]
124mod test_pg_version {
125 #![allow(clippy::neg_cmp_op_on_partial_ord)]
126 use insta::assert_debug_snapshot;
127
128 use super::*;
129 #[test]
130 fn eq() {
131 assert_eq!(Version::new(10, None, None), Version::new(10, None, None));
132 }
133 #[test]
134 fn gt() {
135 assert!(Version::new(10, Some(1), None) > Version::new(10, None, None));
136 assert!(Version::new(10, None, Some(1)) > Version::new(10, None, None));
137 assert!(Version::new(10, None, Some(1)) > Version::new(9, None, None));
138
139 assert!(!(Version::new(10, None, None) > Version::new(10, None, None)));
140 }
141 #[test]
142 fn parse() {
143 assert_eq!(
144 Version::from_str("10.1"),
145 Ok(Version::new(10, Some(1), None))
146 );
147 assert_eq!(Version::from_str("10"), Ok(Version::new(10, None, None)));
148 assert_eq!(
149 Version::from_str("10.2.1"),
150 Ok(Version::new(10, Some(2), Some(1)))
151 );
152 assert_debug_snapshot!(Version::from_str("test").unwrap_err());
153 }
154}