use anyhow::{anyhow, Context, Result};
use serde::{Deserialize, Serialize};
use std::cmp::Ordering;
use std::fmt;
use std::str::FromStr;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Version {
pub major: u64,
pub minor: u64,
pub patch: u64,
pub pre_release: Option<String>,
pub build: Option<String>,
}
impl Version {
pub fn new(major: u64, minor: u64, patch: u64) -> Self {
Self {
major,
minor,
patch,
pre_release: None,
build: None,
}
}
pub fn is_compatible_with(&self, other: &Version) -> bool {
self.major == other.major && self.major > 0
}
pub fn satisfies(&self, req: &VersionReq) -> bool {
match req {
VersionReq::Exact(v) => self == v,
VersionReq::GreaterThan(v) => self > v,
VersionReq::GreaterOrEqual(v) => self >= v,
VersionReq::LessThan(v) => self < v,
VersionReq::LessOrEqual(v) => self <= v,
VersionReq::Compatible(v) => self.is_compatible_with(v) && self >= v,
VersionReq::Any => true,
}
}
pub fn is_prerelease(&self) -> bool {
self.pre_release.is_some()
}
pub fn is_stable(&self) -> bool {
self.major >= 1 && !self.is_prerelease()
}
}
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(pre) = &self.pre_release {
write!(f, "-{}", pre)?;
}
if let Some(build) = &self.build {
write!(f, "+{}", build)?;
}
Ok(())
}
}
impl FromStr for Version {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self> {
let s = s.strip_prefix('v').unwrap_or(s);
let (version_pre, build) = match s.split_once('+') {
Some((v, b)) => (v, Some(b.to_string())),
None => (s, None),
};
let (version, pre_release) = match version_pre.split_once('-') {
Some((v, p)) => (v, Some(p.to_string())),
None => (version_pre, None),
};
let parts: Vec<&str> = version.split('.').collect();
if parts.len() != 3 {
return Err(anyhow!("Invalid version format: {}", s));
}
let major = parts[0]
.parse()
.context("Failed to parse major version")?;
let minor = parts[1]
.parse()
.context("Failed to parse minor version")?;
let patch = parts[2]
.parse()
.context("Failed to parse patch version")?;
Ok(Self {
major,
minor,
patch,
pre_release,
build,
})
}
}
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_release, &other.pre_release) {
(None, None) => Ordering::Equal,
(Some(_), None) => Ordering::Less,
(None, Some(_)) => Ordering::Greater,
(Some(a), Some(b)) => a.cmp(b),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum VersionReq {
Exact(Version),
GreaterThan(Version),
GreaterOrEqual(Version),
LessThan(Version),
LessOrEqual(Version),
Compatible(Version),
Any,
}
impl fmt::Display for VersionReq {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
VersionReq::Exact(v) => write!(f, "={}", v),
VersionReq::GreaterThan(v) => write!(f, ">{}", v),
VersionReq::GreaterOrEqual(v) => write!(f, ">={}", v),
VersionReq::LessThan(v) => write!(f, "<{}", v),
VersionReq::LessOrEqual(v) => write!(f, "<={}", v),
VersionReq::Compatible(v) => write!(f, "^{}", v),
VersionReq::Any => write!(f, "*"),
}
}
}
impl FromStr for VersionReq {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self> {
let s = s.trim();
if s == "*" {
return Ok(VersionReq::Any);
}
if let Some(v) = s.strip_prefix(">=") {
return Ok(VersionReq::GreaterOrEqual(v.trim().parse()?));
}
if let Some(v) = s.strip_prefix('>') {
return Ok(VersionReq::GreaterThan(v.trim().parse()?));
}
if let Some(v) = s.strip_prefix("<=") {
return Ok(VersionReq::LessOrEqual(v.trim().parse()?));
}
if let Some(v) = s.strip_prefix('<') {
return Ok(VersionReq::LessThan(v.trim().parse()?));
}
if let Some(v) = s.strip_prefix('=') {
return Ok(VersionReq::Exact(v.trim().parse()?));
}
if let Some(v) = s.strip_prefix('^') {
return Ok(VersionReq::Compatible(v.trim().parse()?));
}
Ok(VersionReq::Compatible(s.parse()?))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_version_parsing() {
let v = "1.2.3".parse::<Version>().unwrap();
assert_eq!(v.major, 1);
assert_eq!(v.minor, 2);
assert_eq!(v.patch, 3);
let v = "v2.0.0-beta.1".parse::<Version>().unwrap();
assert_eq!(v.major, 2);
assert_eq!(v.pre_release, Some("beta.1".to_string()));
let v = "1.0.0+build.123".parse::<Version>().unwrap();
assert_eq!(v.build, Some("build.123".to_string()));
}
#[test]
fn test_version_comparison() {
let v1 = Version::new(1, 2, 3);
let v2 = Version::new(1, 2, 4);
let v3 = Version::new(2, 0, 0);
assert!(v1 < v2);
assert!(v2 < v3);
assert!(v1 < v3);
}
#[test]
fn test_version_compatibility() {
let v1 = Version::new(1, 2, 3);
let v2 = Version::new(1, 5, 0);
let v3 = Version::new(2, 0, 0);
assert!(v1.is_compatible_with(&v2));
assert!(!v1.is_compatible_with(&v3));
}
#[test]
fn test_version_requirements() {
let v = Version::new(1, 2, 3);
let req = "^1.0.0".parse::<VersionReq>().unwrap();
assert!(v.satisfies(&req));
let req = ">=1.2.0".parse::<VersionReq>().unwrap();
assert!(v.satisfies(&req));
let req = ">2.0.0".parse::<VersionReq>().unwrap();
assert!(!v.satisfies(&req));
}
}