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