libcnb_data/buildpack/
version.rs

1use serde::Deserialize;
2use std::fmt;
3use std::fmt::{Display, Formatter};
4
5/// The Buildpack version.
6///
7/// This MUST be in the form `<X>.<Y>.<Z>` where `X`, `Y`, and `Z` are non-negative integers
8/// and must not contain leading zeros.
9#[derive(Deserialize, Debug, Eq, PartialEq)]
10#[serde(try_from = "String")]
11pub struct BuildpackVersion {
12    pub major: u64,
13    pub minor: u64,
14    pub patch: u64,
15}
16
17impl BuildpackVersion {
18    #[must_use]
19    pub fn new(major: u64, minor: u64, patch: u64) -> Self {
20        Self {
21            major,
22            minor,
23            patch,
24        }
25    }
26}
27
28impl TryFrom<String> for BuildpackVersion {
29    type Error = BuildpackVersionError;
30
31    fn try_from(value: String) -> Result<Self, Self::Error> {
32        // We're not using the `semver` crate, since semver versions also permit pre-release and
33        // build metadata suffixes, which are not valid in buildpack versions.
34        match value
35            .split('.')
36            .map(|s| {
37                // The spec forbids redundant leading zeros.
38                if s.starts_with('0') && s != "0" {
39                    None
40                } else {
41                    s.parse().ok()
42                }
43            })
44            .collect::<Option<Vec<_>>>()
45            .unwrap_or_default()
46            .as_slice()
47        {
48            &[major, minor, patch] => Ok(Self::new(major, minor, patch)),
49            _ => Err(Self::Error::InvalidBuildpackVersion(value)),
50        }
51    }
52}
53
54impl Display for BuildpackVersion {
55    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
56        formatter.write_str(&format!("{}.{}.{}", self.major, self.minor, self.patch))
57    }
58}
59
60#[derive(thiserror::Error, Debug)]
61pub enum BuildpackVersionError {
62    #[error("Invalid buildpack version: `{0}`")]
63    InvalidBuildpackVersion(String),
64}
65
66#[cfg(test)]
67mod tests {
68    use serde_test::{Token, assert_de_tokens, assert_de_tokens_error};
69
70    use super::*;
71
72    #[test]
73    fn deserialize_valid_versions() {
74        assert_de_tokens(
75            &BuildpackVersion {
76                major: 1,
77                minor: 3,
78                patch: 4,
79            },
80            &[Token::BorrowedStr("1.3.4")],
81        );
82        assert_de_tokens(
83            &BuildpackVersion {
84                major: 0,
85                minor: 0,
86                patch: 0,
87            },
88            &[Token::BorrowedStr("0.0.0")],
89        );
90        assert_de_tokens(
91            &BuildpackVersion {
92                major: 1234,
93                minor: 5678,
94                patch: 9876,
95            },
96            &[Token::BorrowedStr("1234.5678.9876")],
97        );
98    }
99
100    #[test]
101    fn reject_wrong_number_of_version_parts() {
102        assert_de_tokens_error::<BuildpackVersion>(
103            &[Token::BorrowedStr("")],
104            "Invalid buildpack version: ``",
105        );
106        assert_de_tokens_error::<BuildpackVersion>(
107            &[Token::BorrowedStr("12345")],
108            "Invalid buildpack version: `12345`",
109        );
110        assert_de_tokens_error::<BuildpackVersion>(
111            &[Token::BorrowedStr("1.2")],
112            "Invalid buildpack version: `1.2`",
113        );
114        assert_de_tokens_error::<BuildpackVersion>(
115            &[Token::BorrowedStr("1.2.3.4")],
116            "Invalid buildpack version: `1.2.3.4`",
117        );
118        assert_de_tokens_error::<BuildpackVersion>(
119            &[Token::BorrowedStr(".2.3")],
120            "Invalid buildpack version: `.2.3`",
121        );
122        assert_de_tokens_error::<BuildpackVersion>(
123            &[Token::BorrowedStr("1.2.")],
124            "Invalid buildpack version: `1.2.`",
125        );
126        assert_de_tokens_error::<BuildpackVersion>(
127            &[Token::BorrowedStr("1..3")],
128            "Invalid buildpack version: `1..3`",
129        );
130        assert_de_tokens_error::<BuildpackVersion>(
131            &[Token::BorrowedStr("1..2.3")],
132            "Invalid buildpack version: `1..2.3`",
133        );
134        assert_de_tokens_error::<BuildpackVersion>(
135            &[Token::BorrowedStr("1.2_3")],
136            "Invalid buildpack version: `1.2_3`",
137        );
138    }
139
140    #[test]
141    fn reject_version_suffixes() {
142        // These are valid semver, but not a valid buildpack version.
143        assert_de_tokens_error::<BuildpackVersion>(
144            &[Token::BorrowedStr("1.2.3-dev")],
145            "Invalid buildpack version: `1.2.3-dev`",
146        );
147        assert_de_tokens_error::<BuildpackVersion>(
148            &[Token::BorrowedStr("1.2.3+abc")],
149            "Invalid buildpack version: `1.2.3+abc`",
150        );
151    }
152
153    #[test]
154    fn reject_negative_versions() {
155        assert_de_tokens_error::<BuildpackVersion>(
156            &[Token::BorrowedStr("-1.2.3")],
157            "Invalid buildpack version: `-1.2.3`",
158        );
159        assert_de_tokens_error::<BuildpackVersion>(
160            &[Token::BorrowedStr("1.-2.3")],
161            "Invalid buildpack version: `1.-2.3`",
162        );
163        assert_de_tokens_error::<BuildpackVersion>(
164            &[Token::BorrowedStr("1.2.-3")],
165            "Invalid buildpack version: `1.2.-3`",
166        );
167    }
168
169    #[test]
170    fn reject_versions_with_leading_zeros() {
171        assert_de_tokens_error::<BuildpackVersion>(
172            &[Token::BorrowedStr("01.2.3")],
173            "Invalid buildpack version: `01.2.3`",
174        );
175        assert_de_tokens_error::<BuildpackVersion>(
176            &[Token::BorrowedStr("1.00.3")],
177            "Invalid buildpack version: `1.00.3`",
178        );
179        assert_de_tokens_error::<BuildpackVersion>(
180            &[Token::BorrowedStr("1.2.030")],
181            "Invalid buildpack version: `1.2.030`",
182        );
183    }
184
185    #[test]
186    fn buildpack_version_display() {
187        assert_eq!(
188            BuildpackVersion {
189                major: 0,
190                minor: 1,
191                patch: 2,
192            }
193            .to_string(),
194            "0.1.2"
195        );
196        assert_eq!(
197            BuildpackVersion {
198                major: 2000,
199                minor: 10,
200                patch: 20,
201            }
202            .to_string(),
203            "2000.10.20"
204        );
205    }
206}