use serde::{Deserialize, Serialize};
use std::cmp::Ordering;
use std::fmt;
use std::str::FromStr;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum VersionError {
#[error("Invalid version string: {0}")]
InvalidVersion(String),
#[error("Invalid version specifier: {0}")]
InvalidSpecifier(String),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Version {
pub major: u64,
pub minor: u64,
pub patch: u64,
pub pre_release: Option<String>,
pub local: Option<String>,
pub original: String,
}
impl PartialEq for Version {
fn eq(&self, other: &Self) -> bool {
self.major == other.major
&& self.minor == other.minor
&& self.patch == other.patch
&& self.pre_release == other.pre_release
}
}
impl Eq for Version {}
impl Version {
pub fn new(major: u64, minor: u64, patch: u64) -> Self {
Self {
major,
minor,
patch,
pre_release: None,
local: None,
original: format!("{major}.{minor}.{patch}"),
}
}
pub fn is_prerelease(&self) -> bool {
self.pre_release.is_some()
}
pub fn same_major(&self, other: &Version) -> bool {
self.major == other.major
}
pub fn same_minor(&self, other: &Version) -> bool {
self.major == other.major && self.minor == other.minor
}
}
impl FromStr for Version {
type Err = VersionError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let s = s.trim();
let (version_part, local) = if let Some(idx) = s.find('+') {
(&s[..idx], Some(s[idx + 1..].to_string()))
} else {
(s, None)
};
let (base_part, pre_release) = parse_prerelease(version_part);
let parts: Vec<&str> = base_part.split('.').collect();
let major = parts
.first()
.and_then(|s| s.parse().ok())
.ok_or_else(|| VersionError::InvalidVersion(s.to_string()))?;
let minor = parts.get(1).and_then(|s| s.parse().ok()).unwrap_or(0);
let patch = parts.get(2).and_then(|s| s.parse().ok()).unwrap_or(0);
Ok(Version {
major,
minor,
patch,
pre_release,
local,
original: s.to_string(),
})
}
}
fn parse_prerelease(s: &str) -> (&str, Option<String>) {
let patterns = [
"dev", "post", "alpha", "beta", "rc", "a", "b", "c", "-",
];
for pattern in patterns {
if let Some(idx) = s.to_lowercase().find(pattern)
&& idx > 0 {
return (&s[..idx], Some(s[idx..].to_string()));
}
}
(s, None)
}
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, Some(_)) => Ordering::Greater,
(Some(_), None) => Ordering::Less,
(Some(a), Some(b)) => a.cmp(b),
(None, None) => Ordering::Equal,
}
}
}
impl PartialOrd for Version {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl fmt::Display for Version {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.original)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum VersionSpec {
Pinned(Version),
Minimum(Version),
Maximum(Version),
GreaterThan(Version),
LessThan(Version),
Range { min: Version, max: Version },
Caret(Version),
Tilde(Version),
Compatible(Version),
Wildcard { prefix: String, pattern: String },
NotEqual(Version),
Complex(String),
Any,
}
impl VersionSpec {
pub fn parse(s: &str) -> Result<Self, VersionError> {
let s = s.trim();
if s.is_empty() || s == "*" {
return Ok(VersionSpec::Any);
}
if let Some(version_str) = s.strip_prefix('^') {
let version = Version::from_str(version_str)?;
return Ok(VersionSpec::Caret(version));
}
if let Some(version_str) = s.strip_prefix("~=") {
let version = Version::from_str(version_str)?;
return Ok(VersionSpec::Compatible(version));
}
if let Some(version_str) = s.strip_prefix('~') {
let version = Version::from_str(version_str)?;
return Ok(VersionSpec::Tilde(version));
}
if s.contains('*') {
if let Some(prefix) = s.strip_prefix("==") {
return Ok(VersionSpec::Wildcard {
prefix: prefix.replace(".*", "").replace("*", ""),
pattern: s.to_string(),
});
}
return Ok(VersionSpec::Wildcard {
prefix: s.replace(".*", "").replace("*", ""),
pattern: s.to_string(),
});
}
if s.contains(',') {
let parts: Vec<&str> = s.split(',').collect();
if parts.len() == 2 {
let min_part = parts[0].trim();
let max_part = parts[1].trim();
if let (Some(min_str), Some(max_str)) = (
min_part.strip_prefix(">="),
max_part.strip_prefix('<'),
) {
let min = Version::from_str(min_str)?;
let max = Version::from_str(max_str)?;
return Ok(VersionSpec::Range { min, max });
}
}
return Ok(VersionSpec::Complex(s.to_string()));
}
if let Some(version_str) = s.strip_prefix("==") {
let version = Version::from_str(version_str)?;
return Ok(VersionSpec::Pinned(version));
}
if let Some(version_str) = s.strip_prefix(">=") {
let version = Version::from_str(version_str)?;
return Ok(VersionSpec::Minimum(version));
}
if let Some(version_str) = s.strip_prefix("<=") {
let version = Version::from_str(version_str)?;
return Ok(VersionSpec::Maximum(version));
}
if let Some(version_str) = s.strip_prefix("!=") {
let version = Version::from_str(version_str)?;
return Ok(VersionSpec::NotEqual(version));
}
if let Some(version_str) = s.strip_prefix('>') {
let version = Version::from_str(version_str)?;
return Ok(VersionSpec::GreaterThan(version));
}
if let Some(version_str) = s.strip_prefix('<') {
let version = Version::from_str(version_str)?;
return Ok(VersionSpec::LessThan(version));
}
if let Ok(version) = Version::from_str(s) {
return Ok(VersionSpec::Pinned(version));
}
Ok(VersionSpec::Complex(s.to_string()))
}
pub fn satisfies(&self, version: &Version) -> bool {
match self {
VersionSpec::Any => true,
VersionSpec::Pinned(v) => version == v,
VersionSpec::Minimum(v) => version >= v,
VersionSpec::Maximum(v) => version <= v,
VersionSpec::GreaterThan(v) => version > v,
VersionSpec::LessThan(v) => version < v,
VersionSpec::Range { min, max } => version >= min && version < max,
VersionSpec::Caret(v) => {
if version < v {
return false;
}
if v.major == 0 {
if v.minor == 0 {
version.major == 0 && version.minor == 0 && version.patch == v.patch
} else {
version.major == 0 && version.minor == v.minor
}
} else {
version.major == v.major
}
}
VersionSpec::Tilde(v) => {
version >= v && version.major == v.major && version.minor == v.minor
}
VersionSpec::Compatible(v) => {
let dot_count = v.original.chars().filter(|c| *c == '.').count();
if dot_count < 2 {
version >= v && version.major == v.major
} else {
version >= v && version.major == v.major && version.minor == v.minor
}
}
VersionSpec::Wildcard { prefix, .. } => {
version.original.starts_with(&format!("{prefix}."))
|| version.original == *prefix
}
VersionSpec::NotEqual(v) => version != v,
VersionSpec::Complex(_) => false, }
}
pub fn base_version(&self) -> Option<&Version> {
match self {
VersionSpec::Pinned(v)
| VersionSpec::Minimum(v)
| VersionSpec::Maximum(v)
| VersionSpec::GreaterThan(v)
| VersionSpec::LessThan(v)
| VersionSpec::Caret(v)
| VersionSpec::Tilde(v)
| VersionSpec::Compatible(v)
| VersionSpec::NotEqual(v) => Some(v),
VersionSpec::Range { min, .. } => Some(min),
VersionSpec::Wildcard { .. } | VersionSpec::Complex(_) | VersionSpec::Any => None,
}
}
pub fn max_major(&self) -> Option<u64> {
match self {
VersionSpec::Range { max, .. } => Some(max.major),
VersionSpec::Caret(v) => Some(v.major),
VersionSpec::LessThan(v) | VersionSpec::Maximum(v) => Some(v.major),
VersionSpec::Minimum(v)
| VersionSpec::GreaterThan(v)
| VersionSpec::Pinned(v)
| VersionSpec::Compatible(v)
| VersionSpec::Tilde(v) => Some(v.major),
VersionSpec::NotEqual(v) => Some(v.major),
VersionSpec::Wildcard { prefix, .. } => {
prefix.split('.').next().and_then(|s| s.parse().ok())
}
VersionSpec::Complex(_) | VersionSpec::Any => None,
}
}
pub fn version_string(&self) -> Option<String> {
match self {
VersionSpec::Pinned(v)
| VersionSpec::Minimum(v)
| VersionSpec::Maximum(v)
| VersionSpec::GreaterThan(v)
| VersionSpec::LessThan(v)
| VersionSpec::Caret(v)
| VersionSpec::Tilde(v)
| VersionSpec::Compatible(v)
| VersionSpec::NotEqual(v) => Some(v.to_string()),
VersionSpec::Range { min, .. } => Some(min.to_string()),
VersionSpec::Wildcard { prefix, .. } => Some(format!("{prefix}.*")),
VersionSpec::Complex(s) => Some(s.clone()),
VersionSpec::Any => None,
}
}
pub fn to_cargo_string(&self) -> Option<String> {
match self {
VersionSpec::Caret(v) => Some(v.to_string()), VersionSpec::Tilde(v) => Some(format!("~{v}")),
VersionSpec::Pinned(v) => Some(format!("={v}")), VersionSpec::Minimum(v) => Some(format!(">={v}")),
VersionSpec::Maximum(v) => Some(format!("<={v}")),
VersionSpec::GreaterThan(v) => Some(format!(">{v}")),
VersionSpec::LessThan(v) => Some(format!("<{v}")),
VersionSpec::Range { min, max } => Some(format!(">={min}, <{max}")),
VersionSpec::Wildcard { prefix, .. } => Some(format!("{prefix}.*")),
VersionSpec::NotEqual(v) => Some(format!("!={v}")),
VersionSpec::Compatible(v) => Some(v.to_string()), VersionSpec::Complex(s) => Some(s.clone()),
VersionSpec::Any => Some("*".to_string()),
}
}
pub fn is_rewritable(&self) -> bool {
!matches!(self, VersionSpec::Complex(_) | VersionSpec::Any)
}
pub fn with_version(&self, new_version: &Version) -> VersionSpec {
match self {
VersionSpec::Pinned(_) => VersionSpec::Pinned(new_version.clone()),
VersionSpec::Minimum(_) => VersionSpec::Minimum(new_version.clone()),
VersionSpec::Maximum(_) => VersionSpec::Maximum(new_version.clone()),
VersionSpec::GreaterThan(_) => VersionSpec::GreaterThan(new_version.clone()),
VersionSpec::LessThan(_) => VersionSpec::LessThan(new_version.clone()),
VersionSpec::Range { max, .. } => {
if new_version >= max {
VersionSpec::Range {
min: new_version.clone(),
max: Version::new(new_version.major + 1, 0, 0),
}
} else {
VersionSpec::Range {
min: new_version.clone(),
max: max.clone(),
}
}
}
VersionSpec::Caret(_) => VersionSpec::Caret(new_version.clone()),
VersionSpec::Tilde(_) => VersionSpec::Tilde(new_version.clone()),
VersionSpec::Compatible(_) => VersionSpec::Compatible(new_version.clone()),
VersionSpec::Wildcard { prefix, pattern } => {
let segments = prefix.split('.').count();
let new_prefix = match segments {
0 | 1 => format!("{}", new_version.major),
_ => format!("{}.{}", new_version.major, new_version.minor),
};
VersionSpec::Wildcard {
prefix: new_prefix,
pattern: pattern.clone(),
}
}
VersionSpec::NotEqual(_) => VersionSpec::NotEqual(new_version.clone()),
VersionSpec::Complex(s) => VersionSpec::Complex(s.clone()),
VersionSpec::Any => VersionSpec::Any,
}
}
}
impl fmt::Display for VersionSpec {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
VersionSpec::Any => write!(f, "*"),
VersionSpec::Pinned(v) => write!(f, "=={v}"),
VersionSpec::Minimum(v) => write!(f, ">={v}"),
VersionSpec::Maximum(v) => write!(f, "<={v}"),
VersionSpec::GreaterThan(v) => write!(f, ">{v}"),
VersionSpec::LessThan(v) => write!(f, "<{v}"),
VersionSpec::Range { min, max } => write!(f, ">={min},<{max}"),
VersionSpec::Caret(v) => write!(f, "^{v}"),
VersionSpec::Tilde(v) => write!(f, "~{v}"),
VersionSpec::Compatible(v) => write!(f, "~={v}"),
VersionSpec::Wildcard { prefix, .. } => write!(f, "=={prefix}.*"),
VersionSpec::NotEqual(v) => write!(f, "!={v}"),
VersionSpec::Complex(s) => write!(f, "{s}"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_version() {
let v = Version::from_str("1.2.3").unwrap();
assert_eq!(v.major, 1);
assert_eq!(v.minor, 2);
assert_eq!(v.patch, 3);
let v = Version::from_str("2.0").unwrap();
assert_eq!(v.major, 2);
assert_eq!(v.minor, 0);
assert_eq!(v.patch, 0);
}
#[test]
fn test_version_comparison() {
let v1 = Version::from_str("1.2.3").unwrap();
let v2 = Version::from_str("1.2.4").unwrap();
let v3 = Version::from_str("2.0.0").unwrap();
assert!(v1 < v2);
assert!(v2 < v3);
assert!(v1 < v3);
}
#[test]
fn test_parse_version_spec() {
assert!(matches!(
VersionSpec::parse("==1.2.3").unwrap(),
VersionSpec::Pinned(_)
));
assert!(matches!(
VersionSpec::parse(">=1.2.3").unwrap(),
VersionSpec::Minimum(_)
));
assert!(matches!(
VersionSpec::parse("^1.2.3").unwrap(),
VersionSpec::Caret(_)
));
assert!(matches!(
VersionSpec::parse(">=1.0.0,<2.0.0").unwrap(),
VersionSpec::Range { .. }
));
}
#[test]
fn test_satisfies() {
let spec = VersionSpec::parse(">=1.0.0,<2.0.0").unwrap();
assert!(spec.satisfies(&Version::from_str("1.5.0").unwrap()));
assert!(!spec.satisfies(&Version::from_str("2.0.0").unwrap()));
assert!(!spec.satisfies(&Version::from_str("0.9.0").unwrap()));
}
}