1use std::cmp::Ordering;
4use std::fmt;
5use std::num::ParseIntError;
6use std::str::FromStr;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
13pub struct FreeswitchVersion {
14 major: u32,
15 minor: u32,
16 micro: u32,
17 dev: bool,
18}
19
20impl FreeswitchVersion {
21 pub const fn new(major: u32, minor: u32, micro: u32) -> Self {
23 Self {
24 major,
25 minor,
26 micro,
27 dev: false,
28 }
29 }
30
31 pub const fn dev(mut self) -> Self {
33 self.dev = true;
34 self
35 }
36
37 pub fn major(&self) -> u32 {
39 self.major
40 }
41
42 pub fn minor(&self) -> u32 {
44 self.minor
45 }
46
47 pub fn micro(&self) -> u32 {
49 self.micro
50 }
51
52 pub fn is_dev(&self) -> bool {
54 self.dev
55 }
56}
57
58impl Ord for FreeswitchVersion {
59 fn cmp(&self, other: &Self) -> Ordering {
60 (self.major, self.minor, self.micro)
61 .cmp(&(other.major, other.minor, other.micro))
62 .then_with(|| {
63 other
64 .dev
65 .cmp(&self.dev)
66 })
67 }
68}
69
70impl PartialOrd for FreeswitchVersion {
71 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
72 Some(self.cmp(other))
73 }
74}
75
76impl fmt::Display for FreeswitchVersion {
77 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
78 write!(f, "{}.{}.{}", self.major, self.minor, self.micro)?;
79 if self.dev {
80 f.write_str("-dev")?;
81 }
82 Ok(())
83 }
84}
85
86impl FromStr for FreeswitchVersion {
87 type Err = ParseFreeswitchVersionError;
88
89 fn from_str(s: &str) -> Result<Self, Self::Err> {
90 let (numbers, dev) = match s.split_once('-') {
91 None => (s, false),
92 Some((numbers, "dev")) => (numbers, true),
93 Some((numbers, "release")) => (numbers, false),
94 Some(_) => return Err(ParseFreeswitchVersionError::UnknownSuffix),
95 };
96 let mut parts = numbers.split('.');
97 let major = component(&mut parts, "major")?;
98 let minor = component(&mut parts, "minor")?;
99 let micro = component(&mut parts, "micro")?;
100 if parts
101 .next()
102 .is_some()
103 {
104 return Err(ParseFreeswitchVersionError::TrailingComponent);
105 }
106 Ok(Self {
107 major,
108 minor,
109 micro,
110 dev,
111 })
112 }
113}
114
115fn component(
116 parts: &mut std::str::Split<'_, char>,
117 name: &'static str,
118) -> Result<u32, ParseFreeswitchVersionError> {
119 parts
120 .next()
121 .ok_or(ParseFreeswitchVersionError::MissingComponent(name))?
122 .parse()
123 .map_err(|source| ParseFreeswitchVersionError::InvalidComponent {
124 component: name,
125 source,
126 })
127}
128
129#[derive(Debug, Clone, PartialEq, Eq)]
132#[non_exhaustive]
133pub enum ParseFreeswitchVersionError {
134 MissingComponent(&'static str),
136 InvalidComponent {
138 component: &'static str,
140 source: ParseIntError,
142 },
143 TrailingComponent,
145 UnknownSuffix,
147}
148
149impl fmt::Display for ParseFreeswitchVersionError {
150 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
151 match self {
152 Self::MissingComponent(name) => {
153 write!(f, "FreeSWITCH version lacks its {name} component")
154 }
155 Self::InvalidComponent { component, .. } => {
156 write!(
157 f,
158 "FreeSWITCH version {component} component is not a number"
159 )
160 }
161 Self::TrailingComponent => {
162 f.write_str("FreeSWITCH version has components past major.minor.micro")
163 }
164 Self::UnknownSuffix => {
165 f.write_str("FreeSWITCH version suffix is neither -dev nor -release")
166 }
167 }
168 }
169}
170
171impl std::error::Error for ParseFreeswitchVersionError {
172 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
173 match self {
174 Self::InvalidComponent { source, .. } => Some(source),
175 _ => None,
176 }
177 }
178}
179
180#[cfg(feature = "serde")]
181impl serde::Serialize for FreeswitchVersion {
182 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
183 serializer.collect_str(self)
184 }
185}
186
187#[cfg(feature = "serde")]
188impl<'de> serde::Deserialize<'de> for FreeswitchVersion {
189 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
190 String::deserialize(deserializer)?
191 .parse()
192 .map_err(serde::de::Error::custom)
193 }
194}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199
200 #[test]
201 fn parses_release_and_dev_short_forms() {
202 let cases = [
203 ("1.10.12", FreeswitchVersion::new(1, 10, 12)),
204 ("1.10.12-release", FreeswitchVersion::new(1, 10, 12)),
205 ("1.10.13-dev", FreeswitchVersion::new(1, 10, 13).dev()),
206 ];
207 for (input, want) in cases {
208 assert_eq!(
209 input
210 .parse::<FreeswitchVersion>()
211 .ok(),
212 Some(want),
213 "{input}"
214 );
215 }
216 let dev: FreeswitchVersion = "1.10.13-dev"
217 .parse()
218 .unwrap();
219 assert!(dev.is_dev());
220 assert_eq!((dev.major(), dev.minor(), dev.micro()), (1, 10, 13));
221 }
222
223 #[test]
226 fn a_dev_build_sorts_below_its_release() {
227 assert!(FreeswitchVersion::new(1, 10, 13).dev() < FreeswitchVersion::new(1, 10, 13));
228 assert!(FreeswitchVersion::new(1, 10, 12) < FreeswitchVersion::new(1, 10, 13).dev());
229 assert!(FreeswitchVersion::new(1, 9, 99) < FreeswitchVersion::new(1, 10, 0));
230 }
231
232 #[test]
233 fn displays_the_short_form() {
234 for input in ["1.10.12", "1.10.13-dev"] {
235 let version: FreeswitchVersion = input
236 .parse()
237 .unwrap();
238 assert_eq!(version.to_string(), input);
239 }
240 }
241
242 #[test]
243 fn malformed_versions_are_refused_without_quoting_them() {
244 for (input, fragment) in [
245 ("1.10", "1.10"),
246 ("1.10.x7", "x7"),
247 ("1.10.12-beta9", "beta9"),
248 ("1.10.12.4", "12.4"),
249 ("v1.10.12", "v1"),
250 ] {
251 let err = input
252 .parse::<FreeswitchVersion>()
253 .expect_err(input)
254 .to_string();
255 assert!(!err.contains(fragment), "error quoted {input}: {err}");
256 }
257 assert!(""
258 .parse::<FreeswitchVersion>()
259 .is_err());
260 }
261
262 #[cfg(feature = "serde")]
263 #[test]
264 fn serde_uses_the_short_form() {
265 let version = FreeswitchVersion::new(1, 10, 13).dev();
266 let json = serde_json::to_string(&version).unwrap();
267 assert_eq!(json, r#""1.10.13-dev""#);
268 let back: FreeswitchVersion = serde_json::from_str(&json).unwrap();
269 assert_eq!(back, version);
270 }
271}