use serde::{Deserialize, Serialize};
use std::cmp::Ordering;
use std::fmt;
use std::str::FromStr;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Version {
pub major: u32,
pub minor: u32,
pub patch: u32,
pub pre: Option<String>,
}
impl Version {
pub fn new(major: u32, minor: u32, patch: u32) -> Self {
Self {
major,
minor,
patch,
pre: None,
}
}
pub fn is_compatible_with(&self, other: &Version) -> bool {
self.major == other.major && self.minor == other.minor
}
pub fn parse_from_output(output: &str) -> Option<Self> {
let version_pattern = regex_lite::Regex::new(r"(\d+)\.(\d+)\.(\d+)(?:\.(\d+))?").ok()?;
if let Some(captures) = version_pattern.captures(output) {
let major: u32 = captures.get(1)?.as_str().parse().ok()?;
let minor: u32 = captures.get(2)?.as_str().parse().ok()?;
let patch: u32 = captures.get(3)?.as_str().parse().ok()?;
if let Some(fourth) = captures.get(4) {
let fourth_num: u32 = fourth.as_str().parse().ok()?;
return Some(Self::new(major, minor, patch * 100 + fourth_num));
}
return Some(Self::new(major, minor, patch));
}
None
}
}
impl fmt::Display for Version {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}.{}.{}", self.major, self.minor, self.patch)?;
if let Some(ref pre) = self.pre {
write!(f, "-{}", pre)?;
}
Ok(())
}
}
impl FromStr for Version {
type Err = VersionParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let s = s.trim();
let (version_part, pre) = if let Some(idx) = s.find('-') {
(&s[..idx], Some(s[idx + 1..].to_string()))
} else {
(s, None)
};
let parts: Vec<&str> = version_part.split('.').collect();
if parts.len() < 2 {
return Err(VersionParseError::InvalidFormat(s.to_string()));
}
let major = parts[0]
.parse()
.map_err(|_| VersionParseError::InvalidNumber(parts[0].to_string()))?;
let minor = parts[1]
.parse()
.map_err(|_| VersionParseError::InvalidNumber(parts[1].to_string()))?;
let patch = if parts.len() > 2 {
if parts.len() == 4 {
let p: u32 = parts[2]
.parse()
.map_err(|_| VersionParseError::InvalidNumber(parts[2].to_string()))?;
let q: u32 = parts[3]
.parse()
.map_err(|_| VersionParseError::InvalidNumber(parts[3].to_string()))?;
p * 100 + q
} else {
parts[2]
.parse()
.map_err(|_| VersionParseError::InvalidNumber(parts[2].to_string()))?
}
} else {
0
};
Ok(Version {
major,
minor,
patch,
pre,
})
}
}
impl PartialOrd for Version {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Version {
fn cmp(&self, other: &Self) -> Ordering {
match self.major.cmp(&other.major) {
Ordering::Equal => {}
ord => return ord,
}
match self.minor.cmp(&other.minor) {
Ordering::Equal => {}
ord => return ord,
}
match self.patch.cmp(&other.patch) {
Ordering::Equal => {}
ord => return ord,
}
match (&self.pre, &other.pre) {
(None, None) => Ordering::Equal,
(Some(_), None) => Ordering::Less,
(None, Some(_)) => Ordering::Greater,
(Some(a), Some(b)) => a.cmp(b),
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum VersionParseError {
#[error("invalid version format: {0}")]
InvalidFormat(String),
#[error("invalid version number: {0}")]
InvalidNumber(String),
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_version() {
assert_eq!("9.8.2".parse::<Version>().unwrap(), Version::new(9, 8, 2));
assert_eq!("1.0.0".parse::<Version>().unwrap(), Version::new(1, 0, 0));
}
#[test]
fn test_parse_from_output() {
assert_eq!(
Version::parse_from_output(
"The Glorious Glasgow Haskell Compilation System, version 9.8.2"
),
Some(Version::new(9, 8, 2))
);
assert_eq!(
Version::parse_from_output("cabal-install version 3.12.1.0"),
Some(Version::new(3, 12, 100))
);
}
#[test]
fn test_version_ordering() {
assert!(Version::new(9, 8, 2) > Version::new(9, 6, 4));
assert!(Version::new(9, 8, 2) > Version::new(9, 8, 1));
assert!(Version::new(10, 0, 0) > Version::new(9, 99, 99));
}
}