1use indexmap::IndexMap;
2use serde::{Deserialize, Deserializer, de::DeserializeOwned};
3
4#[inline]
5pub fn deserialize_defaultable<'de, T, D>(deserializer: D) -> Result<T, D::Error>
6where
7 T: DeserializeOwned + Default,
8 D: Deserializer<'de>,
9{
10 let value = serde_json::Value::deserialize(deserializer)?;
11
12 Ok(serde_json::from_value(value).unwrap_or_default())
13}
14
15#[inline]
16pub fn deserialize_non_null_option<'de, T, D>(deserializer: D) -> Result<Option<T>, D::Error>
17where
18 T: Deserialize<'de>,
19 D: Deserializer<'de>,
20{
21 T::deserialize(deserializer).map(Some)
22}
23
24pub fn deserialize_stringable_option<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
25where
26 D: Deserializer<'de>,
27{
28 let value = serde_json::Value::deserialize(deserializer).unwrap_or_default();
29 Ok(match value {
30 serde_json::Value::Null => None,
31 serde_json::Value::String(string) if string.is_empty() => None,
32 serde_json::Value::String(string) => Some(string),
33 value => Some(value.to_string()),
34 })
35}
36
37pub fn deserialize_string_option<'de, D>(
38 deserializer: D,
39) -> Result<Option<compact_str::CompactString>, D::Error>
40where
41 D: Deserializer<'de>,
42{
43 let value: Option<compact_str::CompactString> =
44 Option::deserialize(deserializer).unwrap_or_default();
45 Ok(value.filter(|s| !s.is_empty()))
46}
47
48pub fn deserialize_search_option<'de, D>(
51 deserializer: D,
52) -> Result<Option<compact_str::CompactString>, D::Error>
53where
54 D: Deserializer<'de>,
55{
56 let value: Option<compact_str::CompactString> =
57 Option::deserialize(deserializer).unwrap_or_default();
58
59 Ok(value
60 .map(|s| match uuid::Uuid::parse_str(s.trim()) {
61 Ok(uuid) => compact_str::format_compact!("{uuid}"),
62 Err(_) => s.trim().into(),
63 })
64 .filter(|s| !s.is_empty()))
65}
66
67pub fn deserialize_array_or_not<'de, D, T: DeserializeOwned>(
68 deserializer: D,
69) -> Result<Vec<T>, D::Error>
70where
71 D: Deserializer<'de>,
72{
73 let value = serde_json::Value::deserialize(deserializer).unwrap_or_default();
74 let value: Vec<T> = match value {
75 serde_json::Value::Array(values) => {
76 serde_json::from_value(serde_json::Value::Array(values))
77 .map_err(serde::de::Error::custom)?
78 }
79 value => vec![serde_json::from_value(value).map_err(serde::de::Error::custom)?],
80 };
81
82 Ok(value)
83}
84
85pub fn deserialize_map_or_not<
86 'de,
87 D,
88 K: DeserializeOwned + std::hash::Hash + Eq,
89 V: DeserializeOwned,
90>(
91 deserializer: D,
92) -> Result<IndexMap<K, V>, D::Error>
93where
94 D: Deserializer<'de>,
95{
96 let value = serde_json::Value::deserialize(deserializer).unwrap_or_default();
97 let value: IndexMap<K, V> = match value {
98 serde_json::Value::Object(map) => serde_json::from_value(serde_json::Value::Object(map))
99 .map_err(serde::de::Error::custom)?,
100 value => {
101 let v = serde_json::from_value(value).map_err(serde::de::Error::custom)?;
102 let mut map = IndexMap::new();
103 map.insert(
104 K::deserialize(serde_json::Value::String("Default".into()))
105 .map_err(serde::de::Error::custom)?,
106 v,
107 );
108 map
109 }
110 };
111
112 Ok(value)
113}
114
115pub fn deserialize_pre_stringified<'de, D, T: DeserializeOwned>(
116 deserializer: D,
117) -> Result<T, D::Error>
118where
119 D: Deserializer<'de>,
120{
121 let value: serde_json::Value = serde_json::Value::deserialize(deserializer)?;
122 let value: T = match value {
123 serde_json::Value::String(value) => {
124 serde_json::from_str(&value).map_err(serde::de::Error::custom)?
125 }
126 value => serde_json::from_value(value).map_err(serde::de::Error::custom)?,
127 };
128
129 Ok(value)
130}
131
132pub fn deserialize_nest_egg_config_stop<'de, D>(
133 deserializer: D,
134) -> Result<crate::models::nest_egg::NestEggConfigStop, D::Error>
135where
136 D: Deserializer<'de>,
137{
138 let value: serde_json::Value = serde_json::Value::deserialize(deserializer)?;
139 let value: crate::models::nest_egg::NestEggConfigStop = match value {
140 serde_json::Value::String(value) => {
141 serde_json::from_str(&value).unwrap_or_else(|_| match value.strip_prefix('^') {
142 Some(signal) => crate::models::nest_egg::NestEggConfigStop {
143 r#type: "signal".into(),
144 value: Some(match signal.to_uppercase().as_str() {
145 "C" | "SIGINT" => "SIGINT".into(),
146 signal @ ("SIGABRT" | "SIGTERM" | "SIGQUIT" | "SIGKILL") => signal.into(),
147 _ => "SIGKILL".into(),
148 }),
149 },
150 None => crate::models::nest_egg::NestEggConfigStop {
151 r#type: "command".into(),
152 value: Some(value.into()),
153 },
154 })
155 }
156 value => serde_json::from_value(value).map_err(serde::de::Error::custom)?,
157 };
158
159 Ok(value)
160}
161
162fn true_fn() -> bool {
163 true
164}
165
166pub fn deserialize_nest_egg_config_files<'de, D>(
167 deserializer: D,
168) -> Result<
169 IndexMap<compact_str::CompactString, crate::models::nest_egg::ExportedNestEggConfigsFilesFile>,
170 D::Error,
171>
172where
173 D: Deserializer<'de>,
174{
175 let value: serde_json::Value = serde_json::Value::deserialize(deserializer)?;
176 let value: serde_json::Value = match value {
177 serde_json::Value::String(value) => serde_json::from_str(&value).unwrap_or_default(),
178 value => value,
179 };
180
181 #[derive(Deserialize, Clone)]
182 pub struct OldExportedNestEggConfigsFilesFile {
183 #[serde(default = "true_fn")]
184 pub create_file: bool,
185 pub parser: crate::models::nest_egg::ServerConfigurationFileParser,
186 pub find: IndexMap<compact_str::CompactString, serde_json::Value>,
187 }
188
189 if let Ok(value) = serde_json::from_value::<
190 IndexMap<compact_str::CompactString, OldExportedNestEggConfigsFilesFile>,
191 >(value.clone())
192 {
193 Ok(value
194 .into_iter()
195 .map(|(k, v)| {
196 (
197 k,
198 crate::models::nest_egg::ExportedNestEggConfigsFilesFile {
199 create_new: v.create_file,
200 parser: v.parser,
201 replace: v
202 .find
203 .into_iter()
204 .flat_map(|(k, replace_with)| {
205 let insert_new = !matches!(
206 v.parser,
207 crate::models::nest_egg::ServerConfigurationFileParser::File
208 );
209 let replacements = match replace_with {
210 serde_json::Value::Object(values) => values
211 .into_iter()
212 .map(|(if_value, replace_with)| {
213 (Some(if_value.into()), replace_with)
214 })
215 .collect(),
216 replace_with => vec![(None, replace_with)],
217 };
218
219 replacements.into_iter().map(move |(if_value, replace_with)| {
220 crate::models::nest_egg::ProcessConfigurationFileReplacement {
221 r#match: k.clone(),
222 insert_new,
223 update_existing: true,
224 if_value,
225 replace_with,
226 }
227 })
228 })
229 .collect(),
230 },
231 )
232 })
233 .collect())
234 } else {
235 serde_json::from_value(value).map_err(serde::de::Error::custom)
236 }
237}
238
239pub fn deserialize_nest_egg_variable_rules<'de, D>(
240 deserializer: D,
241) -> Result<Vec<compact_str::CompactString>, D::Error>
242where
243 D: Deserializer<'de>,
244{
245 let value: serde_json::Value = serde_json::Value::deserialize(deserializer)?;
246 let value: Vec<compact_str::CompactString> = match value {
247 serde_json::Value::String(value) => value.split('|').map(|v| v.into()).collect(),
248 value => serde_json::from_value(value).map_err(serde::de::Error::custom)?,
249 };
250
251 Ok(value)
252}
253
254pub fn deserialize_public_key<'de, D>(deserializer: D) -> Result<russh::keys::PublicKey, D::Error>
255where
256 D: Deserializer<'de>,
257{
258 let value = <&str>::deserialize(deserializer)?;
259 let public_key = russh::keys::PublicKey::from_openssh(value)
260 .map_err(|_| serde::de::Error::custom("invalid public key"))?;
261
262 Ok(public_key)
263}
264
265#[cfg(test)]
266mod tests {
267 use crate::models::nest_egg::ExportedNestEggConfigs;
268 use serde_json::json;
269
270 fn parse(value: serde_json::Value) -> ExportedNestEggConfigs {
271 serde_json::from_value(value).unwrap()
272 }
273
274 fn stop_of(stop: serde_json::Value) -> (String, Option<String>) {
275 let stop = parse(json!({ "stop": stop })).stop;
276 (stop.r#type.to_string(), stop.value.map(|v| v.to_string()))
277 }
278
279 #[test]
282 fn stop_legacy_caret_c_is_sigint() {
283 assert_eq!(
284 stop_of(json!("^C")),
285 ("signal".into(), Some("SIGINT".into()))
286 );
287 }
288
289 #[test]
290 fn stop_legacy_signal_is_uppercased() {
291 assert_eq!(
292 stop_of(json!("^sigterm")),
293 ("signal".into(), Some("SIGTERM".into()))
294 );
295 }
296
297 #[test]
298 fn stop_legacy_unknown_signal_falls_back_to_sigkill() {
299 assert_eq!(
300 stop_of(json!("^^C")),
301 ("signal".into(), Some("SIGKILL".into()))
302 );
303 assert_eq!(
304 stop_of(json!("^SIGHUP")),
305 ("signal".into(), Some("SIGKILL".into()))
306 );
307 }
308
309 #[test]
310 fn stop_plain_string_is_command() {
311 assert_eq!(
312 stop_of(json!("stop")),
313 ("command".into(), Some("stop".into()))
314 );
315 }
316
317 #[test]
318 fn stop_native_object_passes_through() {
319 assert_eq!(
320 stop_of(json!({ "type": "signal", "value": "SIGTERM" })),
321 ("signal".into(), Some("SIGTERM".into()))
322 );
323 }
324
325 #[test]
328 fn files_legacy_scalar_find_becomes_structured_replacement() {
329 let files = parse(json!({
330 "files": {
331 "server.properties": {
332 "parser": "properties",
333 "find": { "server-port": "{{server.build.default.port}}" }
334 }
335 }
336 }))
337 .files;
338
339 let file = &files["server.properties"];
340 assert!(file.create_new);
341 assert_eq!(
342 serde_json::to_value(file.parser).unwrap(),
343 json!("properties")
344 );
345 assert_eq!(file.replace.len(), 1);
346 let replacement = &file.replace[0];
347 assert_eq!(replacement.r#match, "server-port");
348 assert!(replacement.insert_new);
349 assert!(replacement.update_existing);
350 assert!(replacement.if_value.is_none());
351 assert_eq!(
352 replacement.replace_with,
353 json!("{{server.build.default.port}}")
354 );
355 }
356
357 #[test]
358 fn files_legacy_if_value_map_expands_in_order() {
359 let files = parse(json!({
360 "files": {
361 "config.yml": {
362 "parser": "yaml",
363 "find": {
364 "listeners[0].host": {
365 "0.0.0.0": "0.0.0.0:25565",
366 "127.0.0.1": "127.0.0.1:25565"
367 }
368 }
369 }
370 }
371 }))
372 .files;
373
374 let replace = &files["config.yml"].replace;
375 assert_eq!(replace.len(), 2);
376 for (replacement, (old, new)) in replace.iter().zip([
377 ("0.0.0.0", "0.0.0.0:25565"),
378 ("127.0.0.1", "127.0.0.1:25565"),
379 ]) {
380 assert_eq!(replacement.r#match, "listeners[0].host");
381 assert_eq!(replacement.if_value.as_deref(), Some(old));
382 assert_eq!(replacement.replace_with, json!(new));
383 assert!(replacement.update_existing);
384 }
385 }
386
387 #[test]
388 fn files_legacy_file_parser_does_not_insert_and_honours_create_file() {
389 let files = parse(json!({
390 "files": {
391 "eula.txt": {
392 "parser": "file",
393 "find": { "eula": "eula=true" },
394 "create_file": false
395 }
396 }
397 }))
398 .files;
399
400 let file = &files["eula.txt"];
401 assert!(!file.create_new);
402 let replacement = &file.replace[0];
403 assert!(!replacement.insert_new);
404 assert!(replacement.update_existing);
405 }
406
407 #[test]
408 fn files_legacy_accepts_pre_stringified_json() {
409 let legacy = json!({
410 "server.properties": {
411 "parser": "properties",
412 "find": { "server-ip": "0.0.0.0" }
413 }
414 })
415 .to_string();
416 let files = parse(json!({ "files": legacy })).files;
417
418 let replace = &files["server.properties"].replace;
419 assert_eq!(replace.len(), 1);
420 assert_eq!(replace[0].r#match, "server-ip");
421 assert_eq!(replace[0].replace_with, json!("0.0.0.0"));
422 }
423}