Skip to main content

holodeck_simctl_core/models/
semantic_version.rs

1use std::cmp::Ordering;
2use std::fmt;
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
5pub struct SemanticVersion {
6    pub major: i64,
7    pub minor: i64,
8    pub patch: i64,
9}
10
11impl SemanticVersion {
12    pub fn new(major: i64, minor: i64, patch: i64) -> Self {
13        Self { major, minor, patch }
14    }
15
16    /// Lenient parse: requires a parseable major component; an unparseable
17    /// minor or patch silently becomes 0 (matching the Swift `init?(string:)`).
18    pub fn parse(string: &str) -> Option<Self> {
19        let parts: Vec<&str> = string.split('.').collect();
20        let major = parts.first()?.parse::<i64>().ok()?;
21        let minor = parts.get(1).and_then(|p| p.parse::<i64>().ok()).unwrap_or(0);
22        let patch = parts.get(2).and_then(|p| p.parse::<i64>().ok()).unwrap_or(0);
23        Some(Self { major, minor, patch })
24    }
25}
26
27impl PartialOrd for SemanticVersion {
28    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
29        Some(self.cmp(other))
30    }
31}
32
33impl Ord for SemanticVersion {
34    fn cmp(&self, other: &Self) -> Ordering {
35        self.major.cmp(&other.major).then(self.minor.cmp(&other.minor)).then(self.patch.cmp(&other.patch))
36    }
37}
38
39impl fmt::Display for SemanticVersion {
40    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41        if self.patch == 0 {
42            write!(f, "{}.{}", self.major, self.minor)
43        } else {
44            write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
45        }
46    }
47}
48
49#[cfg(test)]
50mod tests {
51    use super::*;
52
53    #[test]
54    fn parses_major_minor_patch() {
55        assert_eq!(SemanticVersion::parse("18.4.1"), Some(SemanticVersion::new(18, 4, 1)));
56    }
57
58    #[test]
59    fn defaults_missing_components_to_zero() {
60        assert_eq!(SemanticVersion::parse("18"), Some(SemanticVersion::new(18, 0, 0)));
61    }
62
63    #[test]
64    fn rejects_unparseable_major() {
65        assert_eq!(SemanticVersion::parse("x.4"), None);
66    }
67
68    #[test]
69    fn display_omits_zero_patch() {
70        assert_eq!(SemanticVersion::new(26, 4, 0).to_string(), "26.4");
71        assert_eq!(SemanticVersion::new(26, 4, 1).to_string(), "26.4.1");
72    }
73
74    #[test]
75    fn orders_by_major_then_minor_then_patch() {
76        assert!(SemanticVersion::new(18, 0, 0) < SemanticVersion::new(18, 1, 0));
77        assert!(SemanticVersion::new(17, 9, 9) < SemanticVersion::new(18, 0, 0));
78    }
79}