1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
use serde::Deserialize;
use std::convert::TryFrom;
use std::fmt;
use std::fmt::{Display, Formatter};

/// The Buildpack API version.
///
/// This MUST be in form `<major>.<minor>` or `<major>`, where `<major>` is equivalent to `<major>.0`.
#[derive(Deserialize, Debug, Eq, PartialEq)]
#[serde(try_from = "&str")]
pub struct BuildpackApi {
    pub major: u64,
    pub minor: u64,
}

impl TryFrom<&str> for BuildpackApi {
    type Error = BuildpackApiError;

    fn try_from(value: &str) -> Result<Self, Self::Error> {
        // We're not using the `semver` crate, since it only supports non-range versions of form `X.Y.Z`.
        // If no minor version is specified, it defaults to `0`.
        let (major, minor) = value.split_once('.').unwrap_or((value, "0"));
        Ok(Self {
            major: major
                .parse()
                .map_err(|_| Self::Error::InvalidBuildpackApi(String::from(value)))?,
            minor: minor
                .parse()
                .map_err(|_| Self::Error::InvalidBuildpackApi(String::from(value)))?,
        })
    }
}

impl Display for BuildpackApi {
    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
        formatter.write_str(&format!("{}.{}", self.major, self.minor))
    }
}

#[derive(thiserror::Error, Debug)]
pub enum BuildpackApiError {
    #[error("Invalid Buildpack API version: `{0}`")]
    InvalidBuildpackApi(String),
}

#[cfg(test)]
mod tests {
    use serde_test::{assert_de_tokens, assert_de_tokens_error, Token};

    use super::*;

    #[test]
    fn deserialize_valid_api_versions() {
        assert_de_tokens(
            &BuildpackApi { major: 1, minor: 3 },
            &[Token::BorrowedStr("1.3")],
        );
        assert_de_tokens(
            &BuildpackApi { major: 0, minor: 0 },
            &[Token::BorrowedStr("0.0")],
        );
        assert_de_tokens(
            &BuildpackApi {
                major: 2020,
                minor: 10,
            },
            &[Token::BorrowedStr("2020.10")],
        );
        assert_de_tokens(
            &BuildpackApi { major: 2, minor: 0 },
            &[Token::BorrowedStr("2")],
        );
    }

    #[test]
    fn reject_invalid_api_versions() {
        assert_de_tokens_error::<BuildpackApi>(
            &[Token::BorrowedStr("1.2.3")],
            "Invalid Buildpack API version: `1.2.3`",
        );
        assert_de_tokens_error::<BuildpackApi>(
            &[Token::BorrowedStr("1.2-dev")],
            "Invalid Buildpack API version: `1.2-dev`",
        );
        assert_de_tokens_error::<BuildpackApi>(
            &[Token::BorrowedStr("-1")],
            "Invalid Buildpack API version: `-1`",
        );
        assert_de_tokens_error::<BuildpackApi>(
            &[Token::BorrowedStr(".1")],
            "Invalid Buildpack API version: `.1`",
        );
        assert_de_tokens_error::<BuildpackApi>(
            &[Token::BorrowedStr("1.")],
            "Invalid Buildpack API version: `1.`",
        );
        assert_de_tokens_error::<BuildpackApi>(
            &[Token::BorrowedStr("1..2")],
            "Invalid Buildpack API version: `1..2`",
        );
        assert_de_tokens_error::<BuildpackApi>(
            &[Token::BorrowedStr("")],
            "Invalid Buildpack API version: ``",
        );
    }

    #[test]
    fn buildpack_api_display() {
        assert_eq!(BuildpackApi { major: 1, minor: 0 }.to_string(), "1.0");
        assert_eq!(BuildpackApi { major: 1, minor: 2 }.to_string(), "1.2");
        assert_eq!(
            BuildpackApi {
                major: 0,
                minor: 10
            }
            .to_string(),
            "0.10"
        );
    }
}