use std::fmt;
use std::str::FromStr;
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct TinyVersion {
major: u32,
minor: u32,
patch: u32,
pre_release: Option<String>,
}
#[derive(Debug, Eq, PartialEq)]
pub enum ParseError {
InvalidFormat,
InvalidNumber,
InvalidPreRelease,
}
#[derive(Debug, Eq, PartialEq)]
pub enum NameError {
InvalidName(String),
}
#[derive(Debug, Eq, PartialEq)]
pub enum SplitError {
MissingHyphen,
VersionParseError(ParseError),
}
impl FromStr for TinyVersion {
type Err = ParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut parts = s.splitn(2, '-');
let version_part = parts.next().ok_or(ParseError::InvalidFormat)?;
let pre_release_part = parts.next();
let version_parts: Vec<&str> = version_part.split('.').collect();
if version_parts.len() != 3 {
return Err(ParseError::InvalidFormat);
}
let major = version_parts[0]
.parse()
.map_err(|_| ParseError::InvalidNumber)?;
let minor = version_parts[1]
.parse()
.map_err(|_| ParseError::InvalidNumber)?;
let patch = version_parts[2]
.parse()
.map_err(|_| ParseError::InvalidNumber)?;
let pre_release = match pre_release_part {
Some(s) => {
if s.is_empty() {
return Err(ParseError::InvalidPreRelease);
}
let identifiers: Vec<&str> = s.split('.').collect();
if identifiers.iter().any(|id| id.is_empty()) {
return Err(ParseError::InvalidPreRelease);
}
for id in identifiers {
if !id.chars().all(|c| c.is_ascii_alphanumeric() || c == '-') {
return Err(ParseError::InvalidPreRelease);
}
if id.chars().all(|c| c.is_ascii_digit()) && id.len() > 1 && id.starts_with('0')
{
return Err(ParseError::InvalidPreRelease);
}
}
Some(s.to_string())
}
None => None,
};
Ok(Self {
major,
minor,
patch,
pre_release,
})
}
}
impl TinyVersion {
pub fn versioned_name(&self, name: &str) -> Result<String, NameError> {
if !is_valid_name(name) {
return Err(NameError::InvalidName(name.to_string()));
}
let result = self.pre_release.as_ref().map_or_else(
|| format!("{}-{}.{}.{}", name, self.major, self.minor, self.patch),
|pre| {
format!(
"{}-{}.{}.{}-{}",
name, self.major, self.minor, self.patch, pre
)
},
);
Ok(result)
}
}
impl fmt::Display for TinyVersion {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.pre_release {
Some(pre) => write!(f, "{}.{}.{}-{}", self.major, self.minor, self.patch, pre),
None => write!(f, "{}.{}.{}", self.major, self.minor, self.patch),
}
}
}
#[must_use]
pub fn is_valid_name(name: &str) -> bool {
let Some(first) = name.chars().next() else {
return false;
};
let Some(last) = name.chars().last() else {
return false;
};
if !first.is_ascii_lowercase() || !last.is_ascii_lowercase() {
return false;
}
name.chars().all(|c| c.is_ascii_lowercase() || c == '_')
}
pub fn split_versioned_name(full_name: &str) -> Result<(String, TinyVersion), SplitError> {
let hyphen_index = full_name.find('-').ok_or(SplitError::MissingHyphen)?;
let name = &full_name[..hyphen_index];
let version_str = &full_name[hyphen_index + 1..];
let version = TinyVersion::from_str(version_str).map_err(SplitError::VersionParseError)?;
Ok((name.to_string(), version))
}