use crate::{Error, Result};
use serde::{Deserialize, Serialize};
use std::cmp::Ordering;
use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Version {
pub major: u64,
pub minor: u64,
pub patch: u64,
pub prerelease: Option<String>,
pub build: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VersionRange {
pub expression: String,
pub components: Vec<VersionRangeComponent>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VersionRangeComponent {
pub operator: VersionOperator,
pub version: Version,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum VersionOperator {
Equal,
GreaterThan,
GreaterThanOrEqual,
LessThan,
LessThanOrEqual,
Caret,
Tilde,
}
pub struct VersionMatcher;
impl Version {
pub fn parse(version_str: &str) -> Result<Self> {
let version_str = version_str.trim();
let version_str = version_str.strip_prefix('v').unwrap_or(version_str);
let (version_part, build) = if let Some(pos) = version_str.find('+') {
let (v, b) = version_str.split_at(pos);
(v, Some(b[1..].to_string()))
} else {
(version_str, None)
};
let (core_version, prerelease) = if let Some(pos) = version_part.find('-') {
let (v, p) = version_part.split_at(pos);
(v, Some(p[1..].to_string()))
} else {
(version_part, None)
};
let parts: Vec<&str> = core_version.split('.').collect();
if parts.len() < 2 {
return Err(Error::InvalidVersionConstraint {
constraint: version_str.to_string(),
});
}
let major = parts[0]
.parse()
.map_err(|_| Error::InvalidVersionConstraint {
constraint: version_str.to_string(),
})?;
let minor = parts[1]
.parse()
.map_err(|_| Error::InvalidVersionConstraint {
constraint: version_str.to_string(),
})?;
let patch = if parts.len() > 2 {
parts[2]
.parse()
.map_err(|_| Error::InvalidVersionConstraint {
constraint: version_str.to_string(),
})?
} else {
0
};
Ok(Version {
major,
minor,
patch,
prerelease,
build,
})
}
pub fn is_prerelease(&self) -> bool {
self.prerelease.is_some()
}
pub fn core_version(&self) -> String {
format!("{}.{}.{}", self.major, self.minor, self.patch)
}
}
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 prerelease) = self.prerelease {
write!(f, "-{}", prerelease)?;
}
if let Some(ref build) = self.build {
write!(f, "+{}", build)?;
}
Ok(())
}
}
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 => {}
other => return other,
}
match self.minor.cmp(&other.minor) {
Ordering::Equal => {}
other => return other,
}
match self.patch.cmp(&other.patch) {
Ordering::Equal => {}
other => return other,
}
match (&self.prerelease, &other.prerelease) {
(None, None) => Ordering::Equal,
(Some(_), None) => Ordering::Less, (None, Some(_)) => Ordering::Greater, (Some(a), Some(b)) => a.cmp(b), }
}
}
impl VersionRange {
pub fn parse(expression: &str) -> Result<Self> {
let expression = expression.trim();
let components = Self::parse_components(expression)?;
Ok(VersionRange {
expression: expression.to_string(),
components,
})
}
pub fn satisfies(&self, version: &Version) -> bool {
self.components
.iter()
.all(|component| component.satisfies(version))
}
fn parse_components(expression: &str) -> Result<Vec<VersionRangeComponent>> {
let mut components = Vec::new();
let (operator, version_str) = if expression.starts_with(">=") {
(VersionOperator::GreaterThanOrEqual, &expression[2..])
} else if expression.starts_with("<=") {
(VersionOperator::LessThanOrEqual, &expression[2..])
} else if expression.starts_with('>') {
(VersionOperator::GreaterThan, &expression[1..])
} else if expression.starts_with('<') {
(VersionOperator::LessThan, &expression[1..])
} else if expression.starts_with('^') {
(VersionOperator::Caret, &expression[1..])
} else if expression.starts_with('~') {
(VersionOperator::Tilde, &expression[1..])
} else if expression.starts_with('=') {
(VersionOperator::Equal, &expression[1..])
} else {
(VersionOperator::Equal, expression)
};
let version = Version::parse(version_str.trim())?;
components.push(VersionRangeComponent { operator, version });
Ok(components)
}
}
impl VersionRangeComponent {
pub fn satisfies(&self, version: &Version) -> bool {
match self.operator {
VersionOperator::Equal => version == &self.version,
VersionOperator::GreaterThan => version > &self.version,
VersionOperator::GreaterThanOrEqual => version >= &self.version,
VersionOperator::LessThan => version < &self.version,
VersionOperator::LessThanOrEqual => version <= &self.version,
VersionOperator::Caret => self.satisfies_caret(version),
VersionOperator::Tilde => self.satisfies_tilde(version),
}
}
fn satisfies_caret(&self, version: &Version) -> bool {
if version < &self.version {
return false;
}
if self.version.major == 0 {
version.major == self.version.major && version.minor == self.version.minor
} else {
version.major == self.version.major
}
}
fn satisfies_tilde(&self, version: &Version) -> bool {
version >= &self.version
&& version.major == self.version.major
&& version.minor == self.version.minor
}
}
impl VersionMatcher {
pub fn matches(version_str: &str, constraint: &str) -> Result<bool> {
let version = Version::parse(version_str)?;
let range = VersionRange::parse(constraint)?;
Ok(range.satisfies(&version))
}
pub fn find_best_match(versions: &[String], constraint: &str) -> Result<Option<String>> {
let range = VersionRange::parse(constraint)?;
let mut matching_versions = Vec::new();
for version_str in versions {
if let Ok(version) = Version::parse(version_str) {
if range.satisfies(&version) {
matching_versions.push((version, version_str.clone()));
}
}
}
matching_versions.sort_by(|a, b| b.0.cmp(&a.0));
Ok(matching_versions
.first()
.map(|(_, version_str)| version_str.clone()))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_version_parsing() {
let v = Version::parse("1.2.3").unwrap();
assert_eq!(v.major, 1);
assert_eq!(v.minor, 2);
assert_eq!(v.patch, 3);
assert!(v.prerelease.is_none());
let v_pre = Version::parse("2.0.0-beta.1").unwrap();
assert_eq!(v_pre.major, 2);
assert_eq!(v_pre.prerelease, Some("beta.1".to_string()));
assert!(v_pre.is_prerelease());
let v_build = Version::parse("1.0.0+build.123").unwrap();
assert_eq!(v_build.build, Some("build.123".to_string()));
}
#[test]
fn test_version_comparison() {
let v1 = Version::parse("1.0.0").unwrap();
let v2 = Version::parse("1.0.1").unwrap();
let v3 = Version::parse("1.1.0").unwrap();
let v_pre = Version::parse("1.0.0-beta").unwrap();
assert!(v1 < v2);
assert!(v2 < v3);
assert!(v_pre < v1); }
#[test]
fn test_version_range_parsing() {
let range = VersionRange::parse(">=1.0.0").unwrap();
assert_eq!(range.components.len(), 1);
assert_eq!(
range.components[0].operator,
VersionOperator::GreaterThanOrEqual
);
let caret_range = VersionRange::parse("^1.2.3").unwrap();
assert_eq!(caret_range.components[0].operator, VersionOperator::Caret);
}
#[test]
fn test_version_matching() {
assert!(VersionMatcher::matches("1.2.3", ">=1.0.0").unwrap());
assert!(!VersionMatcher::matches("0.9.0", ">=1.0.0").unwrap());
assert!(VersionMatcher::matches("1.2.5", "^1.2.3").unwrap());
assert!(!VersionMatcher::matches("2.0.0", "^1.2.3").unwrap());
assert!(VersionMatcher::matches("1.2.5", "~1.2.3").unwrap());
assert!(!VersionMatcher::matches("1.3.0", "~1.2.3").unwrap());
}
#[test]
fn test_find_best_match() {
let versions = vec![
"1.0.0".to_string(),
"1.1.0".to_string(),
"1.2.0".to_string(),
"2.0.0".to_string(),
];
let best = VersionMatcher::find_best_match(&versions, "^1.0.0").unwrap();
assert_eq!(best, Some("1.2.0".to_string()));
let best_exact = VersionMatcher::find_best_match(&versions, "=1.1.0").unwrap();
assert_eq!(best_exact, Some("1.1.0".to_string()));
}
}