1use std::cmp::Ordering;
2use std::fmt;
3use thiserror::Error;
4
5#[derive(Debug, Clone, Eq, PartialEq)]
10pub enum Segment {
11 Numeric(u64),
12 String(String),
13}
14
15impl PartialOrd for Segment {
16 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
17 Some(self.cmp(other))
18 }
19}
20
21impl Ord for Segment {
22 fn cmp(&self, other: &Self) -> Ordering {
23 match (self, other) {
24 (Segment::Numeric(a), Segment::Numeric(b)) => a.cmp(b),
25 (Segment::String(a), Segment::String(b)) => a.cmp(b),
26 (Segment::Numeric(_), Segment::String(_)) => Ordering::Greater,
28 (Segment::String(_), Segment::Numeric(_)) => Ordering::Less,
29 }
30 }
31}
32
33impl fmt::Display for Segment {
34 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
35 match self {
36 Segment::Numeric(n) => write!(f, "{}", n),
37 Segment::String(s) => write!(f, "{}", s),
38 }
39 }
40}
41
42#[derive(Debug, Clone, Eq)]
63pub struct Version {
64 segments: Vec<Segment>,
65 original: String,
66}
67
68#[derive(Debug, Clone, PartialEq, Eq, Error)]
69pub enum VersionError {
70 #[error("empty version string")]
71 Empty,
72 #[error("invalid character in version: '{0}'")]
73 InvalidCharacter(char),
74 #[error("invalid version format: '{0}'")]
75 InvalidFormat(String),
76}
77
78impl Version {
79 pub fn parse(input: &str) -> Result<Self, VersionError> {
88 let input = input.trim();
89
90 if input.is_empty() {
91 return Ok(Version {
92 segments: vec![Segment::Numeric(0)],
93 original: "0".to_string(),
94 });
95 }
96
97 for c in input.chars() {
99 if !c.is_ascii_alphanumeric() && c != '.' && c != '-' {
100 return Err(VersionError::InvalidCharacter(c));
101 }
102 }
103
104 let normalized = input.replace('-', ".pre.");
106
107 let segments = parse_segments(&normalized)?;
108
109 if segments.is_empty() {
110 return Err(VersionError::InvalidFormat(input.to_string()));
111 }
112
113 Ok(Version {
114 segments,
115 original: input.to_string(),
116 })
117 }
118
119 pub fn is_prerelease(&self) -> bool {
123 self.segments
124 .iter()
125 .any(|s| matches!(s, Segment::String(_)))
126 }
127
128 pub fn bump(&self) -> Version {
135 let mut new_segments = self.segments.clone();
141
142 while new_segments
144 .last()
145 .is_some_and(|s| matches!(s, Segment::String(_)))
146 {
147 new_segments.pop();
148 }
149
150 if new_segments.len() > 1 {
152 new_segments.pop();
153 }
154
155 if let Some(Segment::Numeric(n)) = new_segments.last_mut() {
157 *n += 1;
158 }
159
160 if new_segments.is_empty() {
161 new_segments.push(Segment::Numeric(1));
162 }
163
164 let original = new_segments
165 .iter()
166 .map(|s| s.to_string())
167 .collect::<Vec<_>>()
168 .join(".");
169
170 Version {
171 segments: new_segments,
172 original,
173 }
174 }
175
176 pub fn segments(&self) -> &[Segment] {
178 &self.segments
179 }
180}
181
182fn parse_segments(input: &str) -> Result<Vec<Segment>, VersionError> {
184 let mut segments = Vec::new();
185
186 for part in input.split('.') {
187 if part.is_empty() {
188 continue;
189 }
190
191 let mut chars = part.chars().peekable();
194 while chars.peek().is_some() {
195 let first = *chars.peek().unwrap();
196 if first.is_ascii_digit() {
197 let mut num_str = String::new();
198 while let Some(&c) = chars.peek() {
199 if c.is_ascii_digit() {
200 num_str.push(c);
201 chars.next();
202 } else {
203 break;
204 }
205 }
206 let n: u64 = num_str.parse().map_err(|_| {
207 VersionError::InvalidFormat(format!("numeric overflow: {}", num_str))
208 })?;
209 segments.push(Segment::Numeric(n));
210 } else if first.is_ascii_alphabetic() {
211 let mut s = String::new();
212 while let Some(&c) = chars.peek() {
213 if c.is_ascii_alphabetic() {
214 s.push(c);
215 chars.next();
216 } else {
217 break;
218 }
219 }
220 segments.push(Segment::String(s));
221 } else {
222 return Err(VersionError::InvalidCharacter(first));
223 }
224 }
225 }
226
227 Ok(segments)
228}
229
230impl PartialEq for Version {
231 fn eq(&self, other: &Self) -> bool {
232 self.cmp(other) == Ordering::Equal
233 }
234}
235
236impl PartialOrd for Version {
237 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
238 Some(self.cmp(other))
239 }
240}
241
242impl Ord for Version {
243 fn cmp(&self, other: &Self) -> Ordering {
244 let a = &self.segments;
245 let b = &other.segments;
246 let max_len = a.len().max(b.len());
247
248 for i in 0..max_len {
249 let seg_a = a.get(i);
250 let seg_b = b.get(i);
251
252 let ord = match (seg_a, seg_b) {
253 (Some(sa), Some(sb)) => sa.cmp(sb),
254 (Some(sa), None) => sa.cmp(&Segment::Numeric(0)),
256 (None, Some(sb)) => Segment::Numeric(0).cmp(sb),
257 (None, None) => Ordering::Equal,
258 };
259
260 if ord != Ordering::Equal {
261 return ord;
262 }
263 }
264
265 Ordering::Equal
266 }
267}
268
269impl fmt::Display for Version {
270 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
271 write!(f, "{}", self.original)
272 }
273}
274
275#[cfg(test)]
276mod tests {
277 use super::*;
278
279 #[test]
282 fn parse_simple_version() {
283 let v = Version::parse("1.2.3").unwrap();
284 assert_eq!(
285 v.segments,
286 vec![
287 Segment::Numeric(1),
288 Segment::Numeric(2),
289 Segment::Numeric(3)
290 ]
291 );
292 }
293
294 #[test]
295 fn parse_single_segment() {
296 let v = Version::parse("5").unwrap();
297 assert_eq!(v.segments, vec![Segment::Numeric(5)]);
298 }
299
300 #[test]
301 fn parse_with_leading_zeros() {
302 let v = Version::parse("01.02.03").unwrap();
303 assert_eq!(
304 v.segments,
305 vec![
306 Segment::Numeric(1),
307 Segment::Numeric(2),
308 Segment::Numeric(3)
309 ]
310 );
311 }
312
313 #[test]
314 fn parse_prerelease_with_dot() {
315 let v = Version::parse("1.0.0.alpha").unwrap();
316 assert_eq!(
317 v.segments,
318 vec![
319 Segment::Numeric(1),
320 Segment::Numeric(0),
321 Segment::Numeric(0),
322 Segment::String("alpha".to_string()),
323 ]
324 );
325 assert!(v.is_prerelease());
326 }
327
328 #[test]
329 fn parse_prerelease_inline() {
330 let v = Version::parse("1.0.0rc1").unwrap();
331 assert_eq!(
332 v.segments,
333 vec![
334 Segment::Numeric(1),
335 Segment::Numeric(0),
336 Segment::Numeric(0),
337 Segment::String("rc".to_string()),
338 Segment::Numeric(1),
339 ]
340 );
341 }
342
343 #[test]
344 fn parse_prerelease_with_hyphen() {
345 let v = Version::parse("1.0.0-rc1").unwrap();
346 assert_eq!(
347 v.segments,
348 vec![
349 Segment::Numeric(1),
350 Segment::Numeric(0),
351 Segment::Numeric(0),
352 Segment::String("pre".to_string()),
353 Segment::String("rc".to_string()),
354 Segment::Numeric(1),
355 ]
356 );
357 }
358
359 #[test]
360 fn parse_empty_string() {
361 let v = Version::parse("").unwrap();
362 assert_eq!(v.segments, vec![Segment::Numeric(0)]);
363 }
364
365 #[test]
366 fn parse_invalid_character() {
367 assert!(Version::parse("1.0+build").is_err());
368 assert!(Version::parse("1.0_pre1").is_err());
369 }
370
371 #[test]
374 fn compare_simple_versions() {
375 let v1 = Version::parse("1.0.0").unwrap();
376 let v2 = Version::parse("1.0.1").unwrap();
377 assert!(v1 < v2);
378 }
379
380 #[test]
381 fn compare_major_versions() {
382 let v1 = Version::parse("1.0.0").unwrap();
383 let v2 = Version::parse("2.0.0").unwrap();
384 assert!(v1 < v2);
385 }
386
387 #[test]
388 fn trailing_zeros_are_equal() {
389 let v1 = Version::parse("1.0").unwrap();
390 let v2 = Version::parse("1.0.0").unwrap();
391 let v3 = Version::parse("1.0.0.0").unwrap();
392 assert_eq!(v1, v2);
393 assert_eq!(v2, v3);
394 assert_eq!(v1, v3);
395 }
396
397 #[test]
398 fn single_segment_equals_with_trailing_zeros() {
399 let v1 = Version::parse("1").unwrap();
400 let v2 = Version::parse("1.0").unwrap();
401 assert_eq!(v1, v2);
402 }
403
404 #[test]
405 fn prerelease_less_than_release() {
406 let pre = Version::parse("1.0.0.alpha").unwrap();
407 let rel = Version::parse("1.0.0").unwrap();
408 assert!(pre < rel);
409 }
410
411 #[test]
412 fn prerelease_inline_less_than_release() {
413 let pre = Version::parse("1.0.0a").unwrap();
414 let rel = Version::parse("1.0.0").unwrap();
415 assert!(pre < rel);
416 }
417
418 #[test]
419 fn prerelease_ordering() {
420 let alpha = Version::parse("1.0.0.alpha").unwrap();
421 let beta = Version::parse("1.0.0.beta").unwrap();
422 let rc = Version::parse("1.0.0.rc").unwrap();
423 let release = Version::parse("1.0.0").unwrap();
424
425 assert!(alpha < beta);
426 assert!(beta < rc);
427 assert!(rc < release);
428 }
429
430 #[test]
431 fn prerelease_a_b_ordering() {
432 let a = Version::parse("1.0.0a").unwrap();
433 let b = Version::parse("1.0.0b").unwrap();
434 assert!(a < b);
435 }
436
437 #[test]
438 fn string_segment_less_than_integer() {
439 let with_str = Version::parse("1.0.0.alpha").unwrap();
441 let without = Version::parse("1.0.0").unwrap();
442 assert!(with_str < without);
443
444 let with_str2 = Version::parse("1.0.0a").unwrap();
445 assert!(with_str2 < without);
446 }
447
448 #[test]
449 fn compare_different_lengths() {
450 let short = Version::parse("1.0").unwrap();
451 let long = Version::parse("1.0.1").unwrap();
452 assert!(short < long);
453 }
454
455 #[test]
456 fn compare_year_based_versions() {
457 let v1 = Version::parse("2020.1.1").unwrap();
458 let v2 = Version::parse("2021.1.1").unwrap();
459 assert!(v1 < v2);
460 }
461
462 #[test]
465 fn not_prerelease() {
466 let v = Version::parse("1.2.3").unwrap();
467 assert!(!v.is_prerelease());
468 }
469
470 #[test]
471 fn is_prerelease_with_alpha() {
472 let v = Version::parse("1.0.0.alpha").unwrap();
473 assert!(v.is_prerelease());
474 }
475
476 #[test]
477 fn is_prerelease_inline() {
478 let v = Version::parse("1.0.0rc1").unwrap();
479 assert!(v.is_prerelease());
480 }
481
482 #[test]
483 fn four_segment_numeric_not_prerelease() {
484 let v = Version::parse("1.0.0.1").unwrap();
485 assert!(!v.is_prerelease());
486 }
487
488 #[test]
491 fn bump_three_segments() {
492 let v = Version::parse("1.2.3").unwrap();
493 let bumped = v.bump();
494 assert_eq!(bumped, Version::parse("1.3").unwrap());
495 }
496
497 #[test]
498 fn bump_two_segments() {
499 let v = Version::parse("1.0").unwrap();
500 let bumped = v.bump();
501 assert_eq!(bumped, Version::parse("2").unwrap());
502 }
503
504 #[test]
505 fn bump_single_segment() {
506 let v = Version::parse("1").unwrap();
507 let bumped = v.bump();
508 assert_eq!(bumped, Version::parse("2").unwrap());
509 }
510
511 #[test]
512 fn bump_four_segments() {
513 let v = Version::parse("1.2.3.4").unwrap();
514 let bumped = v.bump();
515 assert_eq!(bumped, Version::parse("1.2.4").unwrap());
516 }
517
518 #[test]
519 fn bump_with_trailing_zeros() {
520 let v = Version::parse("1.0.0").unwrap();
523 let bumped = v.bump();
524 assert_eq!(bumped, Version::parse("1.1").unwrap());
525 }
526
527 #[test]
530 fn display_preserves_original() {
531 let v = Version::parse("1.2.3").unwrap();
532 assert_eq!(v.to_string(), "1.2.3");
533 }
534
535 #[test]
536 fn display_prerelease() {
537 let v = Version::parse("1.0.0.alpha").unwrap();
538 assert_eq!(v.to_string(), "1.0.0.alpha");
539 }
540}