1use serde::{Deserialize, Serialize};
7use std::cmp::Ordering;
8use std::fmt;
9
10#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
14pub struct Version {
15 pub major: u64,
16 pub minor: u64,
17 pub patch: u64,
18 #[serde(default)]
21 pub pre: Option<String>,
22 #[serde(default)]
26 pub build: Option<String>,
27 #[serde(default)]
31 pub extension: Option<String>,
32}
33
34impl Version {
35 #[must_use]
37 pub const fn new(major: u64, minor: u64, patch: u64) -> Self {
38 Self {
39 major,
40 minor,
41 patch,
42 pre: None,
43 build: None,
44 extension: None,
45 }
46 }
47
48 #[must_use]
52 pub fn parse(s: &str) -> Option<Self> {
53 let (without_build, build) = match s.split_once('+') {
55 Some((a, b)) => (a, Some(b.to_string())),
56 None => (s, None),
57 };
58 let (without_pre, pre) = match without_build.split_once('-') {
60 Some((a, b)) => (a, Some(b.to_string())),
61 None => (without_build, None),
62 };
63 let mut parts = without_pre.split('.');
64 let major = parts.next()?.parse().ok()?;
65 let minor = parts.next()?.parse().ok()?;
66 let patch = parts.next()?.parse().ok()?;
67 let rest: Vec<&str> = parts.collect();
69 let extension = if rest.is_empty() {
70 None
71 } else {
72 Some(rest.join("."))
73 };
74 Some(Self {
75 major,
76 minor,
77 patch,
78 pre,
79 build,
80 extension,
81 })
82 }
83}
84
85impl PartialOrd for Version {
86 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
87 Some(self.cmp(other))
88 }
89}
90
91impl Ord for Version {
92 fn cmp(&self, other: &Self) -> Ordering {
93 match (self.major, self.minor, self.patch).cmp(&(other.major, other.minor, other.patch)) {
97 Ordering::Equal => match (&self.pre, &other.pre) {
98 (None, None) => Ordering::Equal,
99 (None, Some(_)) => Ordering::Greater,
100 (Some(_), None) => Ordering::Less,
101 (Some(a), Some(b)) => a.cmp(b),
102 },
103 other => other,
104 }
105 }
106}
107
108impl fmt::Display for Version {
109 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
110 write!(f, "{}.{}.{}", self.major, self.minor, self.patch)?;
111 if let Some(pre) = &self.pre {
112 write!(f, "-{pre}")?;
113 }
114 if let Some(build) = &self.build {
115 write!(f, "+{build}")?;
116 }
117 if let Some(ext) = &self.extension {
118 write!(f, ".{ext}")?;
119 }
120 Ok(())
121 }
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127
128 #[test]
129 fn parse_basic_semver() {
130 let v = Version::parse("1.2.3").unwrap();
131 assert_eq!(v, Version::new(1, 2, 3));
132 }
133
134 #[test]
135 fn parse_pre_release() {
136 let v = Version::parse("1.2.3-rc.1").unwrap();
137 assert_eq!(v.pre.as_deref(), Some("rc.1"));
138 }
139
140 #[test]
141 fn parse_build_metadata() {
142 let v = Version::parse("1.2.3+build.42").unwrap();
143 assert_eq!(v.build.as_deref(), Some("build.42"));
144 }
145
146 #[test]
147 fn parse_pre_and_build() {
148 let v = Version::parse("1.2.3-rc.1+build.42").unwrap();
149 assert_eq!(v.pre.as_deref(), Some("rc.1"));
150 assert_eq!(v.build.as_deref(), Some("build.42"));
151 }
152
153 #[test]
154 fn parse_rubygems_four_segment() {
155 let v = Version::parse("1.2.3.beta1").unwrap();
157 assert_eq!(v.extension.as_deref(), Some("beta1"));
158 }
159
160 #[test]
161 fn parse_invalid_returns_none() {
162 assert!(Version::parse("not-a-version").is_none());
163 assert!(Version::parse("1").is_none());
164 assert!(Version::parse("1.2").is_none());
165 }
166
167 #[test]
168 fn ordering_basic() {
169 assert!(Version::new(1, 0, 0) < Version::new(1, 0, 1));
170 assert!(Version::new(1, 1, 0) > Version::new(1, 0, 99));
171 assert!(Version::new(2, 0, 0) > Version::new(1, 99, 99));
172 }
173
174 #[test]
175 fn ordering_pre_release_sorts_before_release() {
176 let pre = Version::parse("1.2.3-rc.1").unwrap();
177 let rel = Version::new(1, 2, 3);
178 assert!(pre < rel);
179 }
180
181 #[test]
182 fn display_round_trip() {
183 let v = Version::parse("1.2.3-rc.1+build.42").unwrap();
184 assert_eq!(format!("{v}"), "1.2.3-rc.1+build.42");
185 }
186
187 #[test]
188 fn serde_json_round_trip() {
189 let v = Version::parse("1.2.3-rc.1").unwrap();
190 let j = serde_json::to_string(&v).unwrap();
191 let parsed: Version = serde_json::from_str(&j).unwrap();
192 assert_eq!(v, parsed);
193 }
194}