use semver::{Version, VersionReq};
pub fn satisfies(build: &BuildId, req: &VersionReq) -> bool {
build.version.as_ref().is_some_and(|v| req.matches(v))
}
pub fn min_version(builds: &[BuildId]) -> Option<Version> {
builds.iter().filter_map(|b| b.version.clone()).min()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Drift {
Current,
Ahead,
Patch,
Minor,
Major,
}
impl Drift {
pub fn grade(built: &Version, pin: &Version) -> Drift {
use std::cmp::Ordering::*;
match built.cmp(pin) {
Equal => Drift::Current,
Greater => Drift::Ahead,
Less => {
if built.major != pin.major {
Drift::Major
} else if built.minor != pin.minor {
Drift::Minor
} else {
Drift::Patch
}
}
}
}
pub fn label(self) -> &'static str {
match self {
Drift::Current => "current",
Drift::Ahead => "ahead",
Drift::Patch => "patch",
Drift::Minor => "minor",
Drift::Major => "major",
}
}
}
#[derive(Clone)]
pub struct BuildId {
pub version: Option<Version>,
pub sha: String,
}
impl BuildId {
pub fn parse(s: &str) -> BuildId {
let s = s.trim();
let mut parts = s.split_whitespace();
let first = parts.next().unwrap_or("");
match Version::parse(first) {
Ok(v) => BuildId { version: Some(v), sha: parts.next().unwrap_or("").to_string() },
Err(_) => BuildId { version: None, sha: first.to_string() }, }
}
fn core(v: &Version) -> String {
if v.pre.is_empty() {
format!("{}.{}.{}", v.major, v.minor, v.patch)
} else {
format!("{}.{}.{}-{}", v.major, v.minor, v.patch, v.pre)
}
}
pub fn pin_string(&self) -> String {
match &self.version {
Some(v) if !self.sha.is_empty() => format!("{} {}", Self::core(v), self.sha),
Some(v) => Self::core(v),
None => self.sha.clone(),
}
}
pub fn label(&self) -> String {
match (&self.version, self.sha.is_empty()) {
(Some(v), false) => format!("{} ({})", Self::core(v), self.sha),
(Some(v), true) => Self::core(v),
(None, false) => self.sha.clone(),
(None, true) => "unknown".to_string(),
}
}
}
pub struct Assessment {
pub grade: &'static str,
pub outdated: bool,
}
pub fn assess(built: &BuildId, pin: Option<&BuildId>) -> Assessment {
let Some(pin) = pin else {
return Assessment { grade: "no-pin", outdated: false };
};
match (&built.version, &pin.version) {
(Some(b), Some(p)) => {
use std::cmp::Ordering::*;
match b.cmp(p) {
Greater => Assessment { grade: "ahead", outdated: false },
Less => Assessment { grade: Drift::grade(b, p).label(), outdated: true },
Equal => {
if built.sha == pin.sha {
Assessment { grade: "current", outdated: false }
} else {
Assessment { grade: "rebuild", outdated: true }
}
}
}
}
_ => {
if pin.sha.is_empty() {
Assessment { grade: "no-pin", outdated: false }
} else if built.sha == pin.sha {
Assessment { grade: "current", outdated: false }
} else {
Assessment { grade: "drift", outdated: true }
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn ver(s: &str) -> Version {
Version::parse(s).unwrap()
}
fn b(s: &str) -> BuildId {
BuildId::parse(s)
}
#[test]
fn drift_grades_by_highest_differing_component() {
use Drift::*;
assert_eq!(Drift::grade(&ver("1.2.3"), &ver("1.2.3")), Current);
assert_eq!(Drift::grade(&ver("1.3.0"), &ver("1.2.9")), Ahead);
assert_eq!(Drift::grade(&ver("1.2.3"), &ver("1.2.4")), Patch);
assert_eq!(Drift::grade(&ver("1.2.9"), &ver("1.3.0")), Minor);
assert_eq!(Drift::grade(&ver("1.9.9"), &ver("2.0.0")), Major);
assert_eq!(Drift::grade(&ver("1.2.3"), &ver("2.3.4")), Major); assert_eq!(Drift::grade(&ver("1.0.0-rc.1"), &ver("1.0.0")), Patch); }
#[test]
fn parses_pin_forms() {
let x = b("0.2.0 67a1148");
assert_eq!(x.version.as_ref().unwrap().to_string(), "0.2.0");
assert_eq!(x.sha, "67a1148");
assert!(b("abc1234").version.is_none()); assert_eq!(b("abc1234").sha, "abc1234");
assert_eq!(b("1.0.0").sha, "");
assert_eq!(b("0.2.0 abc").pin_string(), "0.2.0 abc");
}
#[test]
fn assess_grades_every_case() {
let g = |built: &str, pin: Option<&str>| {
let pinb = pin.map(b);
assess(&b(built), pinb.as_ref())
};
assert_eq!(g("0.1.0 aaa", None).grade, "no-pin");
let cur = g("0.2.0 aaa", Some("0.2.0 aaa"));
assert_eq!(cur.grade, "current");
assert!(!cur.outdated);
assert_eq!(g("0.3.0 zzz", Some("0.2.0 aaa")).grade, "ahead");
let r = g("0.2.0 bbb", Some("0.2.0 aaa"));
assert_eq!(r.grade, "rebuild");
assert!(r.outdated);
assert_eq!(g("0.2.0 x", Some("0.2.4 y")).grade, "patch");
assert_eq!(g("0.2.0 x", Some("0.5.0 y")).grade, "minor");
assert_eq!(g("0.2.0 x", Some("1.0.0 y")).grade, "major");
assert!(g("0.2.0 x", Some("1.0.0 y")).outdated);
assert_eq!(g("0.1.0 aaa", Some("aaa")).grade, "current");
assert_eq!(g("0.1.0 aaa", Some("bbb")).grade, "drift");
assert!(g("0.1.0 aaa", Some("bbb")).outdated);
}
#[test]
fn floor_satisfaction_and_min() {
let req = VersionReq::parse(">=0.2.0").unwrap();
assert!(satisfies(&b("0.2.0 aaa"), &req));
assert!(satisfies(&b("0.3.1 aaa"), &req));
assert!(!satisfies(&b("0.1.9 aaa"), &req));
assert!(!satisfies(&b("abc1234"), &req)); let builds = [b("0.2.0 a"), b("0.3.0 b"), b("0.1.5 c")];
assert_eq!(min_version(&builds).unwrap().to_string(), "0.1.5");
}
}