use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, JsonSchema)]
#[serde(untagged)]
pub enum Mount {
Path(String),
Part(MountPart),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct MountPart {
pub path: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub subpath: Option<String>,
}
impl Mount {
#[must_use]
pub fn path(&self) -> &str {
match self {
Mount::Path(p) => p,
Mount::Part(m) => &m.path,
}
}
#[must_use]
pub fn subpath(&self) -> Option<&str> {
match self {
Mount::Path(_) => None,
Mount::Part(m) => m.subpath.as_deref(),
}
}
}
impl From<&str> for Mount {
fn from(path: &str) -> Mount {
Mount::Path(path.to_owned())
}
}
impl<'de> Deserialize<'de> for Mount {
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
struct V;
impl<'de> serde::de::Visitor<'de> for V {
type Value = Mount;
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.write_str("an absolute path, or `{ path, subpath }`")
}
fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Mount, E> {
Ok(Mount::Path(v.to_owned()))
}
fn visit_map<A: serde::de::MapAccess<'de>>(self, map: A) -> Result<Mount, A::Error> {
MountPart::deserialize(serde::de::value::MapAccessDeserializer::new(map)).map(Mount::Part)
}
}
d.deserialize_any(V)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, JsonSchema)]
#[serde(untagged)]
pub enum Tmpfs {
Path(String),
Sized(TmpfsMount),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct TmpfsMount {
pub path: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub size: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub mode: Option<String>,
}
impl Tmpfs {
#[must_use]
pub fn path(&self) -> &str {
match self {
Tmpfs::Path(p) => p,
Tmpfs::Sized(t) => &t.path,
}
}
#[must_use]
pub fn size(&self) -> Option<&str> {
match self {
Tmpfs::Path(_) => None,
Tmpfs::Sized(t) => t.size.as_deref(),
}
}
#[must_use]
pub fn mode(&self) -> Option<&str> {
match self {
Tmpfs::Path(_) => None,
Tmpfs::Sized(t) => t.mode.as_deref(),
}
}
}
impl From<&str> for Tmpfs {
fn from(path: &str) -> Tmpfs {
Tmpfs::Path(path.to_owned())
}
}
impl<'de> Deserialize<'de> for Tmpfs {
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
struct V;
impl<'de> serde::de::Visitor<'de> for V {
type Value = Tmpfs;
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.write_str("an absolute path, or `{ path, size, mode }`")
}
fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Tmpfs, E> {
Ok(Tmpfs::Path(v.to_owned()))
}
fn visit_map<A: serde::de::MapAccess<'de>>(self, map: A) -> Result<Tmpfs, A::Error> {
TmpfsMount::deserialize(serde::de::value::MapAccessDeserializer::new(map)).map(Tmpfs::Sized)
}
}
d.deserialize_any(V)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct Disk {
pub size_gb: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub fs: Option<Filesystem>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub mount: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "lowercase")]
pub enum Filesystem {
Ext4,
Xfs,
Ntfs,
Raw,
}
impl Filesystem {
#[must_use]
pub fn id(self) -> &'static str {
match self {
Filesystem::Ext4 => "ext4",
Filesystem::Xfs => "xfs",
Filesystem::Ntfs => "ntfs",
Filesystem::Raw => "raw",
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn refusal<T: serde::de::DeserializeOwned>(yaml: &str) -> String {
serde_yaml_ng::from_str::<T>(yaml).map(|_| ()).err().map(|e| e.to_string()).unwrap_or_default()
}
#[test]
fn a_mount_is_a_path_or_a_folder_of_a_shared_volume() -> Result<(), serde_yaml_ng::Error> {
let whole: Mount = serde_yaml_ng::from_str("/srv")?;
assert_eq!((whole.path(), whole.subpath()), ("/srv", None));
let part: Mount = serde_yaml_ng::from_str("{ path: /srv/www, subpath: www }")?;
assert_eq!((part.path(), part.subpath()), ("/srv/www", Some("www")));
assert!(refusal::<Mount>("3").ends_with("expected an absolute path, or `{ path, subpath }`"));
Ok(())
}
#[test]
fn a_tmpfs_is_a_path_or_one_with_its_size_and_mode() -> Result<(), serde_yaml_ng::Error> {
let bare: Tmpfs = serde_yaml_ng::from_str("/run")?;
assert_eq!((bare.path(), bare.size(), bare.mode()), ("/run", None, None));
let sized: Tmpfs = serde_yaml_ng::from_str("{ path: /tmp, size: 64m, mode: \"1777\" }")?;
assert_eq!((sized.path(), sized.size(), sized.mode()), ("/tmp", Some("64m"), Some("1777")));
assert!(refusal::<Tmpfs>("3").ends_with("expected an absolute path, or `{ path, size, mode }`"));
Ok(())
}
#[test]
fn a_filesystem_goes_by_the_name_a_spec_writes() -> Result<(), serde_yaml_ng::Error> {
for fs in [Filesystem::Ext4, Filesystem::Xfs, Filesystem::Ntfs, Filesystem::Raw] {
assert_eq!(serde_yaml_ng::from_str::<Filesystem>(fs.id())?, fs);
}
Ok(())
}
}