1use schemars::JsonSchema;
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, PartialEq, Eq, Serialize, JsonSchema)]
10#[serde(untagged)]
11pub enum Mount {
12 Path(String),
14 Part(MountPart),
16}
17
18#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
20#[serde(deny_unknown_fields)]
21pub struct MountPart {
22 pub path: String,
24 #[serde(default, skip_serializing_if = "Option::is_none")]
27 pub subpath: Option<String>,
28}
29
30impl Mount {
31 #[must_use]
33 pub fn path(&self) -> &str {
34 match self {
35 Mount::Path(p) => p,
36 Mount::Part(m) => &m.path,
37 }
38 }
39
40 #[must_use]
42 pub fn subpath(&self) -> Option<&str> {
43 match self {
44 Mount::Path(_) => None,
45 Mount::Part(m) => m.subpath.as_deref(),
46 }
47 }
48}
49
50impl From<&str> for Mount {
51 fn from(path: &str) -> Mount {
52 Mount::Path(path.to_owned())
53 }
54}
55
56impl<'de> Deserialize<'de> for Mount {
59 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
60 struct V;
61 impl<'de> serde::de::Visitor<'de> for V {
62 type Value = Mount;
63 fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
64 f.write_str("an absolute path, or `{ path, subpath }`")
65 }
66 fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Mount, E> {
67 Ok(Mount::Path(v.to_owned()))
68 }
69 fn visit_map<A: serde::de::MapAccess<'de>>(self, map: A) -> Result<Mount, A::Error> {
70 MountPart::deserialize(serde::de::value::MapAccessDeserializer::new(map)).map(Mount::Part)
71 }
72 }
73 d.deserialize_any(V)
74 }
75}
76
77#[derive(Debug, Clone, PartialEq, Eq, Serialize, JsonSchema)]
80#[serde(untagged)]
81pub enum Tmpfs {
82 Path(String),
84 Sized(TmpfsMount),
86}
87
88#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
90#[serde(deny_unknown_fields)]
91pub struct TmpfsMount {
92 pub path: String,
94 #[serde(default, skip_serializing_if = "Option::is_none")]
96 pub size: Option<String>,
97 #[serde(default, skip_serializing_if = "Option::is_none")]
100 pub mode: Option<String>,
101}
102
103impl Tmpfs {
104 #[must_use]
106 pub fn path(&self) -> &str {
107 match self {
108 Tmpfs::Path(p) => p,
109 Tmpfs::Sized(t) => &t.path,
110 }
111 }
112
113 #[must_use]
115 pub fn size(&self) -> Option<&str> {
116 match self {
117 Tmpfs::Path(_) => None,
118 Tmpfs::Sized(t) => t.size.as_deref(),
119 }
120 }
121
122 #[must_use]
124 pub fn mode(&self) -> Option<&str> {
125 match self {
126 Tmpfs::Path(_) => None,
127 Tmpfs::Sized(t) => t.mode.as_deref(),
128 }
129 }
130}
131
132impl From<&str> for Tmpfs {
133 fn from(path: &str) -> Tmpfs {
134 Tmpfs::Path(path.to_owned())
135 }
136}
137
138impl<'de> Deserialize<'de> for Tmpfs {
139 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
140 struct V;
141 impl<'de> serde::de::Visitor<'de> for V {
142 type Value = Tmpfs;
143 fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
144 f.write_str("an absolute path, or `{ path, size, mode }`")
145 }
146 fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Tmpfs, E> {
147 Ok(Tmpfs::Path(v.to_owned()))
148 }
149 fn visit_map<A: serde::de::MapAccess<'de>>(self, map: A) -> Result<Tmpfs, A::Error> {
150 TmpfsMount::deserialize(serde::de::value::MapAccessDeserializer::new(map)).map(Tmpfs::Sized)
151 }
152 }
153 d.deserialize_any(V)
154 }
155}
156
157#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
161#[serde(deny_unknown_fields)]
162pub struct Disk {
163 pub size_gb: u32,
165 #[serde(default, skip_serializing_if = "Option::is_none")]
168 pub fs: Option<Filesystem>,
169 #[serde(default, skip_serializing_if = "Option::is_none")]
172 pub mount: Option<String>,
173}
174
175#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
177#[serde(rename_all = "lowercase")]
178pub enum Filesystem {
179 Ext4,
181 Xfs,
183 Ntfs,
185 Raw,
187}
188
189impl Filesystem {
190 #[must_use]
192 pub fn id(self) -> &'static str {
193 match self {
194 Filesystem::Ext4 => "ext4",
195 Filesystem::Xfs => "xfs",
196 Filesystem::Ntfs => "ntfs",
197 Filesystem::Raw => "raw",
198 }
199 }
200}
201
202#[cfg(test)]
203mod tests {
204 use super::*;
205
206 fn refusal<T: serde::de::DeserializeOwned>(yaml: &str) -> String {
208 serde_yaml_ng::from_str::<T>(yaml).map(|_| ()).err().map(|e| e.to_string()).unwrap_or_default()
209 }
210
211 #[test]
212 fn a_mount_is_a_path_or_a_folder_of_a_shared_volume() -> Result<(), serde_yaml_ng::Error> {
213 let whole: Mount = serde_yaml_ng::from_str("/srv")?;
214 assert_eq!((whole.path(), whole.subpath()), ("/srv", None));
215 let part: Mount = serde_yaml_ng::from_str("{ path: /srv/www, subpath: www }")?;
216 assert_eq!((part.path(), part.subpath()), ("/srv/www", Some("www")));
217 assert!(refusal::<Mount>("3").ends_with("expected an absolute path, or `{ path, subpath }`"));
218 Ok(())
219 }
220
221 #[test]
222 fn a_tmpfs_is_a_path_or_one_with_its_size_and_mode() -> Result<(), serde_yaml_ng::Error> {
223 let bare: Tmpfs = serde_yaml_ng::from_str("/run")?;
224 assert_eq!((bare.path(), bare.size(), bare.mode()), ("/run", None, None));
225 let sized: Tmpfs = serde_yaml_ng::from_str("{ path: /tmp, size: 64m, mode: \"1777\" }")?;
226 assert_eq!((sized.path(), sized.size(), sized.mode()), ("/tmp", Some("64m"), Some("1777")));
227 assert!(refusal::<Tmpfs>("3").ends_with("expected an absolute path, or `{ path, size, mode }`"));
228 Ok(())
229 }
230
231 #[test]
232 fn a_filesystem_goes_by_the_name_a_spec_writes() -> Result<(), serde_yaml_ng::Error> {
233 for fs in [Filesystem::Ext4, Filesystem::Xfs, Filesystem::Ntfs, Filesystem::Raw] {
234 assert_eq!(serde_yaml_ng::from_str::<Filesystem>(fs.id())?, fs);
235 }
236 Ok(())
237 }
238}