use anyhow::{bail, Result};
use std::fmt;
use crate::version::GoVersion;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum VersionSpec {
Latest,
Partial { major: u32, minor: u32 },
Exact { major: u32, minor: u32, patch: u32 },
}
impl VersionSpec {
pub fn parse(input: &str) -> Result<Self> {
let s = input.trim();
if s.eq_ignore_ascii_case("latest") {
return Ok(Self::Latest);
}
let stripped = s.strip_prefix("go").unwrap_or(s);
let parts: Vec<&str> = stripped.split('.').collect();
match parts.as_slice() {
[maj, min] => Ok(Self::Partial {
major: maj.parse()?,
minor: min.parse()?,
}),
[maj, min, pat] => Ok(Self::Exact {
major: maj.parse()?,
minor: min.parse()?,
patch: pat.parse()?,
}),
_ => bail!(
"Invalid version spec '{}'. Use 'latest', X.Y, or X.Y.Z.",
input
),
}
}
pub fn matches(&self, v: &GoVersion) -> bool {
match self {
Self::Latest => true,
Self::Partial { major, minor } => v.major == *major && v.minor == *minor,
Self::Exact {
major,
minor,
patch,
} => v.major == *major && v.minor == *minor && v.patch == *patch,
}
}
}
impl fmt::Display for VersionSpec {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Latest => write!(f, "latest"),
Self::Partial { major, minor } => write!(f, "{major}.{minor}"),
Self::Exact {
major,
minor,
patch,
} => write!(f, "{major}.{minor}.{patch}"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_latest_case_insensitive() {
assert_eq!(VersionSpec::parse("latest").unwrap(), VersionSpec::Latest);
assert_eq!(VersionSpec::parse("LATEST").unwrap(), VersionSpec::Latest);
assert_eq!(VersionSpec::parse(" Latest ").unwrap(), VersionSpec::Latest);
}
#[test]
fn parse_partial_with_and_without_go_prefix() {
let expected = VersionSpec::Partial {
major: 1,
minor: 22,
};
assert_eq!(VersionSpec::parse("1.22").unwrap(), expected);
assert_eq!(VersionSpec::parse("go1.22").unwrap(), expected);
}
#[test]
fn parse_exact_with_and_without_go_prefix() {
let expected = VersionSpec::Exact {
major: 1,
minor: 22,
patch: 4,
};
assert_eq!(VersionSpec::parse("1.22.4").unwrap(), expected);
assert_eq!(VersionSpec::parse("go1.22.4").unwrap(), expected);
}
#[test]
fn parse_rejects_malformed_input() {
assert!(VersionSpec::parse("not-a-version").is_err());
assert!(VersionSpec::parse("1").is_err());
assert!(VersionSpec::parse("1.2.3.4").is_err());
assert!(VersionSpec::parse("1.x").is_err());
}
#[test]
fn matches_latest_matches_anything() {
let v = GoVersion::parse("1.22.4").unwrap();
assert!(VersionSpec::Latest.matches(&v));
}
#[test]
fn matches_partial_ignores_patch() {
let spec = VersionSpec::Partial {
major: 1,
minor: 22,
};
assert!(spec.matches(&GoVersion::parse("1.22.4").unwrap()));
assert!(spec.matches(&GoVersion::parse("1.22.0").unwrap()));
assert!(!spec.matches(&GoVersion::parse("1.21.4").unwrap()));
assert!(!spec.matches(&GoVersion::parse("2.22.4").unwrap()));
}
#[test]
fn matches_exact_requires_full_equality() {
let spec = VersionSpec::Exact {
major: 1,
minor: 22,
patch: 4,
};
assert!(spec.matches(&GoVersion::parse("1.22.4").unwrap()));
assert!(!spec.matches(&GoVersion::parse("1.22.5").unwrap()));
}
#[test]
fn display_formats_match_user_input_style() {
assert_eq!(VersionSpec::Latest.to_string(), "latest");
assert_eq!(
VersionSpec::Partial {
major: 1,
minor: 22
}
.to_string(),
"1.22"
);
assert_eq!(
VersionSpec::Exact {
major: 1,
minor: 22,
patch: 4
}
.to_string(),
"1.22.4"
);
}
}