1use std::fmt;
7use std::str::FromStr;
8
9#[derive(Debug, Clone, Eq, PartialEq)]
10pub struct TinyVersion {
11 major: u32,
12 minor: u32,
13 patch: u32,
14 pre_release: Option<String>,
15}
16
17#[derive(Debug, Eq, PartialEq)]
18pub enum ParseError {
19 InvalidFormat,
20 InvalidNumber,
21 InvalidPreRelease,
22}
23
24#[derive(Debug, Eq, PartialEq)]
25pub enum NameError {
26 InvalidName(String),
27}
28
29#[derive(Debug, Eq, PartialEq)]
30pub enum SplitError {
31 MissingHyphen,
32 VersionParseError(ParseError),
33}
34
35impl FromStr for TinyVersion {
36 type Err = ParseError;
37
38 fn from_str(s: &str) -> Result<Self, Self::Err> {
51 let mut parts = s.splitn(2, '-');
52 let version_part = parts.next().ok_or(ParseError::InvalidFormat)?;
53 let pre_release_part = parts.next();
54
55 let version_parts: Vec<&str> = version_part.split('.').collect();
56 if version_parts.len() != 3 {
57 return Err(ParseError::InvalidFormat);
58 }
59
60 let major = version_parts[0]
61 .parse()
62 .map_err(|_| ParseError::InvalidNumber)?;
63 let minor = version_parts[1]
64 .parse()
65 .map_err(|_| ParseError::InvalidNumber)?;
66 let patch = version_parts[2]
67 .parse()
68 .map_err(|_| ParseError::InvalidNumber)?;
69
70 let pre_release = match pre_release_part {
71 Some(s) => {
72 if s.is_empty() {
74 return Err(ParseError::InvalidPreRelease);
75 }
76 let identifiers: Vec<&str> = s.split('.').collect();
78 if identifiers.iter().any(|id| id.is_empty()) {
79 return Err(ParseError::InvalidPreRelease);
80 }
81 for id in identifiers {
82 if !id.chars().all(|c| c.is_ascii_alphanumeric() || c == '-') {
84 return Err(ParseError::InvalidPreRelease);
85 }
86 if id.chars().all(|c| c.is_ascii_digit()) && id.len() > 1 && id.starts_with('0')
88 {
89 return Err(ParseError::InvalidPreRelease);
90 }
91 }
92 Some(s.to_string())
93 }
94 None => None,
95 };
96
97 Ok(Self {
98 major,
99 minor,
100 patch,
101 pre_release,
102 })
103 }
104}
105
106impl TinyVersion {
107 pub fn versioned_name(&self, name: &str) -> Result<String, NameError> {
110 if !is_valid_name(name) {
111 return Err(NameError::InvalidName(name.to_string()));
112 }
113 let result = self.pre_release.as_ref().map_or_else(
114 || format!("{}-{}.{}.{}", name, self.major, self.minor, self.patch),
115 |pre| {
116 format!(
117 "{}-{}.{}.{}-{}",
118 name, self.major, self.minor, self.patch, pre
119 )
120 },
121 );
122
123 Ok(result)
124 }
125}
126
127impl fmt::Display for TinyVersion {
128 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
129 match &self.pre_release {
130 Some(pre) => write!(f, "{}.{}.{}-{}", self.major, self.minor, self.patch, pre),
131 None => write!(f, "{}.{}.{}", self.major, self.minor, self.patch),
132 }
133 }
134}
135
136#[must_use]
143pub fn is_valid_name(name: &str) -> bool {
144 let Some(first) = name.chars().next() else {
145 return false;
146 };
147
148 let Some(last) = name.chars().last() else {
149 return false;
150 };
151
152 if !first.is_ascii_lowercase() || !last.is_ascii_lowercase() {
154 return false;
155 }
156 name.chars().all(|c| c.is_ascii_lowercase() || c == '_')
158}
159
160pub fn split_versioned_name(full_name: &str) -> Result<(String, TinyVersion), SplitError> {
177 let hyphen_index = full_name.find('-').ok_or(SplitError::MissingHyphen)?;
178 let name = &full_name[..hyphen_index];
179 let version_str = &full_name[hyphen_index + 1..];
180 let version = TinyVersion::from_str(version_str).map_err(SplitError::VersionParseError)?;
181
182 Ok((name.to_string(), version))
183}