1use std::fmt;
7use std::str::FromStr;
8
9#[derive(Debug, 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
29impl FromStr for TinyVersion {
30 type Err = ParseError;
31
32 fn from_str(s: &str) -> Result<Self, Self::Err> {
45 let mut parts = s.splitn(2, '-');
46 let version_part = parts.next().ok_or(ParseError::InvalidFormat)?;
47 let pre_release_part = parts.next();
48
49 let version_parts: Vec<&str> = version_part.split('.').collect();
50 if version_parts.len() != 3 {
51 return Err(ParseError::InvalidFormat);
52 }
53
54 let major = version_parts[0]
55 .parse()
56 .map_err(|_| ParseError::InvalidNumber)?;
57 let minor = version_parts[1]
58 .parse()
59 .map_err(|_| ParseError::InvalidNumber)?;
60 let patch = version_parts[2]
61 .parse()
62 .map_err(|_| ParseError::InvalidNumber)?;
63
64 let pre_release = match pre_release_part {
65 Some(s) => {
66 if s.is_empty() {
68 return Err(ParseError::InvalidPreRelease);
69 }
70 let identifiers: Vec<&str> = s.split('.').collect();
72 if identifiers.iter().any(|id| id.is_empty()) {
73 return Err(ParseError::InvalidPreRelease);
74 }
75 for id in identifiers {
76 if !id.chars().all(|c| c.is_ascii_alphanumeric() || c == '-') {
78 return Err(ParseError::InvalidPreRelease);
79 }
80 if id.chars().all(|c| c.is_ascii_digit()) && id.len() > 1 && id.starts_with('0')
82 {
83 return Err(ParseError::InvalidPreRelease);
84 }
85 }
86 Some(s.to_string())
87 }
88 None => None,
89 };
90
91 Ok(Self {
92 major,
93 minor,
94 patch,
95 pre_release,
96 })
97 }
98}
99
100impl TinyVersion {
101 pub fn versioned_name(&self, name: &str) -> Result<String, NameError> {
104 if !is_valid_name(name) {
105 return Err(NameError::InvalidName(name.to_string()));
106 }
107 let result = self.pre_release.as_ref().map_or_else(
108 || format!("{}-{}.{}.{}", name, self.major, self.minor, self.patch),
109 |pre| {
110 format!(
111 "{}-{}.{}.{}-{}",
112 name, self.major, self.minor, self.patch, pre
113 )
114 },
115 );
116
117 Ok(result)
118 }
119}
120
121impl fmt::Display for TinyVersion {
122 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
123 match &self.pre_release {
124 Some(pre) => write!(f, "{}.{}.{}-{}", self.major, self.minor, self.patch, pre),
125 None => write!(f, "{}.{}.{}", self.major, self.minor, self.patch),
126 }
127 }
128}
129
130#[must_use]
137pub fn is_valid_name(name: &str) -> bool {
138 let Some(first) = name.chars().next() else {
139 return false;
140 };
141
142 let Some(last) = name.chars().last() else {
143 return false;
144 };
145
146 if !first.is_ascii_lowercase() || !last.is_ascii_lowercase() {
148 return false;
149 }
150 name.chars().all(|c| c.is_ascii_lowercase() || c == '_')
152}