1use std::fmt::{self, Display, Formatter};
2
3#[cfg(feature = "serde")]
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
8#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
9pub enum Version {
10 Unknown,
12 Semantic(u64, u64, u64),
14 Rolling(Option<String>),
16 Custom(String),
18}
19
20impl Version {
21 pub fn from_string<S: Into<String> + AsRef<str>>(s: S) -> Self {
38 if s.as_ref().is_empty() {
39 Self::Unknown
40 } else if let Some((major, minor, patch)) = parse_version(s.as_ref()) {
41 Self::Semantic(major, minor, patch)
42 } else {
43 Self::Custom(s.into())
44 }
45 }
46}
47
48impl Default for Version {
49 fn default() -> Self {
50 Version::Unknown
51 }
52}
53
54impl Display for Version {
55 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
56 match *self {
57 Self::Unknown => f.write_str("Unknown"),
58 Self::Semantic(major, minor, patch) => write!(f, "{major}.{minor}.{patch}"),
59 Self::Rolling(ref date) => {
60 let date = match date {
61 Some(date) => format!(" ({date})"),
62 None => "".to_owned(),
63 };
64 write!(f, "Rolling Release{date}")
65 }
66 Self::Custom(ref version) => write!(f, "{version}"),
67 }
68 }
69}
70
71fn parse_version(s: &str) -> Option<(u64, u64, u64)> {
72 let mut iter = s.trim().split_terminator('.').fuse();
73
74 let major = iter.next().and_then(|s| s.parse().ok())?;
75 let minor = iter.next().unwrap_or("0").parse().ok()?;
76 let patch = iter.next().unwrap_or("0").parse().ok()?;
77
78 if iter.next().is_some() {
79 return None;
80 }
81
82 Some((major, minor, patch))
83}
84
85#[cfg(test)]
86mod tests {
87 use super::*;
88 use pretty_assertions::assert_eq;
89
90 #[test]
91 fn parse_semantic_version() {
92 let data = [
93 ("", None),
94 ("version", None),
95 ("1", Some((1, 0, 0))),
96 ("1.", Some((1, 0, 0))),
97 ("1.2", Some((1, 2, 0))),
98 ("1.2.", Some((1, 2, 0))),
99 ("1.2.3", Some((1, 2, 3))),
100 ("1.2.3.", Some((1, 2, 3))),
101 ("1.2.3. ", Some((1, 2, 3))),
102 (" 1.2.3.", Some((1, 2, 3))),
103 (" 1.2.3. ", Some((1, 2, 3))),
104 ("1.2.3.4", None),
105 ("1.2.3.4.5.6.7.8.9", None),
106 ];
107
108 for (s, expected) in &data {
109 let result = parse_version(s);
110 assert_eq!(expected, &result);
111 }
112 }
113
114 #[test]
115 fn from_string() {
116 let custom_version = "some version";
117 let data = [
118 ("", Version::Unknown),
119 ("1.2.3", Version::Semantic(1, 2, 3)),
120 (custom_version, Version::Custom(custom_version.to_owned())),
121 ];
122
123 for (s, expected) in &data {
124 let version = Version::from_string(*s);
125 assert_eq!(expected, &version);
126 }
127 }
128
129 #[test]
130 fn default() {
131 assert_eq!(Version::Unknown, Version::default());
132 }
133
134 #[test]
135 fn display() {
136 let data = [
137 (Version::Unknown, "Unknown"),
138 (Version::Semantic(1, 5, 0), "1.5.0"),
139 (Version::Rolling(None), "Rolling Release"),
140 (
141 Version::Rolling(Some("date".to_owned())),
142 "Rolling Release (date)",
143 ),
144 ];
145
146 for (version, expected) in &data {
147 assert_eq!(expected, &version.to_string());
148 }
149 }
150}