use std::fmt;
use indexmap::IndexMap;
use schemars::JsonSchema;
use serde::de::{self, MapAccess, Visitor};
use serde::{Deserialize, Deserializer};
use super::step::Step;
use crate::Error;
use crate::model::Appliance;
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(try_from = "AppInput")]
pub struct App {
pub container: Option<Recipe>,
pub steps: Vec<Step>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Recipe {
Image(String),
Build(Build),
Appliance(ApplianceRecipe),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ApplianceRecipe {
pub kind: Appliance,
pub image: Option<String>,
pub config: Option<String>,
pub firmware: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct Build {
pub context: String,
#[serde(default)]
pub dockerfile: Option<String>,
#[serde(default)]
pub target: Option<String>,
#[serde(default)]
pub args: IndexMap<String, String>,
}
#[derive(Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
struct AppInput {
#[serde(default)]
image: Option<String>,
#[serde(default, deserialize_with = "build")]
#[schemars(with = "Option<BuildInput>")]
build: Option<Build>,
#[serde(default)]
steps: Vec<Step>,
#[serde(default)]
appliance: Option<Appliance>,
#[serde(default)]
config: Option<String>,
#[serde(default)]
firmware: Option<String>,
}
#[derive(JsonSchema)]
#[serde(untagged)]
enum BuildInput {
Folder(String),
Full(Build),
}
impl<'de> Deserialize<'de> for BuildInput {
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<BuildInput, D::Error> {
struct V;
impl<'de> Visitor<'de> for V {
type Value = BuildInput;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str("a project folder, or `{ context, dockerfile, target, args }`")
}
fn visit_str<E: de::Error>(self, v: &str) -> Result<BuildInput, E> {
Ok(BuildInput::Folder(v.to_owned()))
}
fn visit_map<A: MapAccess<'de>>(self, map: A) -> Result<BuildInput, A::Error> {
Build::deserialize(de::value::MapAccessDeserializer::new(map)).map(BuildInput::Full)
}
}
d.deserialize_any(V)
}
}
impl From<BuildInput> for Build {
fn from(written: BuildInput) -> Build {
match written {
BuildInput::Folder(context) => Build {
context,
dockerfile: None,
target: None,
args: IndexMap::new(),
},
BuildInput::Full(build) => build,
}
}
}
fn build<'de, D: Deserializer<'de>>(d: D) -> Result<Option<Build>, D::Error> {
BuildInput::deserialize(d).map(|written| Some(written.into()))
}
impl TryFrom<AppInput> for App {
type Error = Error;
fn try_from(a: AppInput) -> Result<App, Error> {
if a.config.is_some() && a.appliance.is_none() {
return Err(Error::ConfigWithoutAppliance);
}
let container = match (a.image, a.build, a.appliance) {
(Some(_), Some(_), _) => return Err(Error::ImageAndBuild),
(_, Some(_), Some(_)) => return Err(Error::ApplianceBuild),
(image, None, Some(kind)) => Some(Recipe::Appliance(appliance(kind, image, a.config, a.firmware)?)),
(_, _, None) if a.firmware.is_some() => return Err(Error::FirmwareWithoutDynamips),
(Some(image), None, None) => Some(Recipe::Image(image)),
(None, Some(b), None) => Some(Recipe::Build(b)),
(None, None, None) => None,
};
if container.is_none() && a.steps.is_empty() {
return Err(Error::NoRecipe);
}
Ok(App { container, steps: a.steps })
}
}
fn appliance(kind: Appliance, image: Option<String>, config: Option<String>, firmware: Option<String>) -> Result<ApplianceRecipe, Error> {
let dynamips = kind == Appliance::CiscoDynamips;
match (dynamips, image.is_some(), firmware.is_some()) {
(true, true, _) => Err(Error::DynamipsImage),
(true, false, false) => Err(Error::DynamipsFirmware),
(false, _, true) => Err(Error::ApplianceFirmware),
(false, false, false) => Err(Error::ApplianceImage),
(true, false, true) | (false, true, false) => Ok(ApplianceRecipe { kind, image, config, firmware }),
}
}
impl JsonSchema for App {
fn schema_name() -> std::borrow::Cow<'static, str> {
"App".into()
}
fn json_schema(g: &mut schemars::SchemaGenerator) -> schemars::Schema {
AppInput::json_schema(g)
}
}
#[cfg(test)]
mod tests;