mod crates_io;
mod go;
mod npm;
mod pypi;
pub use crates_io::CratesIoVersionSource;
pub use go::GoVersionSource;
pub use npm::NpmVersionSource;
pub use pypi::PyPiVersionSource;
use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use std::time::Duration;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VersionInfo {
pub version: String,
pub yanked: bool,
pub prerelease: bool,
pub published_at: Option<String>,
pub downloads: Option<u64>,
}
impl VersionInfo {
pub fn is_prerelease_version(version: &str) -> bool {
let lower = version.to_lowercase();
lower.contains("-alpha")
|| lower.contains("-beta")
|| lower.contains("-rc")
|| lower.contains("-pre")
|| lower.contains("-dev")
|| lower.contains("-snapshot")
|| lower.contains("-canary")
|| lower.contains("-next")
|| lower.contains("-nightly")
|| version.contains('-')
|| regex::Regex::new(r"\d+\.\d+\.\d+[ab]\d+$").is_ok_and(|re| re.is_match(version))
|| regex::Regex::new(r"\d+\.\d+\.\d+rc\d+$").is_ok_and(|re| re.is_match(version))
}
}
pub trait VersionSource: Send + Sync {
fn ecosystem(&self) -> &'static str;
fn list_versions(&self, package: &str) -> Result<Vec<VersionInfo>>;
fn is_available(&self) -> bool;
}
#[derive(Debug, Clone)]
pub struct VersionSourceConfig {
pub cache_dir: PathBuf,
pub cache_ttl: Duration,
pub offline: bool,
pub timeout: Duration,
}
impl Default for VersionSourceConfig {
fn default() -> Self {
let cache_dir = dirs::cache_dir()
.unwrap_or_else(|| PathBuf::from(".cache"))
.join("rma")
.join("registry");
Self {
cache_dir,
cache_ttl: Duration::from_secs(3600), offline: false,
timeout: Duration::from_secs(30),
}
}
}
pub mod semver_utils {
use std::cmp::Ordering;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SemVer {
pub major: u32,
pub minor: u32,
pub patch: u32,
pub prerelease: Option<String>,
pub build: Option<String>,
}
impl SemVer {
pub fn parse(version: &str) -> Option<Self> {
let version = version.trim().trim_start_matches('v');
let (version, build) = if let Some(idx) = version.find('+') {
(&version[..idx], Some(version[idx + 1..].to_string()))
} else {
(version, None)
};
let (version, prerelease) = if let Some(idx) = version.find('-') {
(&version[..idx], Some(version[idx + 1..].to_string()))
} else {
(version, None)
};
let parts: Vec<&str> = version.split('.').collect();
if parts.is_empty() || parts.len() > 3 {
return None;
}
let major = parts.first()?.parse().ok()?;
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);
Some(Self {
major,
minor,
patch,
prerelease,
build,
})
}
pub fn is_prerelease(&self) -> bool {
self.prerelease.is_some()
}
}
impl PartialOrd for SemVer {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for SemVer {
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.prerelease, &other.prerelease) {
(None, None) => Ordering::Equal,
(Some(_), None) => Ordering::Less,
(None, Some(_)) => Ordering::Greater,
(Some(a), Some(b)) => a.cmp(b),
}
}
}
#[derive(
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
)]
#[serde(rename_all = "lowercase")]
pub enum BumpCategory {
Patch,
Minor,
Major,
}
impl std::fmt::Display for BumpCategory {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
BumpCategory::Patch => write!(f, "patch"),
BumpCategory::Minor => write!(f, "minor"),
BumpCategory::Major => write!(f, "major"),
}
}
}
pub fn classify_bump(from: &SemVer, to: &SemVer) -> BumpCategory {
if from.major != to.major {
BumpCategory::Major
} else if from.minor != to.minor {
BumpCategory::Minor
} else {
BumpCategory::Patch
}
}
pub fn compare_versions(a: &str, b: &str) -> Ordering {
match (SemVer::parse(a), SemVer::parse(b)) {
(Some(va), Some(vb)) => va.cmp(&vb),
(Some(_), None) => Ordering::Greater,
(None, Some(_)) => Ordering::Less,
(None, None) => a.cmp(b), }
}
pub fn is_greater(check: &str, base: &str) -> bool {
compare_versions(check, base) == Ordering::Greater
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_semver_parse() {
let v = SemVer::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 = SemVer::parse("2.0.0-alpha.1").unwrap();
assert_eq!(v.major, 2);
assert_eq!(v.prerelease, Some("alpha.1".to_string()));
let v = SemVer::parse("v3.1.4").unwrap();
assert_eq!(v.major, 3);
}
#[test]
fn test_semver_ordering() {
assert!(SemVer::parse("1.0.0").unwrap() < SemVer::parse("2.0.0").unwrap());
assert!(SemVer::parse("1.0.0").unwrap() < SemVer::parse("1.1.0").unwrap());
assert!(SemVer::parse("1.0.0").unwrap() < SemVer::parse("1.0.1").unwrap());
assert!(SemVer::parse("1.0.0-alpha").unwrap() < SemVer::parse("1.0.0").unwrap());
}
#[test]
fn test_classify_bump() {
let from = SemVer::parse("1.2.3").unwrap();
assert_eq!(
classify_bump(&from, &SemVer::parse("1.2.4").unwrap()),
BumpCategory::Patch
);
assert_eq!(
classify_bump(&from, &SemVer::parse("1.3.0").unwrap()),
BumpCategory::Minor
);
assert_eq!(
classify_bump(&from, &SemVer::parse("2.0.0").unwrap()),
BumpCategory::Major
);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_prerelease_version() {
assert!(VersionInfo::is_prerelease_version("1.0.0-alpha"));
assert!(VersionInfo::is_prerelease_version("2.0.0-beta.1"));
assert!(VersionInfo::is_prerelease_version("3.0.0-rc1"));
assert!(!VersionInfo::is_prerelease_version("1.0.0"));
assert!(!VersionInfo::is_prerelease_version("2.3.4"));
}
}