1use std::fmt;
5
6use indexmap::IndexMap;
7use schemars::JsonSchema;
8use serde::de::{self, MapAccess, Visitor};
9use serde::{Deserialize, Deserializer};
10
11use super::step::Step;
12use crate::Error;
13use crate::model::Appliance;
14
15#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
17#[serde(try_from = "AppInput")]
18pub struct App {
19 pub container: Option<Recipe>,
21 pub steps: Vec<Step>,
23}
24
25#[derive(Debug, Clone, PartialEq, Eq)]
27pub enum Recipe {
28 Image(String),
30 Build(Build),
32 Appliance(ApplianceRecipe),
34}
35
36#[derive(Debug, Clone, PartialEq, Eq)]
38pub struct ApplianceRecipe {
39 pub kind: Appliance,
41 pub image: Option<String>,
43 pub config: Option<String>,
45 pub firmware: Option<String>,
47}
48
49#[derive(Debug, Clone, PartialEq, Eq, Deserialize, JsonSchema)]
51#[serde(deny_unknown_fields)]
52pub struct Build {
53 pub context: String,
55 #[serde(default)]
57 pub dockerfile: Option<String>,
58 #[serde(default)]
60 pub target: Option<String>,
61 #[serde(default)]
63 pub args: IndexMap<String, String>,
64}
65
66#[derive(Deserialize, JsonSchema)]
68#[serde(deny_unknown_fields)]
69struct AppInput {
70 #[serde(default)]
73 image: Option<String>,
74 #[serde(default, deserialize_with = "build")]
78 #[schemars(with = "Option<BuildInput>")]
79 build: Option<Build>,
80 #[serde(default)]
83 steps: Vec<Step>,
84 #[serde(default)]
87 appliance: Option<Appliance>,
88 #[serde(default)]
90 config: Option<String>,
91 #[serde(default)]
93 firmware: Option<String>,
94}
95
96#[derive(JsonSchema)]
99#[serde(untagged)]
100enum BuildInput {
101 Folder(String),
103 Full(Build),
105}
106
107impl<'de> Deserialize<'de> for BuildInput {
108 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<BuildInput, D::Error> {
109 struct V;
110 impl<'de> Visitor<'de> for V {
111 type Value = BuildInput;
112 fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
113 f.write_str("a project folder, or `{ context, dockerfile, target, args }`")
114 }
115 fn visit_str<E: de::Error>(self, v: &str) -> Result<BuildInput, E> {
116 Ok(BuildInput::Folder(v.to_owned()))
117 }
118 fn visit_map<A: MapAccess<'de>>(self, map: A) -> Result<BuildInput, A::Error> {
119 Build::deserialize(de::value::MapAccessDeserializer::new(map)).map(BuildInput::Full)
120 }
121 }
122 d.deserialize_any(V)
123 }
124}
125
126impl From<BuildInput> for Build {
127 fn from(written: BuildInput) -> Build {
128 match written {
129 BuildInput::Folder(context) => Build {
130 context,
131 dockerfile: None,
132 target: None,
133 args: IndexMap::new(),
134 },
135 BuildInput::Full(build) => build,
136 }
137 }
138}
139
140fn build<'de, D: Deserializer<'de>>(d: D) -> Result<Option<Build>, D::Error> {
141 BuildInput::deserialize(d).map(|written| Some(written.into()))
142}
143
144impl TryFrom<AppInput> for App {
145 type Error = Error;
146
147 fn try_from(a: AppInput) -> Result<App, Error> {
148 if a.config.is_some() && a.appliance.is_none() {
149 return Err(Error::ConfigWithoutAppliance);
150 }
151 let container = match (a.image, a.build, a.appliance) {
152 (Some(_), Some(_), _) => return Err(Error::ImageAndBuild),
153 (_, Some(_), Some(_)) => return Err(Error::ApplianceBuild),
154 (image, None, Some(kind)) => Some(Recipe::Appliance(appliance(kind, image, a.config, a.firmware)?)),
155 (_, _, None) if a.firmware.is_some() => return Err(Error::FirmwareWithoutDynamips),
156 (Some(image), None, None) => Some(Recipe::Image(image)),
157 (None, Some(b), None) => Some(Recipe::Build(b)),
158 (None, None, None) => None,
159 };
160 if container.is_none() && a.steps.is_empty() {
161 return Err(Error::NoRecipe);
162 }
163 Ok(App { container, steps: a.steps })
164 }
165}
166
167fn appliance(kind: Appliance, image: Option<String>, config: Option<String>, firmware: Option<String>) -> Result<ApplianceRecipe, Error> {
168 let dynamips = kind == Appliance::CiscoDynamips;
169 match (dynamips, image.is_some(), firmware.is_some()) {
170 (true, true, _) => Err(Error::DynamipsImage),
171 (true, false, false) => Err(Error::DynamipsFirmware),
172 (false, _, true) => Err(Error::ApplianceFirmware),
173 (false, false, false) => Err(Error::ApplianceImage),
174 (true, false, true) | (false, true, false) => Ok(ApplianceRecipe { kind, image, config, firmware }),
175 }
176}
177
178impl JsonSchema for App {
179 fn schema_name() -> std::borrow::Cow<'static, str> {
180 "App".into()
181 }
182
183 fn json_schema(g: &mut schemars::SchemaGenerator) -> schemars::Schema {
184 AppInput::json_schema(g)
185 }
186}
187
188#[cfg(test)]
189mod tests;