libcnb_data/buildpack/
api.rs

1use serde::Deserialize;
2use std::fmt;
3use std::fmt::{Display, Formatter};
4
5/// The Buildpack API version.
6///
7/// This MUST be in form `<major>.<minor>` or `<major>`, where `<major>` is equivalent to `<major>.0`.
8#[derive(Deserialize, Debug, Eq, PartialEq)]
9#[serde(try_from = "String")]
10pub struct BuildpackApi {
11    pub major: u64,
12    pub minor: u64,
13}
14
15impl TryFrom<String> for BuildpackApi {
16    type Error = BuildpackApiError;
17
18    fn try_from(value: String) -> Result<Self, Self::Error> {
19        // We're not using the `semver` crate, since it only supports non-range versions of form `X.Y.Z`.
20        // If no minor version is specified, it defaults to `0`.
21        let (major, minor) = &value.split_once('.').unwrap_or((&value, "0"));
22
23        Ok(Self {
24            major: major
25                .parse()
26                .map_err(|_| Self::Error::InvalidBuildpackApi(value.clone()))?,
27            minor: minor
28                .parse()
29                .map_err(|_| Self::Error::InvalidBuildpackApi(value.clone()))?,
30        })
31    }
32}
33
34impl Display for BuildpackApi {
35    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
36        formatter.write_str(&format!("{}.{}", self.major, self.minor))
37    }
38}
39
40#[derive(thiserror::Error, Debug)]
41pub enum BuildpackApiError {
42    #[error("Invalid Buildpack API version: `{0}`")]
43    InvalidBuildpackApi(String),
44}
45
46#[cfg(test)]
47mod tests {
48    use serde_test::{Token, assert_de_tokens, assert_de_tokens_error};
49
50    use super::*;
51
52    #[test]
53    fn deserialize_valid_api_versions() {
54        assert_de_tokens(
55            &BuildpackApi { major: 1, minor: 3 },
56            &[Token::BorrowedStr("1.3")],
57        );
58        assert_de_tokens(
59            &BuildpackApi { major: 0, minor: 0 },
60            &[Token::BorrowedStr("0.0")],
61        );
62        assert_de_tokens(
63            &BuildpackApi {
64                major: 2020,
65                minor: 10,
66            },
67            &[Token::BorrowedStr("2020.10")],
68        );
69        assert_de_tokens(
70            &BuildpackApi { major: 2, minor: 0 },
71            &[Token::BorrowedStr("2")],
72        );
73    }
74
75    #[test]
76    fn reject_invalid_api_versions() {
77        assert_de_tokens_error::<BuildpackApi>(
78            &[Token::BorrowedStr("1.2.3")],
79            "Invalid Buildpack API version: `1.2.3`",
80        );
81        assert_de_tokens_error::<BuildpackApi>(
82            &[Token::BorrowedStr("1.2-dev")],
83            "Invalid Buildpack API version: `1.2-dev`",
84        );
85        assert_de_tokens_error::<BuildpackApi>(
86            &[Token::BorrowedStr("-1")],
87            "Invalid Buildpack API version: `-1`",
88        );
89        assert_de_tokens_error::<BuildpackApi>(
90            &[Token::BorrowedStr(".1")],
91            "Invalid Buildpack API version: `.1`",
92        );
93        assert_de_tokens_error::<BuildpackApi>(
94            &[Token::BorrowedStr("1.")],
95            "Invalid Buildpack API version: `1.`",
96        );
97        assert_de_tokens_error::<BuildpackApi>(
98            &[Token::BorrowedStr("1..2")],
99            "Invalid Buildpack API version: `1..2`",
100        );
101        assert_de_tokens_error::<BuildpackApi>(
102            &[Token::BorrowedStr("")],
103            "Invalid Buildpack API version: ``",
104        );
105    }
106
107    #[test]
108    fn buildpack_api_display() {
109        assert_eq!(BuildpackApi { major: 1, minor: 0 }.to_string(), "1.0");
110        assert_eq!(BuildpackApi { major: 1, minor: 2 }.to_string(), "1.2");
111        assert_eq!(
112            BuildpackApi {
113                major: 0,
114                minor: 10
115            }
116            .to_string(),
117            "0.10"
118        );
119    }
120}