Skip to main content

freeswitch_types/
version.rs

1//! FreeSWITCH version an application states it targets.
2
3use std::cmp::Ordering;
4use std::fmt;
5use std::num::ParseIntError;
6use std::str::FromStr;
7
8/// A FreeSWITCH version in its short form, `1.10.12` or `1.10.13-dev`.
9///
10/// Supplied by the application from its own configuration; nothing in this
11/// crate reads it from the `FreeSWITCH-Version` event header.
12#[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    /// A release version.
22    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    /// The development build that precedes this release.
32    pub const fn dev(mut self) -> Self {
33        self.dev = true;
34        self
35    }
36
37    /// Major component.
38    pub fn major(&self) -> u32 {
39        self.major
40    }
41
42    /// Minor component.
43    pub fn minor(&self) -> u32 {
44        self.minor
45    }
46
47    /// Micro component.
48    pub fn micro(&self) -> u32 {
49        self.micro
50    }
51
52    /// Whether this is a `-dev` build.
53    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/// Why a string is not a FreeSWITCH short-form version. Names the part at fault,
130/// never the input.
131#[derive(Debug, Clone, PartialEq, Eq)]
132#[non_exhaustive]
133pub enum ParseFreeswitchVersionError {
134    /// A `major.minor.micro` component is absent; carries its name.
135    MissingComponent(&'static str),
136    /// A component is not an unsigned integer.
137    InvalidComponent {
138        /// Which component.
139        component: &'static str,
140        /// The integer parse failure.
141        source: ParseIntError,
142    },
143    /// More than three dot-separated components.
144    TrailingComponent,
145    /// A suffix other than `-dev` or `-release`.
146    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    /// A development build precedes the release it becomes, so a `-dev` version
224    /// must never sort above that release.
225    #[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}