#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]
use core::{fmt, str::FromStr};
use std::error::Error;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum GoVersionParseError {
Empty,
InvalidVersion,
MissingMinor,
TooManyComponents,
}
impl fmt::Display for GoVersionParseError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Empty => formatter.write_str("Go version cannot be empty"),
Self::InvalidVersion => formatter.write_str("invalid Go version"),
Self::MissingMinor => formatter.write_str("Go patch version requires a minor version"),
Self::TooManyComponents => formatter.write_str("Go version has too many components"),
}
}
}
impl Error for GoVersionParseError {}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct GoMajorVersion(u16);
impl GoMajorVersion {
pub const fn new(value: u16) -> Result<Self, GoVersionParseError> {
if value == 0 {
Err(GoVersionParseError::InvalidVersion)
} else {
Ok(Self(value))
}
}
#[must_use]
pub const fn value(self) -> u16 {
self.0
}
}
impl fmt::Display for GoMajorVersion {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "{}", self.0)
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct GoMinorVersion(u16);
impl GoMinorVersion {
#[must_use]
pub const fn new(value: u16) -> Self {
Self(value)
}
#[must_use]
pub const fn value(self) -> u16 {
self.0
}
}
impl fmt::Display for GoMinorVersion {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "{}", self.0)
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct GoPatchVersion(u16);
impl GoPatchVersion {
#[must_use]
pub const fn new(value: u16) -> Self {
Self(value)
}
#[must_use]
pub const fn value(self) -> u16 {
self.0
}
}
impl fmt::Display for GoPatchVersion {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "{}", self.0)
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum GoVersionFamily {
Go1,
Go2,
}
impl GoVersionFamily {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Go1 => "go1",
Self::Go2 => "go2",
}
}
}
impl fmt::Display for GoVersionFamily {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
impl FromStr for GoVersionFamily {
type Err = GoVersionParseError;
fn from_str(input: &str) -> Result<Self, Self::Err> {
match normalize_go_prefix(input)?.as_str() {
"1" => Ok(Self::Go1),
"2" => Ok(Self::Go2),
_ => Err(GoVersionParseError::InvalidVersion),
}
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct GoVersion {
major: GoMajorVersion,
minor: Option<GoMinorVersion>,
patch: Option<GoPatchVersion>,
}
impl GoVersion {
pub const fn new(
major: u16,
minor: Option<u16>,
patch: Option<u16>,
) -> Result<Self, GoVersionParseError> {
if minor.is_none() && patch.is_some() {
return Err(GoVersionParseError::MissingMinor);
}
let Ok(major) = GoMajorVersion::new(major) else {
return Err(GoVersionParseError::InvalidVersion);
};
Ok(Self {
major,
minor: match minor {
Some(value) => Some(GoMinorVersion::new(value)),
None => None,
},
patch: match patch {
Some(value) => Some(GoPatchVersion::new(value)),
None => None,
},
})
}
#[must_use]
pub const fn major(self) -> u16 {
self.major.value()
}
#[must_use]
pub const fn minor(self) -> Option<u16> {
match self.minor {
Some(value) => Some(value.value()),
None => None,
}
}
#[must_use]
pub const fn patch(self) -> Option<u16> {
match self.patch {
Some(value) => Some(value.value()),
None => None,
}
}
#[must_use]
pub const fn family(self) -> Option<GoVersionFamily> {
match self.major() {
1 => Some(GoVersionFamily::Go1),
2 => Some(GoVersionFamily::Go2),
_ => None,
}
}
#[must_use]
pub const fn is_go1(self) -> bool {
matches!(self.family(), Some(GoVersionFamily::Go1))
}
}
impl fmt::Display for GoVersion {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "{}", self.major)?;
if let Some(minor) = self.minor {
write!(formatter, ".{minor}")?;
}
if let Some(patch) = self.patch {
write!(formatter, ".{patch}")?;
}
Ok(())
}
}
impl FromStr for GoVersion {
type Err = GoVersionParseError;
fn from_str(input: &str) -> Result<Self, Self::Err> {
parse_go_version(input)
}
}
impl TryFrom<&str> for GoVersion {
type Error = GoVersionParseError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Self::from_str(value)
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct GoToolchainVersion(GoVersion);
impl GoToolchainVersion {
#[must_use]
pub const fn new(version: GoVersion) -> Self {
Self(version)
}
#[must_use]
pub const fn version(self) -> GoVersion {
self.0
}
}
impl fmt::Display for GoToolchainVersion {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "go{}", self.0)
}
}
impl FromStr for GoToolchainVersion {
type Err = GoVersionParseError;
fn from_str(input: &str) -> Result<Self, Self::Err> {
input.parse::<GoVersion>().map(Self)
}
}
impl TryFrom<&str> for GoToolchainVersion {
type Error = GoVersionParseError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Self::from_str(value)
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct GoCompatibilityVersion(GoVersion);
impl GoCompatibilityVersion {
#[must_use]
pub const fn new(version: GoVersion) -> Self {
Self(version)
}
#[must_use]
pub const fn version(self) -> GoVersion {
self.0
}
}
impl fmt::Display for GoCompatibilityVersion {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(formatter)
}
}
impl FromStr for GoCompatibilityVersion {
type Err = GoVersionParseError;
fn from_str(input: &str) -> Result<Self, Self::Err> {
input.parse::<GoVersion>().map(Self)
}
}
impl TryFrom<&str> for GoCompatibilityVersion {
type Error = GoVersionParseError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Self::from_str(value)
}
}
fn parse_go_version(input: &str) -> Result<GoVersion, GoVersionParseError> {
let normalized = normalize_go_prefix(input)?;
let components = normalized.split('.').collect::<Vec<_>>();
if components.len() > 3 {
return Err(GoVersionParseError::TooManyComponents);
}
if components.iter().any(|component| component.is_empty()) {
return Err(GoVersionParseError::InvalidVersion);
}
let major = parse_component(components[0])?;
let minor = match components.get(1) {
Some(component) => Some(parse_component(component)?),
None => None,
};
let patch = match components.get(2) {
Some(component) => Some(parse_component(component)?),
None => None,
};
GoVersion::new(major, minor, patch)
}
fn normalize_go_prefix(input: &str) -> Result<String, GoVersionParseError> {
let trimmed = input.trim();
if trimmed.is_empty() {
return Err(GoVersionParseError::Empty);
}
let mut characters = trimmed.char_indices();
let first = characters.next();
let second = characters.next();
let without_prefix =
if matches!(first, Some((_, 'g' | 'G'))) && matches!(second, Some((_, 'o' | 'O'))) {
let Some((index, character)) = second else {
return Err(GoVersionParseError::InvalidVersion);
};
trimmed[index + character.len_utf8()..].trim_start()
} else {
trimmed
};
if without_prefix.is_empty() {
Err(GoVersionParseError::InvalidVersion)
} else {
Ok(without_prefix.to_string())
}
}
fn parse_component(component: &str) -> Result<u16, GoVersionParseError> {
if !component
.chars()
.all(|character| character.is_ascii_digit())
{
return Err(GoVersionParseError::InvalidVersion);
}
component
.parse::<u16>()
.map_err(|_| GoVersionParseError::InvalidVersion)
}
#[cfg(test)]
mod tests {
use super::{
GoCompatibilityVersion, GoToolchainVersion, GoVersion, GoVersionFamily, GoVersionParseError,
};
#[test]
fn parses_go_versions() -> Result<(), GoVersionParseError> {
assert_eq!("1".parse::<GoVersion>()?.to_string(), "1");
assert_eq!("1.21".parse::<GoVersion>()?.to_string(), "1.21");
assert_eq!("1.21.6".parse::<GoVersion>()?.to_string(), "1.21.6");
assert_eq!("go1.22.0".parse::<GoVersion>()?.to_string(), "1.22.0");
assert_eq!("Go 1.23.1".parse::<GoVersion>()?.to_string(), "1.23.1");
Ok(())
}
#[test]
fn exposes_version_helpers() -> Result<(), GoVersionParseError> {
let version: GoVersion = "1.22.0".parse()?;
assert_eq!(version.major(), 1);
assert_eq!(version.minor(), Some(22));
assert_eq!(version.patch(), Some(0));
assert_eq!(version.family(), Some(GoVersionFamily::Go1));
assert!(version.is_go1());
Ok(())
}
#[test]
fn rejects_invalid_versions() {
assert_eq!("".parse::<GoVersion>(), Err(GoVersionParseError::Empty));
assert_eq!(
"0".parse::<GoVersion>(),
Err(GoVersionParseError::InvalidVersion)
);
assert_eq!(
"1.2.3.4".parse::<GoVersion>(),
Err(GoVersionParseError::TooManyComponents)
);
assert_eq!(
"1.x".parse::<GoVersion>(),
Err(GoVersionParseError::InvalidVersion)
);
assert_eq!(
"π".parse::<GoVersion>(),
Err(GoVersionParseError::InvalidVersion)
);
}
#[test]
fn models_toolchain_and_compatibility_versions() -> Result<(), GoVersionParseError> {
let version: GoVersion = "1.21".parse()?;
let toolchain = GoToolchainVersion::new(version);
let compatibility = GoCompatibilityVersion::new(version);
assert_eq!(toolchain.to_string(), "go1.21");
assert_eq!(compatibility.to_string(), "1.21");
assert_eq!("go2".parse::<GoVersionFamily>()?, GoVersionFamily::Go2);
Ok(())
}
}