1use serde::{Deserialize, Serialize};
4use std::cmp::Ordering;
5use std::fmt;
6use std::str::FromStr;
7
8#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
10pub struct Version {
11 pub major: u32,
12 pub minor: u32,
13 pub patch: u32,
14 pub pre: Option<String>,
15}
16
17impl Version {
18 pub fn new(major: u32, minor: u32, patch: u32) -> Self {
20 Self {
21 major,
22 minor,
23 patch,
24 pre: None,
25 }
26 }
27
28 pub fn is_compatible_with(&self, other: &Version) -> bool {
30 self.major == other.major && self.minor == other.minor
31 }
32
33 pub fn parse_from_output(output: &str) -> Option<Self> {
35 let version_pattern = regex_lite::Regex::new(r"(\d+)\.(\d+)\.(\d+)(?:\.(\d+))?").ok()?;
37
38 if let Some(captures) = version_pattern.captures(output) {
39 let major: u32 = captures.get(1)?.as_str().parse().ok()?;
40 let minor: u32 = captures.get(2)?.as_str().parse().ok()?;
41 let patch: u32 = captures.get(3)?.as_str().parse().ok()?;
42 if let Some(fourth) = captures.get(4) {
44 let fourth_num: u32 = fourth.as_str().parse().ok()?;
45 return Some(Self::new(major, minor, patch * 100 + fourth_num));
47 }
48 return Some(Self::new(major, minor, patch));
49 }
50
51 None
52 }
53}
54
55impl fmt::Display for Version {
56 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57 write!(f, "{}.{}.{}", self.major, self.minor, self.patch)?;
58 if let Some(ref pre) = self.pre {
59 write!(f, "-{}", pre)?;
60 }
61 Ok(())
62 }
63}
64
65impl FromStr for Version {
66 type Err = VersionParseError;
67
68 fn from_str(s: &str) -> Result<Self, Self::Err> {
69 let s = s.trim();
70
71 let (version_part, pre) = if let Some(idx) = s.find('-') {
73 (&s[..idx], Some(s[idx + 1..].to_string()))
74 } else {
75 (s, None)
76 };
77
78 let parts: Vec<&str> = version_part.split('.').collect();
79 if parts.len() < 2 {
80 return Err(VersionParseError::InvalidFormat(s.to_string()));
81 }
82
83 let major = parts[0]
84 .parse()
85 .map_err(|_| VersionParseError::InvalidNumber(parts[0].to_string()))?;
86 let minor = parts[1]
87 .parse()
88 .map_err(|_| VersionParseError::InvalidNumber(parts[1].to_string()))?;
89 let patch = if parts.len() > 2 {
90 if parts.len() == 4 {
92 let p: u32 = parts[2]
93 .parse()
94 .map_err(|_| VersionParseError::InvalidNumber(parts[2].to_string()))?;
95 let q: u32 = parts[3]
96 .parse()
97 .map_err(|_| VersionParseError::InvalidNumber(parts[3].to_string()))?;
98 p * 100 + q
99 } else {
100 parts[2]
101 .parse()
102 .map_err(|_| VersionParseError::InvalidNumber(parts[2].to_string()))?
103 }
104 } else {
105 0
106 };
107
108 Ok(Version {
109 major,
110 minor,
111 patch,
112 pre,
113 })
114 }
115}
116
117impl PartialOrd for Version {
118 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
119 Some(self.cmp(other))
120 }
121}
122
123impl Ord for Version {
124 fn cmp(&self, other: &Self) -> Ordering {
125 match self.major.cmp(&other.major) {
126 Ordering::Equal => {}
127 ord => return ord,
128 }
129 match self.minor.cmp(&other.minor) {
130 Ordering::Equal => {}
131 ord => return ord,
132 }
133 match self.patch.cmp(&other.patch) {
134 Ordering::Equal => {}
135 ord => return ord,
136 }
137 match (&self.pre, &other.pre) {
139 (None, None) => Ordering::Equal,
140 (Some(_), None) => Ordering::Less,
141 (None, Some(_)) => Ordering::Greater,
142 (Some(a), Some(b)) => a.cmp(b),
143 }
144 }
145}
146
147#[derive(Debug, thiserror::Error)]
149pub enum VersionParseError {
150 #[error("invalid version format: {0}")]
151 InvalidFormat(String),
152 #[error("invalid version number: {0}")]
153 InvalidNumber(String),
154}
155
156#[cfg(test)]
157mod tests {
158 use super::*;
159
160 #[test]
161 fn test_parse_version() {
162 assert_eq!("9.8.2".parse::<Version>().unwrap(), Version::new(9, 8, 2));
163 assert_eq!("1.0.0".parse::<Version>().unwrap(), Version::new(1, 0, 0));
164 }
165
166 #[test]
167 fn test_parse_from_output() {
168 assert_eq!(
169 Version::parse_from_output(
170 "The Glorious Glasgow Haskell Compilation System, version 9.8.2"
171 ),
172 Some(Version::new(9, 8, 2))
173 );
174 assert_eq!(
175 Version::parse_from_output("cabal-install version 3.12.1.0"),
176 Some(Version::new(3, 12, 100))
177 );
178 }
179
180 #[test]
181 fn test_version_ordering() {
182 assert!(Version::new(9, 8, 2) > Version::new(9, 6, 4));
183 assert!(Version::new(9, 8, 2) > Version::new(9, 8, 1));
184 assert!(Version::new(10, 0, 0) > Version::new(9, 99, 99));
185 }
186}