1use std::num::ParseIntError;
26use std::str::FromStr;
27use thiserror::Error;
28
29pub mod command {
31 pub const HEARTBEAT: &str = "$F";
32 pub const COMPETITOR: &str = "$A";
33 pub const COMPETITOR_EXT: &str = "$COMP";
34 pub const RUN: &str = "$B";
35 pub const CLASS: &str = "$C";
36 pub const SETTING: &str = "$E";
37 pub const RACE: &str = "$G";
38 pub const PRAC_QUAL: &str = "$H";
39 pub const INIT: &str = "$I";
40 pub const PASSING: &str = "$J";
41 pub const CORRECTION: &str = "$COR";
42
43 pub const LINE_CROSSING: &str = "$L";
45 pub const TRACK_DESCRIPTION: &str = "$T";
46}
47
48#[derive(Error, Debug)]
50pub enum RecordError {
51 #[error("unknown record type {}", .0)]
53 UnknownRecordType(String),
54 #[error("malformed record")]
56 MalformedRecord,
57 #[error("unknown flag state '{}'", .0)]
59 UnknownFlagState(String),
60 #[error("invalid integer field")]
62 InvalidIntegerField(#[from] ParseIntError),
63 #[error("track description had different number of sections than specified")]
65 IncorrectSectionCount,
66}
67
68#[derive(Copy, Clone, Debug, PartialEq)]
69pub enum Flag {
70 None,
71 Green,
72 Yellow,
73 Red,
74 Finish,
75}
76
77impl FromStr for Flag {
78 type Err = RecordError;
79
80 fn from_str(s: &str) -> Result<Self, Self::Err> {
81 match s {
83 " " => Ok(Flag::None),
84 "Green " => Ok(Flag::Green),
85 "Yellow" => Ok(Flag::Yellow),
86 "Red " => Ok(Flag::Red),
87 "Finish" => Ok(Flag::Finish),
88 _ => Err(RecordError::UnknownFlagState(s.to_owned())),
89 }
90 }
91}
92
93trait FromParts: Sized {
96 fn decode(parts: &[&str]) -> Result<Self, RecordError>;
97}
98
99macro_rules! decode_impl {
100 ($type:ident, $count:expr, $($field:ident),+) => (
101 impl FromParts for $type {
102 fn decode(parts: &[&str]) -> Result<Self, RecordError> {
103 if parts.len() != $count {
104 return Err(RecordError::MalformedRecord);
105 }
106
107 let mut idx = 0;
109 $(
110 idx += 1;
111 let $field = parts[idx].decode()?;
112 )*
113
114 Ok(Self {
115 $(
116 $field,
117 )*
118 })
119 }
120 }
121 )
122}
123
124trait FieldExt<T> {
126 fn decode(self) -> Result<T, RecordError>;
127}
128
129impl FieldExt<String> for &str {
130 fn decode(self) -> Result<String, RecordError> {
131 Ok(self.trim_matches('"').to_owned())
132 }
133}
134
135impl FieldExt<Flag> for &str {
136 fn decode(self) -> Result<Flag, RecordError> {
137 self.trim_matches('"').parse()
138 }
139}
140
141impl FieldExt<u32> for &str {
142 fn decode(self) -> Result<u32, RecordError> {
143 Ok(self.parse()?)
144 }
145}
146
147impl FieldExt<Option<u32>> for &str {
148 fn decode(self) -> Result<Option<u32>, RecordError> {
149 if self.is_empty() {
150 Ok(None)
151 } else {
152 Ok(Some(self.parse()?))
153 }
154 }
155}
156
157impl FieldExt<u16> for &str {
158 fn decode(self) -> Result<u16, RecordError> {
159 Ok(self.parse()?)
160 }
161}
162
163impl FieldExt<u8> for &str {
164 fn decode(self) -> Result<u8, RecordError> {
165 Ok(self.parse()?)
166 }
167}
168
169#[derive(Clone, Debug)]
171pub enum Record {
172 Heartbeat(Heartbeat),
173 Competitor(Competitor),
174 CompetitorExt(CompetitorExt),
175 Run(Run),
176 Class(Class),
177 Setting(Setting),
178 Race(Race),
179 PracticeQual(PracticeQual),
180 Init(Init),
181 Passing(Passing),
182 Correction(Correction),
183 LineCrossing(LineCrossing),
184 TrackDescription(TrackDescription),
185}
186
187impl Record {
188 pub fn decode(line: &str) -> Result<Self, RecordError> {
190 let splits: Vec<&str> = line.split(',').collect();
191
192 if splits.len() < 2 {
193 return Err(RecordError::MalformedRecord);
194 }
195
196 match splits[0] {
197 command::HEARTBEAT => Ok(Record::Heartbeat(Heartbeat::decode(&splits)?)),
198 command::COMPETITOR => Ok(Record::Competitor(Competitor::decode(&splits)?)),
199 command::COMPETITOR_EXT => Ok(Record::CompetitorExt(CompetitorExt::decode(&splits)?)),
200 command::RUN => Ok(Record::Run(Run::decode(&splits)?)),
201 command::CLASS => Ok(Record::Class(Class::decode(&splits)?)),
202 command::SETTING => Ok(Record::Setting(Setting::decode(&splits)?)),
203 command::RACE => Ok(Record::Race(Race::decode(&splits)?)),
204 command::PRAC_QUAL => Ok(Record::PracticeQual(PracticeQual::decode(&splits)?)),
205 command::INIT => Ok(Record::Init(Init::decode(&splits)?)),
206 command::PASSING => Ok(Record::Passing(Passing::decode(&splits)?)),
207 command::CORRECTION => Ok(Record::Correction(Correction::decode(&splits)?)),
208 command::LINE_CROSSING => Ok(Record::LineCrossing(LineCrossing::decode(&splits)?)),
209 command::TRACK_DESCRIPTION => {
210 Ok(Record::TrackDescription(TrackDescription::decode(&splits)?))
211 }
212 _ => Err(RecordError::UnknownRecordType(splits[0].to_owned())),
213 }
214 }
215}
216
217#[derive(Clone, Debug)]
219pub struct Heartbeat {
220 pub laps_to_go: u32,
222 pub time_to_go: String,
224 pub time_of_day: String,
226 pub race_time: String,
228 pub flag_status: Flag,
230}
231
232decode_impl!(
233 Heartbeat,
234 6,
235 laps_to_go,
236 time_to_go,
237 time_of_day,
238 race_time,
239 flag_status
240);
241
242#[derive(Clone, Debug)]
246pub struct Competitor {
247 pub registration_number: String,
248 pub number: String,
249 pub transponder_number: u32,
250 pub first_name: String,
251 pub last_name: String,
252 pub nationality: String,
254 pub class_number: u8,
256}
257
258decode_impl!(
259 Competitor,
260 8,
261 registration_number,
262 number,
263 transponder_number,
264 first_name,
265 last_name,
266 nationality,
267 class_number
268);
269
270#[derive(Clone, Debug)]
275pub struct CompetitorExt {
276 pub registration_number: String,
277 pub number: String,
278 pub class_number: u8,
279 pub first_name: String,
280 pub last_name: String,
281 pub nationality: String,
282 pub additional_data: String,
283}
284
285decode_impl!(
286 CompetitorExt,
287 8,
288 registration_number,
289 number,
290 class_number,
291 first_name,
292 last_name,
293 nationality,
294 additional_data
295);
296
297#[derive(Debug, Clone)]
299pub struct Run {
300 pub number: u8,
302 pub description: String,
303}
304
305decode_impl!(Run, 3, number, description);
306
307#[derive(Debug, Clone)]
309pub struct Class {
310 pub number: u8,
312 pub description: String,
313}
314
315decode_impl!(Class, 3, number, description);
316
317#[derive(Debug, Clone)]
325pub struct Setting {
326 pub description: String,
327 pub value: String,
330}
331
332decode_impl!(Setting, 3, description, value);
333
334#[derive(Debug, Clone)]
349pub struct Race {
350 pub position: u16,
352 pub registration_number: String,
353 pub laps: Option<u32>,
356 pub total_time: String,
359}
360
361decode_impl!(Race, 5, position, registration_number, laps, total_time);
362
363#[derive(Debug, Clone)]
373pub struct PracticeQual {
374 pub position: u16,
376 pub registration_number: String,
377 pub best_lap: u32,
379 pub best_laptime: String,
381}
382
383decode_impl!(
384 PracticeQual,
385 5,
386 position,
387 registration_number,
388 best_lap,
389 best_laptime
390);
391
392#[derive(Debug, Clone)]
397pub struct Init {
398 pub time: String,
399 pub date: String,
400}
401
402decode_impl!(Init, 3, time, date);
403
404#[derive(Debug, Clone)]
408pub struct Passing {
409 pub registration_number: String,
410 pub laptime: String,
411 pub total_time: String,
412}
413
414decode_impl!(Passing, 4, registration_number, laptime, total_time);
415
416#[derive(Debug, Clone)]
421pub struct Correction {
422 pub registration_number: String,
423 pub number: String,
424 pub laps: u32,
425 pub total_time: String,
427 pub correction: String,
429}
430
431decode_impl!(
432 Correction,
433 6,
434 registration_number,
435 number,
436 laps,
437 total_time,
438 correction
439);
440
441#[derive(Debug, Clone)]
446pub struct LineCrossing {
447 pub number: String,
448 pub timeline_number: String,
449 pub timeline_name: String,
450 pub date: String,
451 pub time: String,
452 pub driver_id: Option<u8>,
455 pub class_name: Option<String>,
456}
457
458impl FromParts for LineCrossing {
460 fn decode(parts: &[&str]) -> Result<Self, RecordError> {
461 if parts.len() < 6 {
462 return Err(RecordError::MalformedRecord);
463 }
464
465 let driver_id = parts
466 .get(6)
467 .map(|p| p.decode())
468 .map_or(Ok(None), |r| r.map(Some))?;
469
470 let class_name = parts
471 .get(7)
472 .map(|p| p.decode())
473 .map_or(Ok(None), |r| r.map(Some))?;
474
475 Ok(Self {
476 number: parts[1].decode()?,
477 timeline_number: parts[2].decode()?,
478 timeline_name: parts[3].decode()?,
479 date: parts[4].decode()?,
480 time: parts[5].decode()?,
481 driver_id,
482 class_name,
483 })
484 }
485}
486
487#[derive(Debug, Clone)]
496pub struct TrackDescription {
497 pub name: String,
498 pub short_name: String,
499 pub distance: String,
500 pub sections: Vec<TrackSection>,
501}
502
503#[derive(Debug, Clone)]
507pub struct TrackSection {
508 pub name: String,
510 pub start: String,
512 pub end: String,
514 pub distance: u32,
516}
517
518impl FromParts for TrackDescription {
519 fn decode(parts: &[&str]) -> Result<Self, RecordError> {
520 if parts.len() < 5 {
521 return Err(RecordError::MalformedRecord);
522 }
523
524 let expected: usize = parts[4].parse()?;
525 let sections: Vec<TrackSection> = parts[5..]
526 .chunks(4)
527 .filter(|s| s.len() == 4) .map(|s| {
529 Ok(TrackSection {
530 name: s[0].decode()?,
531 start: s[1].decode()?,
532 end: s[2].decode()?,
533 distance: s[3].decode()?,
534 })
535 })
536 .collect::<Result<Vec<TrackSection>, RecordError>>()?;
537
538 if sections.len() != expected {
539 return Err(RecordError::IncorrectSectionCount);
540 }
541
542 Ok(Self {
543 name: parts[1].decode()?,
544 short_name: parts[2].decode()?,
545 distance: parts[3].decode()?,
546 sections,
547 })
548 }
549}
550
551#[cfg(test)]
552mod tests {
553 use super::*;
554
555 #[test]
556 fn test_decodes_unknown_record() {
557 let data = "$ZZZ,5,\"Friday free practice\"";
558 let record = Record::decode(&data);
559
560 assert!(record.is_err());
561 assert!(matches!(record, Err(RecordError::UnknownRecordType(_))));
562 }
563
564 #[test]
565 fn test_decodes_heartbeat() {
566 let data = "$F,14,\"00:12:45\",\"13:34:23\",\"00:09:47\",\"Green \"";
567 let record = Record::decode(&data);
568
569 assert!(record.is_ok());
570 assert!(matches!(
571 record,
572 Ok(Record::Heartbeat(Heartbeat { laps_to_go: 14, .. }))
573 ));
574 }
575
576 #[test]
577 fn test_decodes_competitor() {
578 let data = "$A,\"1234BE\",\"12X\",52474,\"John\",\"Johnson\",\"USA\",5";
579 let record = Record::decode(&data);
580
581 assert!(record.is_ok());
582 assert!(matches!(record, Ok(Record::Competitor(_))));
583
584 if let Ok(Record::Competitor(competitor)) = record {
585 assert_eq!(competitor.registration_number, "1234BE");
586 assert_eq!(competitor.number, "12X");
587 assert_eq!(competitor.transponder_number, 52474);
588 assert_eq!(competitor.first_name, "John");
589 assert_eq!(competitor.last_name, "Johnson");
590 assert_eq!(competitor.nationality, "USA");
591 assert_eq!(competitor.class_number, 5);
592 }
593 }
594
595 #[test]
596 fn test_decodes_competitor_ext() {
597 let data = "$COMP,\"1234BE\",\"12X\",5,\"John\",\"Johnson\",\"USA\",\"CAMEL\"";
598 let record = Record::decode(&data);
599
600 assert!(record.is_ok());
601 assert!(matches!(record, Ok(Record::CompetitorExt(_))));
602
603 if let Ok(Record::CompetitorExt(competitor)) = record {
604 assert_eq!(competitor.registration_number, "1234BE");
605 assert_eq!(competitor.number, "12X");
606 assert_eq!(competitor.first_name, "John");
607 assert_eq!(competitor.last_name, "Johnson");
608 assert_eq!(competitor.nationality, "USA");
609 assert_eq!(competitor.additional_data, "CAMEL");
610 assert_eq!(competitor.class_number, 5);
611 }
612 }
613
614 #[test]
615 fn test_decodes_run() {
616 let data = "$B,5,\"Friday free practice\"";
617 let record = Record::decode(&data);
618
619 assert!(record.is_ok());
620 assert!(matches!(record, Ok(Record::Run(_))));
621
622 if let Ok(Record::Run(run)) = record {
623 assert_eq!(run.number, 5);
624 assert_eq!(run.description, "Friday free practice");
625 }
626 }
627
628 #[test]
629 fn test_decodes_class() {
630 let data = "$C,5,\"Formula 3000\"";
631 let record = Record::decode(&data);
632
633 assert!(record.is_ok());
634 assert!(matches!(record, Ok(Record::Class(_))));
635
636 if let Ok(Record::Class(class)) = record {
637 assert_eq!(class.number, 5);
638 assert_eq!(class.description, "Formula 3000");
639 }
640 }
641
642 #[test]
643 fn test_decodes_settings() {
644 let data = "$E,\"TRACKNAME\",\"Indianapolis Motor Speedway\"";
646 let record = Record::decode(&data);
647
648 assert!(record.is_ok());
649 assert!(matches!(record, Ok(Record::Setting(_))));
650
651 if let Ok(Record::Setting(setting)) = record {
652 assert_eq!(setting.description, "TRACKNAME");
653 assert_eq!(setting.value, "Indianapolis Motor Speedway");
654 }
655
656 let data = "$E,\"TRACKLENGTH\",\"2.500\"";
657 let record = Record::decode(&data);
658
659 assert!(record.is_ok());
660 assert!(matches!(record, Ok(Record::Setting(_))));
661
662 if let Ok(Record::Setting(setting)) = record {
663 assert_eq!(setting.description, "TRACKLENGTH");
664 assert_eq!(setting.value, "2.500");
665 }
666 }
667
668 #[test]
669 fn test_decodes_race() {
670 let data = "$G,3,\"1234BE\",14,\"01:12:47.872\"";
671 let record = Record::decode(&data);
672
673 assert!(record.is_ok());
674 assert!(matches!(record, Ok(Record::Race(_))));
675
676 if let Ok(Record::Race(race)) = record {
677 assert_eq!(race.position, 3);
678 assert_eq!(race.registration_number, "1234BE");
679 assert_eq!(race.laps, Some(14));
680 assert_eq!(race.total_time, "01:12:47.872");
681 }
682 }
683
684 #[test]
685 fn test_decodes_practice_qual() {
686 let data = "$H,2,\"1234BE\",3,\"00:02:17.872\"";
687 let record = Record::decode(&data);
688
689 assert!(record.is_ok());
690 assert!(matches!(record, Ok(Record::PracticeQual(_))));
691
692 if let Ok(Record::PracticeQual(pq)) = record {
693 assert_eq!(pq.position, 2);
694 assert_eq!(pq.registration_number, "1234BE");
695 assert_eq!(pq.best_lap, 3);
696 assert_eq!(pq.best_laptime, "00:02:17.872");
697 }
698 }
699
700 #[test]
701 fn test_decodes_init_command() {
702 let data = "$I,\"16:36:08.000\",\"12 jan 01\"";
703 let record = Record::decode(&data);
704
705 assert!(record.is_ok());
706 assert!(matches!(record, Ok(Record::Init(_))));
707
708 if let Ok(Record::Init(init)) = record {
709 assert_eq!(init.time, "16:36:08.000");
710 assert_eq!(init.date, "12 jan 01");
711 }
712 }
713
714 #[test]
715 fn test_decodes_passing() {
716 let data = "$J,\"1234BE\",\"00:02:03.826\",\"01:42:17.672\"";
717 let record = Record::decode(&data);
718
719 assert!(record.is_ok());
720 assert!(matches!(record, Ok(Record::Passing(_))));
721
722 if let Ok(Record::Passing(passing)) = record {
723 assert_eq!(passing.registration_number, "1234BE");
724 assert_eq!(passing.laptime, "00:02:03.826");
725 assert_eq!(passing.total_time, "01:42:17.672");
726 }
727 }
728
729 #[test]
730 fn test_decodes_correction() {
731 let data = "$COR,\"123BE\",\"658\",2,\"00:00:35.272\",\"+00:00:00.012\"";
732 let record = Record::decode(&data);
733
734 assert!(record.is_ok());
735 assert!(matches!(record, Ok(Record::Correction(_))));
736
737 if let Ok(Record::Correction(cor)) = record {
738 assert_eq!(cor.registration_number, "123BE");
739 assert_eq!(cor.number, "658");
740 assert_eq!(cor.laps, 2);
741 assert_eq!(cor.correction, "+00:00:00.012");
742 }
743 }
744
745 #[test]
746 fn test_decodes_line_crossing() {
747 let data = "$L,\"13\",\"P2\",\"POP\",\"01/27/2009\",\"10:10:20.589\",1,\"PC\"";
749 let record = Record::decode(&data);
750
751 assert!(record.is_ok());
752 assert!(matches!(record, Ok(Record::LineCrossing(_))));
753
754 if let Ok(Record::LineCrossing(c)) = record {
755 assert_eq!(c.number, "13");
756 assert_eq!(c.timeline_number, "P2");
757 assert_eq!(c.timeline_name, "POP");
758 assert_eq!(c.date, "01/27/2009");
759 assert_eq!(c.time, "10:10:20.589");
760 assert_eq!(c.driver_id, Some(1));
761 assert_eq!(c.class_name, Some("PC".to_owned()));
762 }
763
764 let data = "$L,\"15\",\"P1\",\"SFP\",\"01/27/2009\",\"14:13:22.818\"";
766 let record = Record::decode(&data);
767
768 assert!(record.is_ok());
769 assert!(matches!(record, Ok(Record::LineCrossing(_))));
770
771 if let Ok(Record::LineCrossing(c)) = record {
772 assert_eq!(c.number, "15");
773 assert_eq!(c.timeline_number, "P1");
774 assert_eq!(c.timeline_name, "SFP");
775 assert_eq!(c.date, "01/27/2009");
776 assert_eq!(c.time, "14:13:22.818");
777 assert_eq!(c.driver_id, None);
778 assert_eq!(c.class_name, None);
779 }
780 }
781
782 #[test]
783 fn test_decodes_track_description() {
784 let data = concat!(
785 r#"$T,"Circuit of the Americas","COTA","3.40",15,"#,
786 r#""S01","T1","T2",3375,"S02","T2","T3",36559,"S03","T3","T4",40933,"S04","T4","T5",13256,"S05","T5",""#,
787 r#"T6",20923,"S06","T6","T7",1181,"S07","T7","T8",12711,"S08","T8","T9",1181,"S09","T9","TA",29313,"S1"#,
788 r#"0","TA","TB",41744,"S11","TB","T1",16113,"LAP","T1","P1",217379,"PIT","PB","P2",19688,"SP4","T6","T"#,
789 r#"7",1181,"SP5","T8","T9",1181"#
790 );
791
792 let record = Record::decode(&data);
793
794 assert!(record.is_ok());
795 assert!(matches!(record, Ok(Record::TrackDescription(_))));
796
797 if let Ok(Record::TrackDescription(td)) = record {
798 assert_eq!(td.name, "Circuit of the Americas");
799 assert_eq!(td.short_name, "COTA");
800 assert_eq!(td.distance, "3.40");
801 assert_eq!(td.sections.len(), 15);
802 }
803 }
804
805 #[test]
806 fn test_errors_wrong_track_section_count() {
807 let data = concat!(
808 r#"$T,"Circuit of the Americas","COTA","3.40",15,"#,
809 r#""S01","T1","T2",3375,"S02","T2","T3",36559,"S03","T3","T4",40933,"S04","T4","T5",13256,"S05","T5",""#,
810 r#"T6",20923,"S06","T6","T7",1181,"S07","T7","T8",12711,"S08","T8","T9",1181,"S09","T9","TA",29313,"S1"#,
811 r#"0","TA","TB",41744,"S11","TB","T1",16113,"LAP","T1","P1",217379,"PIT","PB","P2",19688"#
812 );
813
814 let record = Record::decode(&data);
815 assert!(record.is_err());
816 assert!(matches!(record, Err(RecordError::IncorrectSectionCount)))
817 }
818}