1#![doc = include_str!("../README.md")]
2
3use serde::{Deserialize, Deserializer, Serialize, Serializer};
4use std::fmt::Write as FmtWrite;
5use std::io::BufRead;
6
7#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
8#[serde(rename = "Task")]
9pub struct Task {
10 #[serde(rename = "@type")]
11 pub task_type: TaskType,
12
13 #[serde(
14 rename = "@aat_min_time",
15 default,
16 skip_serializing_if = "Option::is_none"
17 )]
18 pub aat_min_time: Option<u32>,
19
20 #[serde(
21 rename = "@start_requires_arm",
22 default,
23 deserialize_with = "de_opt_bool",
24 serialize_with = "ser_opt_bool",
25 skip_serializing_if = "Option::is_none"
26 )]
27 pub start_requires_arm: Option<bool>,
28
29 #[serde(
30 rename = "@start_score_exit",
31 default,
32 deserialize_with = "de_opt_bool",
33 serialize_with = "ser_opt_bool",
34 skip_serializing_if = "Option::is_none"
35 )]
36 pub start_score_exit: Option<bool>,
37
38 #[serde(
39 rename = "@start_max_speed",
40 default,
41 deserialize_with = "de_opt_f64",
42 serialize_with = "ser_opt_f64",
43 skip_serializing_if = "Option::is_none"
44 )]
45 pub start_max_speed: Option<f64>,
46
47 #[serde(
48 rename = "@start_max_height",
49 default,
50 skip_serializing_if = "Option::is_none"
51 )]
52 pub start_max_height: Option<u32>,
53
54 #[serde(
55 rename = "@start_max_height_ref",
56 default,
57 skip_serializing_if = "Option::is_none"
58 )]
59 pub start_max_height_ref: Option<AltitudeReference>,
60
61 #[serde(
62 rename = "@start_open_time",
63 default,
64 skip_serializing_if = "Option::is_none"
65 )]
66 pub start_open_time: Option<u32>,
67
68 #[serde(
69 rename = "@start_close_time",
70 default,
71 skip_serializing_if = "Option::is_none"
72 )]
73 pub start_close_time: Option<u32>,
74
75 #[serde(
76 rename = "@finish_min_height",
77 default,
78 skip_serializing_if = "Option::is_none"
79 )]
80 pub finish_min_height: Option<u32>,
81
82 #[serde(
83 rename = "@finish_min_height_ref",
84 default,
85 skip_serializing_if = "Option::is_none"
86 )]
87 pub finish_min_height_ref: Option<AltitudeReference>,
88
89 #[serde(
90 rename = "@fai_finish",
91 default,
92 deserialize_with = "de_opt_bool",
93 serialize_with = "ser_opt_bool",
94 skip_serializing_if = "Option::is_none"
95 )]
96 pub fai_finish: Option<bool>,
97
98 #[serde(
99 rename = "@pev_start_wait_time",
100 default,
101 skip_serializing_if = "Option::is_none"
102 )]
103 pub pev_start_wait_time: Option<u32>,
104
105 #[serde(
106 rename = "@pev_start_window",
107 default,
108 skip_serializing_if = "Option::is_none"
109 )]
110 pub pev_start_window: Option<u32>,
111
112 #[serde(rename = "Point", default)]
113 pub points: Vec<Point>,
114}
115
116#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
117pub enum TaskType {
118 AAT,
119 RT,
120 FAIGeneral,
121 FAITriangle,
122 FAIOR,
123 FAIGoal,
124 MAT,
125 Mixed,
126 Touring,
127}
128
129#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
130pub enum AltitudeReference {
131 AGL,
132 MSL,
133}
134
135impl<'de> Deserialize<'de> for AltitudeReference {
136 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
137 let s: String = Deserialize::deserialize(deserializer)?;
138 if s == "MSL" {
140 Ok(AltitudeReference::MSL)
141 } else {
142 Ok(AltitudeReference::AGL)
143 }
144 }
145}
146
147#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
148pub struct Point {
149 #[serde(rename = "@type")]
150 pub point_type: PointType,
151
152 #[serde(
153 rename = "@score_exit",
154 default,
155 deserialize_with = "de_opt_bool",
156 serialize_with = "ser_opt_bool",
157 skip_serializing_if = "Option::is_none"
158 )]
159 pub score_exit: Option<bool>,
160
161 #[serde(rename = "Waypoint")]
162 pub waypoint: Waypoint,
163
164 #[serde(rename = "ObservationZone")]
165 pub observation_zone: ObservationZone,
166}
167
168#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
169pub enum PointType {
170 Start,
171 Turn,
172 Area,
173 Finish,
174 OptionalStart,
175}
176
177#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
178pub struct Waypoint {
179 #[serde(rename = "@name")]
180 pub name: String,
181
182 #[serde(
183 rename = "@altitude",
184 default,
185 deserialize_with = "de_opt_f64",
186 serialize_with = "ser_opt_f64",
187 skip_serializing_if = "Option::is_none"
188 )]
189 pub altitude: Option<f64>,
190
191 #[serde(rename = "@id", default, skip_serializing_if = "Option::is_none")]
192 pub id: Option<String>,
193
194 #[serde(rename = "@comment", default, skip_serializing_if = "Option::is_none")]
195 pub comment: Option<String>,
196
197 #[serde(rename = "Location")]
198 pub location: Location,
199}
200
201#[derive(Debug, Clone, Copy, PartialEq, Deserialize, Serialize)]
202pub struct Location {
203 #[serde(
204 rename = "@longitude",
205 deserialize_with = "de_f64",
206 serialize_with = "ser_f64"
207 )]
208 pub longitude: f64,
209
210 #[serde(
211 rename = "@latitude",
212 deserialize_with = "de_f64",
213 serialize_with = "ser_f64"
214 )]
215 pub latitude: f64,
216}
217
218#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
219#[serde(tag = "@type")]
220pub enum ObservationZone {
221 Cylinder {
223 #[serde(
224 rename = "@radius",
225 deserialize_with = "de_f64",
226 serialize_with = "ser_f64"
227 )]
228 radius: f64,
229 },
230
231 Line {
233 #[serde(
234 rename = "@length",
235 deserialize_with = "de_f64",
236 serialize_with = "ser_f64"
237 )]
238 length: f64,
239 },
240
241 Keyhole,
243
244 FAISector,
246
247 Sector {
251 #[serde(
252 rename = "@radius",
253 deserialize_with = "de_f64",
254 serialize_with = "ser_f64"
255 )]
256 radius: f64,
257 #[serde(
258 rename = "@start_radial",
259 deserialize_with = "de_f64",
260 serialize_with = "ser_f64"
261 )]
262 start_radial: f64,
263 #[serde(
264 rename = "@end_radial",
265 deserialize_with = "de_f64",
266 serialize_with = "ser_f64"
267 )]
268 end_radial: f64,
269 #[serde(
270 rename = "@inner_radius",
271 default,
272 deserialize_with = "de_opt_f64",
273 serialize_with = "ser_opt_f64",
274 skip_serializing_if = "Option::is_none"
275 )]
276 inner_radius: Option<f64>,
277 },
278
279 SymmetricQuadrant {
283 #[serde(
284 rename = "@radius",
285 default,
286 deserialize_with = "de_opt_f64",
287 serialize_with = "ser_opt_f64",
288 skip_serializing_if = "Option::is_none"
289 )]
290 radius: Option<f64>,
291 #[serde(
292 rename = "@angle",
293 default,
294 deserialize_with = "de_opt_f64",
295 serialize_with = "ser_opt_f64",
296 skip_serializing_if = "Option::is_none"
297 )]
298 angle: Option<f64>,
299 },
300
301 CustomKeyhole {
305 #[serde(
306 rename = "@radius",
307 default,
308 deserialize_with = "de_opt_f64",
309 serialize_with = "ser_opt_f64",
310 skip_serializing_if = "Option::is_none"
311 )]
312 radius: Option<f64>,
313 #[serde(
314 rename = "@angle",
315 default,
316 deserialize_with = "de_opt_f64",
317 serialize_with = "ser_opt_f64",
318 skip_serializing_if = "Option::is_none"
319 )]
320 angle: Option<f64>,
321 #[serde(
322 rename = "@inner_radius",
323 default,
324 deserialize_with = "de_opt_f64",
325 serialize_with = "ser_opt_f64",
326 skip_serializing_if = "Option::is_none"
327 )]
328 inner_radius: Option<f64>,
329 },
330
331 MatCylinder,
333
334 BGAStartSector,
336
337 BGAFixedCourse,
339
340 BGAEnhancedOption,
342}
343
344fn de_f64<'de, D: Deserializer<'de>>(deserializer: D) -> Result<f64, D::Error> {
345 let s: String = Deserialize::deserialize(deserializer)?;
346 s.parse().map_err(serde::de::Error::custom)
347}
348
349fn de_opt_f64<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Option<f64>, D::Error> {
350 let s: Option<String> = Deserialize::deserialize(deserializer)?;
351 match s {
352 Some(s) => s.parse().map(Some).map_err(serde::de::Error::custom),
353 None => Ok(None),
354 }
355}
356
357fn de_opt_bool<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Option<bool>, D::Error> {
358 let s: Option<String> = Deserialize::deserialize(deserializer)?;
359 match s {
360 Some(s) => match s.as_str() {
361 "1" | "true" => Ok(Some(true)),
362 "0" | "false" => Ok(Some(false)),
363 _ => Err(serde::de::Error::custom(format!("invalid bool: {s}"))),
364 },
365 None => Ok(None),
366 }
367}
368
369fn ser_f64<S: Serializer>(value: &f64, serializer: S) -> Result<S::Ok, S::Error> {
370 serializer.serialize_str(&value.to_string())
371}
372
373fn ser_opt_f64<S: Serializer>(value: &Option<f64>, serializer: S) -> Result<S::Ok, S::Error> {
374 match value {
375 Some(v) => serializer.serialize_str(&v.to_string()),
376 None => serializer.serialize_none(),
377 }
378}
379
380fn ser_opt_bool<S: Serializer>(value: &Option<bool>, serializer: S) -> Result<S::Ok, S::Error> {
381 match value {
382 Some(true) => serializer.serialize_str("1"),
383 Some(false) => serializer.serialize_str("0"),
384 None => serializer.serialize_none(),
385 }
386}
387
388#[derive(Debug, thiserror::Error)]
389pub enum ParseError {
390 #[error("XML parsing failed: {0}")]
391 Xml(#[from] quick_xml::DeError),
392}
393
394#[derive(Debug, thiserror::Error)]
395pub enum SerializeError {
396 #[error("XML serialization failed: {0}")]
397 Xml(#[from] quick_xml::SeError),
398}
399
400pub fn from_str(xml: &str) -> Result<Task, ParseError> {
401 Ok(quick_xml::de::from_str(xml)?)
402}
403
404pub fn from_reader(reader: impl BufRead) -> Result<Task, ParseError> {
405 Ok(quick_xml::de::from_reader(reader)?)
406}
407
408pub fn to_writer(mut writer: impl FmtWrite, task: &Task) -> Result<(), SerializeError> {
409 let serializer = quick_xml::se::Serializer::new(&mut writer);
410 task.serialize(serializer)?;
411 Ok(())
412}
413
414pub fn to_writer_pretty(mut writer: impl FmtWrite, task: &Task) -> Result<(), SerializeError> {
415 let mut serializer = quick_xml::se::Serializer::new(&mut writer);
416 serializer.indent(' ', 4);
417 task.serialize(serializer)?;
418 Ok(())
419}
420
421pub fn to_string(task: &Task) -> Result<String, SerializeError> {
422 let mut buffer = String::new();
423 to_writer(&mut buffer, task)?;
424 Ok(buffer)
425}
426
427pub fn to_string_pretty(task: &Task) -> Result<String, SerializeError> {
428 let mut buffer = String::new();
429 to_writer_pretty(&mut buffer, task)?;
430 Ok(buffer)
431}
432
433#[cfg(test)]
434mod tests {
435 use super::*;
436 use insta::{assert_debug_snapshot, assert_snapshot};
437
438 #[test]
439 fn parse_aat_task() {
440 let xml = include_str!("../fixtures/aat-task.tsk");
441 let task = from_str(xml).unwrap();
442 assert_debug_snapshot!(task);
443 }
444
445 #[test]
446 fn parse_racing_task() {
447 let xml = include_str!("../fixtures/racing-task.tsk");
448 let task = from_str(xml).unwrap();
449 assert_debug_snapshot!(task);
450 }
451
452 #[test]
453 fn parse_fai_task() {
454 let xml = include_str!("../fixtures/fai-task.tsk");
455 let task = from_str(xml).unwrap();
456 assert_debug_snapshot!(task);
457 }
458
459 #[test]
460 fn parse_all_oz_types() {
461 let xml = include_str!("../fixtures/all-oz-types.tsk");
462 let task = from_str(xml).unwrap();
463 assert_debug_snapshot!(task);
464 }
465
466 #[test]
467 fn roundtrip_aat_task() {
468 let xml = include_str!("../fixtures/aat-task.tsk");
469 let task = from_str(xml).unwrap();
470
471 let serialized = to_string_pretty(&task).unwrap();
472 assert_snapshot!(serialized);
473
474 let roundtripped = from_str(&serialized).unwrap();
475 assert_eq!(task, roundtripped);
476 }
477
478 #[test]
479 fn roundtrip_racing_task() {
480 let xml = include_str!("../fixtures/racing-task.tsk");
481 let task = from_str(xml).unwrap();
482
483 let serialized = to_string_pretty(&task).unwrap();
484 assert_snapshot!(serialized);
485
486 let roundtripped = from_str(&serialized).unwrap();
487 assert_eq!(task, roundtripped);
488 }
489
490 #[test]
491 fn roundtrip_fai_task() {
492 let xml = include_str!("../fixtures/fai-task.tsk");
493 let task = from_str(xml).unwrap();
494
495 let serialized = to_string_pretty(&task).unwrap();
496 assert_snapshot!(serialized);
497
498 let roundtripped = from_str(&serialized).unwrap();
499 assert_eq!(task, roundtripped);
500 }
501
502 #[test]
503 fn roundtrip_all_oz_types() {
504 let xml = include_str!("../fixtures/all-oz-types.tsk");
505 let task = from_str(xml).unwrap();
506
507 let serialized = to_string_pretty(&task).unwrap();
508 assert_snapshot!(serialized);
509
510 let roundtripped = from_str(&serialized).unwrap();
511 assert_eq!(task, roundtripped);
512 }
513
514 #[test]
515 fn serialize_ugly() {
516 let xml = include_str!("../fixtures/aat-task.tsk");
517 let task = from_str(xml).unwrap();
518
519 let serialized = to_string(&task).unwrap();
520 assert_snapshot!(serialized);
521 }
522}