reliakit_primitives/
semver.rs1use crate::{PrimitiveError, PrimitiveResult};
2use alloc::string::{String, ToString};
3use core::fmt;
4
5#[derive(Debug, Clone, PartialEq, Eq, Hash)]
11pub struct SemVer {
12 major: u64,
13 minor: u64,
14 patch: u64,
15 pre: Option<String>,
16 build: Option<String>,
17}
18
19impl SemVer {
20 pub fn new(major: u64, minor: u64, patch: u64) -> Self {
22 Self {
23 major,
24 minor,
25 patch,
26 pre: None,
27 build: None,
28 }
29 }
30
31 pub fn parse(s: &str) -> PrimitiveResult<Self> {
33 if s.is_empty() {
34 return Err(PrimitiveError::Empty);
35 }
36
37 let (s, build) = if let Some(idx) = s.find('+') {
38 let b = s[idx + 1..].to_string();
39 if b.is_empty() {
40 return Err(PrimitiveError::Invalid {
41 message: "build metadata must not be empty after '+'",
42 });
43 }
44 (&s[..idx], Some(b))
45 } else {
46 (s, None)
47 };
48
49 let (s, pre) = if let Some(idx) = s.find('-') {
50 let p = s[idx + 1..].to_string();
51 if p.is_empty() {
52 return Err(PrimitiveError::Invalid {
53 message: "pre-release identifier must not be empty after '-'",
54 });
55 }
56 (&s[..idx], Some(p))
57 } else {
58 (s, None)
59 };
60
61 let mut parts = s.splitn(4, '.');
62 let major = parse_version_component(parts.next().unwrap_or(""))?;
63 let minor = parse_version_component(parts.next().unwrap_or(""))?;
64 let patch = parse_version_component(parts.next().unwrap_or(""))?;
65
66 if parts.next().is_some() {
67 return Err(PrimitiveError::Invalid {
68 message: "semver must have exactly three dot-separated components",
69 });
70 }
71
72 Ok(Self {
73 major,
74 minor,
75 patch,
76 pre,
77 build,
78 })
79 }
80
81 pub fn major(&self) -> u64 {
82 self.major
83 }
84 pub fn minor(&self) -> u64 {
85 self.minor
86 }
87 pub fn patch(&self) -> u64 {
88 self.patch
89 }
90
91 pub fn pre(&self) -> Option<&str> {
93 self.pre.as_deref()
94 }
95
96 pub fn build(&self) -> Option<&str> {
98 self.build.as_deref()
99 }
100
101 pub fn is_pre_release(&self) -> bool {
103 self.pre.is_some()
104 }
105}
106
107fn parse_version_component(s: &str) -> PrimitiveResult<u64> {
108 if s.is_empty() {
109 return Err(PrimitiveError::Invalid {
110 message: "semver component must not be empty",
111 });
112 }
113 if s.len() > 1 && s.starts_with('0') {
114 return Err(PrimitiveError::Invalid {
115 message: "semver component must not have leading zeros",
116 });
117 }
118 parse_u64(s).ok_or(PrimitiveError::Invalid {
119 message: "semver component must be a non-negative integer",
120 })
121}
122
123fn parse_u64(s: &str) -> Option<u64> {
124 if s.is_empty() {
125 return None;
126 }
127 let mut result: u64 = 0;
128 for c in s.chars() {
129 let digit = c.to_digit(10)? as u64;
130 result = result.checked_mul(10)?.checked_add(digit)?;
131 }
132 Some(result)
133}
134
135impl fmt::Display for SemVer {
136 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
137 write!(f, "{}.{}.{}", self.major, self.minor, self.patch)?;
138 if let Some(pre) = &self.pre {
139 write!(f, "-{pre}")?;
140 }
141 if let Some(build) = &self.build {
142 write!(f, "+{build}")?;
143 }
144 Ok(())
145 }
146}
147
148impl PartialOrd for SemVer {
149 fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
150 Some(self.cmp(other))
151 }
152}
153
154impl Ord for SemVer {
155 fn cmp(&self, other: &Self) -> core::cmp::Ordering {
156 let v = (self.major, self.minor, self.patch).cmp(&(other.major, other.minor, other.patch));
157 if v != core::cmp::Ordering::Equal {
158 return v;
159 }
160 match (&self.pre, &other.pre) {
162 (None, None) => core::cmp::Ordering::Equal,
163 (Some(_), None) => core::cmp::Ordering::Less,
164 (None, Some(_)) => core::cmp::Ordering::Greater,
165 (Some(a), Some(b)) => a.cmp(b),
166 }
167 }
168}
169
170#[cfg(test)]
171mod tests {
172 use super::SemVer;
173 use crate::PrimitiveError;
174 use alloc::string::ToString;
175
176 #[test]
177 fn parses_simple() {
178 let v = SemVer::parse("1.2.3").unwrap();
179 assert_eq!(v.major(), 1);
180 assert_eq!(v.minor(), 2);
181 assert_eq!(v.patch(), 3);
182 assert!(v.pre().is_none());
183 assert!(v.build().is_none());
184 }
185
186 #[test]
187 fn parses_with_pre_release() {
188 let v = SemVer::parse("2.0.0-beta.1").unwrap();
189 assert_eq!(v.pre(), Some("beta.1"));
190 assert!(v.is_pre_release());
191 }
192
193 #[test]
194 fn parses_with_build() {
195 let v = SemVer::parse("1.0.0+build.456").unwrap();
196 assert_eq!(v.build(), Some("build.456"));
197 }
198
199 #[test]
200 fn parses_pre_and_build() {
201 let v = SemVer::parse("1.0.0-alpha.1+build.001").unwrap();
202 assert_eq!(v.pre(), Some("alpha.1"));
203 assert_eq!(v.build(), Some("build.001"));
204 }
205
206 #[test]
207 fn rejects_empty() {
208 assert_eq!(SemVer::parse("").unwrap_err(), PrimitiveError::Empty);
209 }
210
211 #[test]
212 fn rejects_missing_components() {
213 assert!(SemVer::parse("1.2").is_err());
214 }
215
216 #[test]
217 fn rejects_too_many_components() {
218 assert!(SemVer::parse("1.2.3.4").is_err());
219 }
220
221 #[test]
222 fn rejects_leading_zeros() {
223 assert!(SemVer::parse("1.02.3").is_err());
224 }
225
226 #[test]
227 fn rejects_non_numeric() {
228 assert!(SemVer::parse("a.b.c").is_err());
229 }
230
231 #[test]
232 fn rejects_empty_pre_release() {
233 assert!(SemVer::parse("1.0.0-").is_err());
234 }
235
236 #[test]
237 fn rejects_empty_build() {
238 assert!(SemVer::parse("1.0.0+").is_err());
239 }
240
241 #[test]
242 fn display() {
243 assert_eq!(SemVer::parse("1.2.3").unwrap().to_string(), "1.2.3");
244 assert_eq!(
245 SemVer::parse("2.0.0-beta.1").unwrap().to_string(),
246 "2.0.0-beta.1"
247 );
248 assert_eq!(
249 SemVer::parse("1.0.0+build").unwrap().to_string(),
250 "1.0.0+build"
251 );
252 assert_eq!(
253 SemVer::parse("1.0.0-alpha+build").unwrap().to_string(),
254 "1.0.0-alpha+build"
255 );
256 }
257
258 #[test]
259 fn new_constructor() {
260 let v = SemVer::new(1, 0, 0);
261 assert_eq!(v.to_string(), "1.0.0");
262 }
263
264 #[test]
265 fn ordering() {
266 let v1 = SemVer::parse("1.0.0").unwrap();
267 let v2 = SemVer::parse("2.0.0").unwrap();
268 let v3 = SemVer::parse("1.1.0").unwrap();
269 assert!(v1 < v2);
270 assert!(v1 < v3);
271 assert!(v3 < v2);
272 }
273
274 #[test]
275 fn pre_release_sorts_below_release() {
276 let release = SemVer::parse("1.0.0").unwrap();
277 let pre = SemVer::parse("1.0.0-alpha").unwrap();
278 assert!(pre < release);
279 assert!(release > pre);
280 }
281
282 #[test]
283 fn pre_release_compared_lexicographically() {
284 let alpha = SemVer::parse("1.0.0-alpha").unwrap();
285 let beta = SemVer::parse("1.0.0-beta").unwrap();
286 assert!(alpha < beta);
287 }
288}