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 #[serde(skip)]
58 #[schemars(skip)]
59 pub image: Option<String>,
60 pub context: String,
62 #[serde(default)]
64 pub dockerfile: Option<String>,
65 #[serde(default)]
67 pub target: Option<String>,
68 #[serde(default)]
70 pub args: IndexMap<String, String>,
71}
72
73#[derive(Deserialize, JsonSchema)]
75#[serde(deny_unknown_fields)]
76struct AppInput {
77 #[serde(default)]
81 image: Option<String>,
82 #[serde(default, deserialize_with = "build")]
86 #[schemars(with = "Option<BuildInput>")]
87 build: Option<Build>,
88 #[serde(default)]
91 steps: Vec<Step>,
92 #[serde(default)]
95 appliance: Option<Appliance>,
96 #[serde(default)]
98 config: Option<String>,
99 #[serde(default)]
101 firmware: Option<String>,
102}
103
104#[derive(JsonSchema)]
107#[serde(untagged)]
108enum BuildInput {
109 Folder(String),
111 Full(Build),
113}
114
115impl<'de> Deserialize<'de> for BuildInput {
116 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<BuildInput, D::Error> {
117 struct V;
118 impl<'de> Visitor<'de> for V {
119 type Value = BuildInput;
120 fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
121 f.write_str("a project folder, or `{ context, dockerfile, target, args }`")
122 }
123 fn visit_str<E: de::Error>(self, v: &str) -> Result<BuildInput, E> {
124 Ok(BuildInput::Folder(v.to_owned()))
125 }
126 fn visit_map<A: MapAccess<'de>>(self, map: A) -> Result<BuildInput, A::Error> {
127 Build::deserialize(de::value::MapAccessDeserializer::new(map)).map(BuildInput::Full)
128 }
129 }
130 d.deserialize_any(V)
131 }
132}
133
134impl From<BuildInput> for Build {
135 fn from(written: BuildInput) -> Build {
136 match written {
137 BuildInput::Folder(context) => Build {
138 image: None,
139 context,
140 dockerfile: None,
141 target: None,
142 args: IndexMap::new(),
143 },
144 BuildInput::Full(build) => build,
145 }
146 }
147}
148
149fn build<'de, D: Deserializer<'de>>(d: D) -> Result<Option<Build>, D::Error> {
150 BuildInput::deserialize(d).map(|written| Some(written.into()))
151}
152
153impl TryFrom<AppInput> for App {
154 type Error = Error;
155
156 fn try_from(a: AppInput) -> Result<App, Error> {
157 if a.config.is_some() && a.appliance.is_none() {
158 return Err(Error::ConfigWithoutAppliance);
159 }
160 let container = match (a.image, a.build, a.appliance) {
161 (_, Some(_), Some(_)) => return Err(Error::ApplianceBuild),
162 (image, None, Some(kind)) => Some(Recipe::Appliance(appliance(kind, image, a.config, a.firmware)?)),
163 (_, _, None) if a.firmware.is_some() => return Err(Error::FirmwareWithoutDynamips),
164 (Some(image), None, None) => Some(Recipe::Image(image)),
165 (Some(image), Some(b), None) => Some(Recipe::Build(Build { image: Some(image), ..b })),
168 (None, Some(b), None) => Some(Recipe::Build(b)),
169 (None, None, None) => None,
170 };
171 if container.is_none() && a.steps.is_empty() {
172 return Err(Error::NoRecipe);
173 }
174 Ok(App { container, steps: a.steps })
175 }
176}
177
178fn appliance(kind: Appliance, image: Option<String>, config: Option<String>, firmware: Option<String>) -> Result<ApplianceRecipe, Error> {
179 let dynamips = kind == Appliance::CiscoDynamips;
180 match (dynamips, image.is_some(), firmware.is_some()) {
181 (true, true, _) => Err(Error::DynamipsImage),
182 (true, false, false) => Err(Error::DynamipsFirmware),
183 (false, _, true) => Err(Error::ApplianceFirmware),
184 (false, false, false) => Err(Error::ApplianceImage),
185 (true, false, true) | (false, true, false) => Ok(ApplianceRecipe { kind, image, config, firmware }),
186 }
187}
188
189impl JsonSchema for App {
190 fn schema_name() -> std::borrow::Cow<'static, str> {
191 "App".into()
192 }
193
194 fn json_schema(g: &mut schemars::SchemaGenerator) -> schemars::Schema {
195 AppInput::json_schema(g)
196 }
197}
198
199#[cfg(test)]
200mod tests;