pubgrub/
version.rs

1// SPDX-License-Identifier: MPL-2.0
2
3//! Traits and implementations to create and compare versions.
4
5use std::fmt::{self, Debug, Display};
6use std::str::FromStr;
7
8use thiserror::Error;
9
10/// Type for semantic versions: major.minor.patch.
11#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)]
12pub struct SemanticVersion {
13    major: u32,
14    minor: u32,
15    patch: u32,
16}
17
18#[cfg(feature = "serde")]
19impl serde::Serialize for SemanticVersion {
20    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
21    where
22        S: serde::Serializer,
23    {
24        serializer.serialize_str(&format!("{self}"))
25    }
26}
27
28#[cfg(feature = "serde")]
29impl<'de> serde::Deserialize<'de> for SemanticVersion {
30    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
31    where
32        D: serde::Deserializer<'de>,
33    {
34        let s = String::deserialize(deserializer)?;
35        FromStr::from_str(&s).map_err(serde::de::Error::custom)
36    }
37}
38
39// Constructors
40impl SemanticVersion {
41    /// Create a version with "major", "minor" and "patch" values.
42    /// `version = major.minor.patch`
43    pub fn new(major: u32, minor: u32, patch: u32) -> Self {
44        Self {
45            major,
46            minor,
47            patch,
48        }
49    }
50
51    /// Version 0.0.0.
52    pub fn zero() -> Self {
53        Self::new(0, 0, 0)
54    }
55
56    /// Version 1.0.0.
57    pub fn one() -> Self {
58        Self::new(1, 0, 0)
59    }
60
61    /// Version 2.0.0.
62    pub fn two() -> Self {
63        Self::new(2, 0, 0)
64    }
65}
66
67// Convert a tuple (major, minor, patch) into a version.
68impl From<(u32, u32, u32)> for SemanticVersion {
69    fn from(tuple: (u32, u32, u32)) -> Self {
70        let (major, minor, patch) = tuple;
71        Self::new(major, minor, patch)
72    }
73}
74
75// Convert a &(major, minor, patch) into a version.
76impl From<&(u32, u32, u32)> for SemanticVersion {
77    fn from(tuple: &(u32, u32, u32)) -> Self {
78        let (major, minor, patch) = *tuple;
79        Self::new(major, minor, patch)
80    }
81}
82
83// Convert an &version into a version.
84impl From<&SemanticVersion> for SemanticVersion {
85    fn from(v: &SemanticVersion) -> Self {
86        *v
87    }
88}
89
90// Convert a version into a tuple (major, minor, patch).
91impl From<SemanticVersion> for (u32, u32, u32) {
92    fn from(v: SemanticVersion) -> Self {
93        (v.major, v.minor, v.patch)
94    }
95}
96
97// Bump versions.
98impl SemanticVersion {
99    /// Bump the patch number of a version.
100    pub fn bump_patch(self) -> Self {
101        Self::new(self.major, self.minor, self.patch + 1)
102    }
103
104    /// Bump the minor number of a version.
105    pub fn bump_minor(self) -> Self {
106        Self::new(self.major, self.minor + 1, 0)
107    }
108
109    /// Bump the major number of a version.
110    pub fn bump_major(self) -> Self {
111        Self::new(self.major + 1, 0, 0)
112    }
113}
114
115/// Error creating [SemanticVersion] from [String].
116#[derive(Error, Debug, PartialEq, Eq)]
117pub enum VersionParseError {
118    /// [SemanticVersion] must contain major, minor, patch versions.
119    #[error("version {full_version} must contain 3 numbers separated by dot")]
120    NotThreeParts {
121        /// [SemanticVersion] that was being parsed.
122        full_version: String,
123    },
124    /// Wrapper around [ParseIntError](core::num::ParseIntError).
125    #[error("cannot parse '{version_part}' in '{full_version}' as u32: {parse_error}")]
126    ParseIntError {
127        /// [SemanticVersion] that was being parsed.
128        full_version: String,
129        /// A version part where parsing failed.
130        version_part: String,
131        /// A specific error resulted from parsing a part of the version as [u32].
132        parse_error: String,
133    },
134}
135
136impl FromStr for SemanticVersion {
137    type Err = VersionParseError;
138
139    fn from_str(s: &str) -> Result<Self, Self::Err> {
140        let parse_u32 = |part: &str| {
141            part.parse::<u32>().map_err(|e| Self::Err::ParseIntError {
142                full_version: s.to_string(),
143                version_part: part.to_string(),
144                parse_error: e.to_string(),
145            })
146        };
147
148        let mut parts = s.split('.');
149        match (parts.next(), parts.next(), parts.next(), parts.next()) {
150            (Some(major), Some(minor), Some(patch), None) => {
151                let major = parse_u32(major)?;
152                let minor = parse_u32(minor)?;
153                let patch = parse_u32(patch)?;
154                Ok(Self {
155                    major,
156                    minor,
157                    patch,
158                })
159            }
160            _ => Err(Self::Err::NotThreeParts {
161                full_version: s.to_string(),
162            }),
163        }
164    }
165}
166
167#[test]
168fn from_str_for_semantic_version() {
169    let parse = |str: &str| str.parse::<SemanticVersion>();
170    assert!(
171        parse(
172            &SemanticVersion {
173                major: 0,
174                minor: 1,
175                patch: 0
176            }
177            .to_string()
178        )
179        .is_ok()
180    );
181    assert!(parse("1.2.3").is_ok());
182    assert_eq!(
183        parse("1.abc.3"),
184        Err(VersionParseError::ParseIntError {
185            full_version: "1.abc.3".to_owned(),
186            version_part: "abc".to_owned(),
187            parse_error: "invalid digit found in string".to_owned(),
188        })
189    );
190    assert_eq!(
191        parse("1.2.-3"),
192        Err(VersionParseError::ParseIntError {
193            full_version: "1.2.-3".to_owned(),
194            version_part: "-3".to_owned(),
195            parse_error: "invalid digit found in string".to_owned(),
196        })
197    );
198    assert_eq!(
199        parse("1.2.9876543210"),
200        Err(VersionParseError::ParseIntError {
201            full_version: "1.2.9876543210".to_owned(),
202            version_part: "9876543210".to_owned(),
203            parse_error: "number too large to fit in target type".to_owned(),
204        })
205    );
206    assert_eq!(
207        parse("1.2"),
208        Err(VersionParseError::NotThreeParts {
209            full_version: "1.2".to_owned(),
210        })
211    );
212    assert_eq!(
213        parse("1.2.3."),
214        Err(VersionParseError::NotThreeParts {
215            full_version: "1.2.3.".to_owned(),
216        })
217    );
218}
219
220impl Display for SemanticVersion {
221    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
222        write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
223    }
224}