1use semver::Version;
16use serde::{Deserialize, Serialize};
17use serde_ignored::Path;
18
19pub mod v3_0;
20pub mod v3_1;
21pub mod v3_2;
22pub mod v3_3;
23pub mod v3_4;
24pub mod v3_5;
25pub mod v3_6;
26
27type Result<T> = std::result::Result<T, Error>;
28
29#[derive(thiserror::Error, Debug)]
30#[non_exhaustive]
31pub enum Error {
32 #[error("serialization error: {0}")]
33 Serialization(#[from] serde_json::Error),
34 #[error("couldn't parse config version: {0}")]
35 InvalidVersion(#[from] semver::Error),
36 #[error("unsupported config version: {0}")]
37 UnknownVersion(Version),
38}
39
40#[derive(thiserror::Error, Debug)]
41#[non_exhaustive]
42pub enum Warning {
43 #[error("unused key: {0}")]
44 UnusedKey(String),
45}
46
47#[derive(Clone, Debug, PartialEq, Serialize)]
50#[serde(untagged)]
51#[non_exhaustive]
52pub enum Config {
53 V3_0(v3_0::Config),
54 V3_1(v3_1::Config),
55 V3_2(v3_2::Config),
56 V3_3(v3_3::Config),
57 V3_4(v3_4::Config),
58 V3_5(v3_5::Config),
59 V3_6(v3_6::Config),
60}
61
62impl Config {
63 pub fn parse_str(s: &str) -> Result<(Self, Vec<Warning>)> {
64 Self::parse_slice(s.as_bytes())
65 }
66
67 pub fn parse_slice(v: &[u8]) -> Result<(Self, Vec<Warning>)> {
68 let minimal: MinimalConfig = serde_json::from_slice(v)?;
69 let version = Version::parse(&minimal.ignition.version)?;
70 let mut warnings = Vec::new();
71 let parsed = if version == v3_0::VERSION {
73 Self::V3_0(parse_warn(v, &mut warnings)?)
74 } else if version == v3_1::VERSION {
75 Self::V3_1(parse_warn(v, &mut warnings)?)
76 } else if version == v3_2::VERSION {
77 Self::V3_2(parse_warn(v, &mut warnings)?)
78 } else if version == v3_3::VERSION {
79 Self::V3_3(parse_warn(v, &mut warnings)?)
80 } else if version == v3_4::VERSION {
81 Self::V3_4(parse_warn(v, &mut warnings)?)
82 } else if version == v3_5::VERSION {
83 Self::V3_5(parse_warn(v, &mut warnings)?)
84 } else if version == v3_6::VERSION {
85 Self::V3_6(parse_warn(v, &mut warnings)?)
86 } else {
87 return Err(Error::UnknownVersion(version));
88 };
89 Ok((parsed, warnings))
90 }
91}
92
93#[derive(Debug, Deserialize)]
94struct MinimalConfig {
95 ignition: MinimalIgnition,
96}
97
98#[derive(Debug, Deserialize)]
99struct MinimalIgnition {
100 version: String,
101}
102
103fn parse_warn<'de, T: Deserialize<'de>>(v: &'de [u8], warnings: &mut Vec<Warning>) -> Result<T> {
105 Ok(serde_ignored::deserialize(
106 &mut serde_json::Deserializer::from_slice(v),
107 |path| warnings.push(Warning::UnusedKey(path_string(&path))),
108 )?)
109}
110
111fn path_string(path: &Path) -> String {
115 use Path::*;
116 match path {
117 Root => "$".into(),
118 Seq { parent, index } => format!("{}.{}", path_string(parent), index),
119 Map { parent, key } => format!("{}.{}", path_string(parent), key),
120 Some { parent } => path_string(parent),
121 NewtypeStruct { parent } => path_string(parent),
122 NewtypeVariant { parent } => path_string(parent),
123 }
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129
130 #[test]
131 fn parse() {
132 assert!(matches!(
133 Config::parse_str("z").unwrap_err(),
134 Error::Serialization(_)
135 ));
136 assert!(matches!(
137 Config::parse_str("{}").unwrap_err(),
138 Error::Serialization(_)
139 ));
140 assert!(matches!(
141 Config::parse_str(r#"{"ignition": {"version": "z"}}"#).unwrap_err(),
142 Error::InvalidVersion(_)
143 ));
144 assert!(matches!(
145 Config::parse_str(r#"{"ignition": {"version": "2.0.0"}}"#).unwrap_err(),
146 Error::UnknownVersion(_)
147 ));
148 assert!(matches!(
149 Config::parse_str(r#"{"ignition": {"version": "3.0.0-experimental"}}"#).unwrap_err(),
150 Error::UnknownVersion(_)
151 ));
152
153 let mut expected = v3_0::Config::default();
154 expected
155 .storage
156 .get_or_insert_with(Default::default)
157 .files
158 .get_or_insert_with(Default::default)
159 .push(v3_0::File::new("/z".into()));
160 let (config, warnings) = Config::parse_str(
161 r#"{"ignition": {"version": "3.0.0"}, "storage": {"files": [{"path": "/z"}]}}"#,
162 )
163 .unwrap();
164 assert_eq!(config, Config::V3_0(expected));
165 assert!(warnings.is_empty());
166
167 let mut expected = v3_1::Config::default();
168 expected
169 .storage
170 .get_or_insert_with(Default::default)
171 .files
172 .get_or_insert_with(Default::default)
173 .push(v3_1::File::new("/z".into()));
174 let (config, warnings) = Config::parse_str(
175 r#"{"ignition": {"version": "3.1.0"}, "storage": {"files": [{"path": "/z"}]}}"#,
176 )
177 .unwrap();
178 assert_eq!(config, Config::V3_1(expected));
179 assert!(warnings.is_empty());
180
181 let mut expected = v3_2::Config::default();
182 expected
183 .storage
184 .get_or_insert_with(Default::default)
185 .files
186 .get_or_insert_with(Default::default)
187 .push(v3_2::File::new("/z".into()));
188 let (config, warnings) = Config::parse_str(
189 r#"{"ignition": {"version": "3.2.0"}, "storage": {"files": [{"path": "/z"}]}}"#,
190 )
191 .unwrap();
192 assert_eq!(config, Config::V3_2(expected));
193 assert!(warnings.is_empty());
194
195 let mut expected = v3_3::Config::default();
196 expected
197 .storage
198 .get_or_insert_with(Default::default)
199 .files
200 .get_or_insert_with(Default::default)
201 .push(v3_3::File::new("/z".into()));
202 let (config, warnings) = Config::parse_str(
203 r#"{"ignition": {"version": "3.3.0"}, "storage": {"files": [{"path": "/z"}]}}"#,
204 )
205 .unwrap();
206 assert_eq!(config, Config::V3_3(expected));
207 assert!(warnings.is_empty());
208
209 let mut expected = v3_4::Config::default();
210 expected
211 .storage
212 .get_or_insert_with(Default::default)
213 .files
214 .get_or_insert_with(Default::default)
215 .push(v3_4::File::new("/z".into()));
216 let (config, warnings) = Config::parse_str(
217 r#"{"ignition": {"version": "3.4.0"}, "storage": {"files": [{"path": "/z"}]}}"#,
218 )
219 .unwrap();
220 assert_eq!(config, Config::V3_4(expected));
221 assert!(warnings.is_empty());
222
223 let mut expected = v3_5::Config::default();
224 expected
225 .storage
226 .get_or_insert_with(Default::default)
227 .files
228 .get_or_insert_with(Default::default)
229 .push(v3_5::File::new("/z".into()));
230 let (config, warnings) = Config::parse_str(
231 r#"{"ignition": {"version": "3.5.0"}, "storage": {"files": [{"path": "/z"}]}}"#,
232 )
233 .unwrap();
234 assert_eq!(config, Config::V3_5(expected));
235 assert!(warnings.is_empty());
236
237 let mut expected = v3_6::Config::default();
238 expected
239 .storage
240 .get_or_insert_with(Default::default)
241 .files
242 .get_or_insert_with(Default::default)
243 .push(v3_6::File::new("/z".into()));
244 let (config, warnings) = Config::parse_str(
245 r#"{"ignition": {"version": "3.6.0"}, "storage": {"files": [{"path": "/z"}]}}"#,
246 )
247 .unwrap();
248 assert_eq!(config, Config::V3_6(expected));
249 assert!(warnings.is_empty());
250 }
251
252 #[test]
253 fn round_trip() {
254 let input = r#"{"ignition":{"version":"3.0.0"}}"#;
255 let (config, warnings) = Config::parse_str(input).unwrap();
256 assert_eq!(serde_json::to_string(&config).unwrap(), input);
257 assert!(warnings.is_empty());
258 }
259
260 #[test]
261 fn warnings() {
262 let (_, warnings) = Config::parse_str(
263 r#"{"ignition": {"version": "3.0.0"}, "a": {"y": "z"}, "b": 7, "c": null, "systemd": {"units": [{"name": "v", "d": "e"}]}}"#,
264 )
265 .unwrap();
266 assert_eq!(
267 warnings
268 .iter()
269 .map(|w| w.to_string())
270 .collect::<Vec<String>>(),
271 vec![
272 "unused key: $.a",
273 "unused key: $.b",
274 "unused key: $.c",
275 "unused key: $.systemd.units.0.d",
276 ]
277 );
278 }
279}