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
use serde::Deserialize;
use super::{StackId, StackIdError};
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct StackUnchecked {
pub id: String,
#[serde(default)]
pub mixins: Vec<String>,
}
#[derive(Deserialize, Debug, Eq, PartialEq)]
#[serde(try_from = "StackUnchecked")]
pub enum Stack {
Any,
Specific { id: StackId, mixins: Vec<String> },
}
impl TryFrom<StackUnchecked> for Stack {
type Error = StackError;
fn try_from(value: StackUnchecked) -> Result<Self, Self::Error> {
let StackUnchecked { id, mixins } = value;
if id.as_str() == "*" {
if mixins.is_empty() {
Ok(Stack::Any)
} else {
Err(Self::Error::InvalidAnyStack(mixins))
}
} else {
Ok(Stack::Specific {
id: id.parse()?,
mixins,
})
}
}
}
#[derive(thiserror::Error, Debug)]
pub enum StackError {
#[error("Stack with id `*` MUST NOT contain mixins, however the following mixins were specified: `{}`", .0.join("`, `"))]
InvalidAnyStack(Vec<String>),
#[error("Invalid Stack ID: {0}")]
InvalidStackId(#[from] StackIdError),
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn deserialize_specific_stack_without_mixins() {
let toml_str = r#"
id = "heroku-20"
"#;
assert_eq!(
toml::from_str::<Stack>(toml_str),
Ok(Stack::Specific {
id: "heroku-20".parse().unwrap(),
mixins: Vec::new()
}),
);
}
#[test]
fn deserialize_specific_stack_with_mixins() {
let toml_str = r#"
id = "io.buildpacks.stacks.focal"
mixins = ["build:jq", "wget"]
"#;
assert_eq!(
toml::from_str::<Stack>(toml_str),
Ok(Stack::Specific {
id: "io.buildpacks.stacks.focal".parse().unwrap(),
mixins: vec![String::from("build:jq"), String::from("wget")]
}),
);
}
#[test]
fn deserialize_any_stack() {
let toml_str = r#"
id = "*"
"#;
assert_eq!(toml::from_str::<Stack>(toml_str), Ok(Stack::Any));
}
#[test]
fn reject_specific_stack_with_invalid_name() {
let toml_str = r#"
id = "io.buildpacks.stacks.*"
"#;
let err = toml::from_str::<Stack>(toml_str).unwrap_err();
assert!(err
.to_string()
.contains("Invalid Stack ID: Invalid Value: io.buildpacks.stacks.*"));
}
#[test]
fn reject_any_stack_with_mixins() {
let toml_str = r#"
id = "*"
mixins = ["build:jq", "wget"]
"#;
let err = toml::from_str::<Stack>(toml_str).unwrap_err();
assert!(err
.to_string()
.contains("Stack with id `*` MUST NOT contain mixins, however the following mixins were specified: `build:jq`, `wget`"));
}
}