use std::{
fmt::{self, Display, Formatter},
path::PathBuf,
};
use serde::{de, ser::SerializeStruct, Deserialize, Deserializer, Serialize, Serializer};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Dockerfile {
File(PathBuf),
Inline(String),
}
impl Dockerfile {
const NAME: &'static str = "Dockerfile";
}
#[derive(Deserialize, Debug, Clone, Copy)]
#[serde(field_identifier, rename_all = "snake_case")]
enum Field {
Dockerfile,
DockerfileInline,
}
impl Field {
const fn as_str(self) -> &'static str {
match self {
Self::Dockerfile => "dockerfile",
Self::DockerfileInline => "dockerfile_inline",
}
}
}
impl From<&Dockerfile> for Field {
fn from(value: &Dockerfile) -> Self {
match value {
Dockerfile::File(_) => Self::Dockerfile,
Dockerfile::Inline(_) => Self::DockerfileInline,
}
}
}
impl Display for Field {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.write_str(self.as_str())
}
}
macro_rules! format_fields {
($args:literal) => {
format_args!($args, Field::Dockerfile, Field::DockerfileInline)
};
}
impl Serialize for Dockerfile {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let mut state = serializer.serialize_struct(Self::NAME, 1)?;
let key = Field::from(self).as_str();
match self {
Self::File(path) => state.serialize_field(key, path)?,
Self::Inline(string) => state.serialize_field(key, string)?,
}
state.end()
}
}
impl<'de> Deserialize<'de> for Dockerfile {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
option::deserialize(deserializer)?
.ok_or_else(|| de::Error::custom(format_fields!("missing required field `{}` or `{}`")))
}
}
pub(super) mod option {
use std::path::PathBuf;
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
use super::{Dockerfile, Field};
pub(in super::super) fn serialize<S: Serializer>(
value: &Option<Dockerfile>,
serializer: S,
) -> Result<S::Ok, S::Error> {
value.serialize(serializer)
}
pub(in super::super) fn deserialize<'de, D: Deserializer<'de>>(
deserializer: D,
) -> Result<Option<Dockerfile>, D::Error> {
let DockerfileFlat {
dockerfile,
dockerfile_inline,
} = DockerfileFlat::deserialize(deserializer)?;
match (dockerfile, dockerfile_inline) {
(Some(dockerfile), None) => Ok(Some(Dockerfile::File(dockerfile))),
(None, Some(dockerfile_inline)) => Ok(Some(Dockerfile::Inline(dockerfile_inline))),
(None, None) => Ok(None),
(Some(_), Some(_)) => Err(de::Error::custom(format_fields!(
"cannot set both `{}` and `{}`"
))),
}
}
#[derive(Deserialize)]
#[serde(
rename = "Dockerfile",
expecting = "a struct with either a `dockerfile` or `dockerfile_inline` field"
)]
struct DockerfileFlat {
#[serde(default)]
dockerfile: Option<PathBuf>,
#[serde(default)]
dockerfile_inline: Option<String>,
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn file() {
let dockerfile = Dockerfile::File("file".into());
let string = "dockerfile: file\n";
assert_eq!(dockerfile, serde_yaml::from_str(string).unwrap());
assert_eq!(serde_yaml::to_string(&dockerfile).unwrap(), string);
}
#[test]
fn inline() {
let dockerfile = Dockerfile::Inline("inline".into());
let string = "dockerfile_inline: inline\n";
assert_eq!(dockerfile, serde_yaml::from_str(string).unwrap());
assert_eq!(serde_yaml::to_string(&dockerfile).unwrap(), string);
}
#[test]
fn missing_err() {
assert!(serde_yaml::from_str::<Dockerfile>("{}")
.unwrap_err()
.to_string()
.contains("missing"));
}
#[test]
fn both_err() {
assert!(serde_yaml::from_str::<Dockerfile>(
"{ dockerfile: file, dockerfile_inline: inline }"
)
.unwrap_err()
.to_string()
.contains("both"));
}
#[derive(Deserialize, Debug)]
struct Test {
#[serde(flatten, with = "option")]
dockerfile: Option<Dockerfile>,
}
#[test]
fn flatten_option_none() {
assert_eq!(serde_yaml::from_str::<Test>("{}").unwrap().dockerfile, None);
}
#[test]
fn flatten_option_both_err() {
assert!(
serde_yaml::from_str::<Test>("{ dockerfile: file, dockerfile_inline: inline }")
.unwrap_err()
.to_string()
.contains("both")
);
}
}