rmonitor/
protocol.rs

1//! Protocol implementation for decoding RMonitor messages.
2//!
3//! # Example
4//!
5//! ```
6//! use rmonitor::protocol::Record;
7//!
8//! let data = r#"$A,"1234BE","12X",52474,"John","Johnson","USA",5"#;
9//! let record = Record::decode(&data);
10//!
11//! assert!(record.is_ok());
12//! assert!(matches!(record, Ok(Record::Competitor(_))));
13//!
14//! if let Ok(Record::Competitor(competitor)) = record {
15//!     assert_eq!(competitor.registration_number, "1234BE");
16//!     assert_eq!(competitor.number, "12X");
17//!     assert_eq!(competitor.transponder_number, 52474);
18//!     assert_eq!(competitor.first_name, "John");
19//!     assert_eq!(competitor.last_name, "Johnson");
20//!     assert_eq!(competitor.nationality, "USA");
21//!     assert_eq!(competitor.class_number, 5);
22//! }
23//! ```
24
25use std::num::ParseIntError;
26use std::str::FromStr;
27use thiserror::Error;
28
29/// RMonitor commands are represented in messages by ASCII strings
30pub 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    // IMSA enhanced protocol messages
44    pub const LINE_CROSSING: &str = "$L";
45    pub const TRACK_DESCRIPTION: &str = "$T";
46}
47
48/// An error occured when decoding a record
49#[derive(Error, Debug)]
50pub enum RecordError {
51    /// The record prefix was not recognised as a valid record type
52    #[error("unknown record type {}", .0)]
53    UnknownRecordType(String),
54    /// The input could not be decoded as the record type indicated by the prefix
55    #[error("malformed record")]
56    MalformedRecord,
57    /// A heartbeat record included an unrecognised flag state
58    #[error("unknown flag state '{}'", .0)]
59    UnknownFlagState(String),
60    /// A numeric record field could't be parsed as an integer
61    #[error("invalid integer field")]
62    InvalidIntegerField(#[from] ParseIntError),
63    /// An IMSA track description record had a different number of sections than specified
64    #[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        // Flag states are fixed width, with trailing spaces
82        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
93/// Implemented for types which can be constructed from the comma-separated parts of an RMonitor
94/// line.
95trait 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                // A little clunky, but should optimize out
108                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
124/// Implemented for types which can be constructed from a single RMonitor message part.
125trait 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/// A unit of data from the RMonitor protocol
170#[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    /// Decodes a record from a single line of valid UTF-8 text
189    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/// Heartbeat message, sent every second that a session is active
218#[derive(Clone, Debug)]
219pub struct Heartbeat {
220    /// Number of laps to go
221    pub laps_to_go: u32,
222    /// Time until the session ends
223    pub time_to_go: String,
224    /// The current time (usually in UTC, but dependent on the timing system in use)
225    pub time_of_day: String,
226    /// The time from the first green flag
227    pub race_time: String,
228    /// Current flag status
229    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/// Competitor information record
243///
244/// Competitors are unqiuely keyed on their `registration_number` field.
245#[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    /// Often used for Make/Model or Team name by some timing software
253    pub nationality: String,
254    /// Unique class number (matches a `Class` record)
255    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/// Extended competitor information
271///
272/// It's unclear why the protocol includes this extra (almost identical) competitor information
273/// message, but it is included for completeness.
274#[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/// Run (session) information
298#[derive(Debug, Clone)]
299pub struct Run {
300    /// Defined as 'unique', it's likely this means unique within a single RMonitor session
301    pub number: u8,
302    pub description: String,
303}
304
305decode_impl!(Run, 3, number, description);
306
307/// Class information
308#[derive(Debug, Clone)]
309pub struct Class {
310    /// Defined as 'unique', it's likely this means unique within a single RMonitor session
311    pub number: u8,
312    pub description: String,
313}
314
315decode_impl!(Class, 3, number, description);
316
317/// Track setting information
318///
319/// This message type supports arbitrary key-value pairs, however the only specified keys in the
320/// protocol documentation are:
321///
322/// - 'TRACKNAME': The name of the track / event venue
323/// - 'TRACKLENGTH': The length of the track / event venue
324#[derive(Debug, Clone)]
325pub struct Setting {
326    pub description: String,
327    /// Specified as a `String` for both defined keys, however `TRACKLENGTH` is normally a string
328    /// representation of a decimal number (e.g. '2.500')
329    pub value: String,
330}
331
332decode_impl!(Setting, 3, description, value);
333
334/// Race position information
335///
336/// Contains the current race position of a competitor (identified by `registration_number`) and
337/// their total time / laps completed.
338///
339/// # Note
340///
341/// If an overtake occurs, the timing software should emit a `Race` record for both the passing and
342/// passed competitors, updated with their new positions, so you shouldn't need to recompute the
343/// running order yourself.
344///
345/// Both `Race` and `PracticeQual` messages should be expected in all types of session, in all
346/// scenarios they provide information about the competitor's best lap and total race time, the
347/// interpretation of the standings will depend on the type of session in progress.
348#[derive(Debug, Clone)]
349pub struct Race {
350    /// The competitor's position in the running order
351    pub position: u16,
352    pub registration_number: String,
353    /// Laps completed, this will be `None` if the competitor has not yet completed a lap after a
354    /// Green flag state has occured.
355    pub laps: Option<u32>,
356    /// Total race time (the sentinel value `00:59:59.999` indicates a competitor for whom no
357    /// passing has yet been recorded).
358    pub total_time: String,
359}
360
361decode_impl!(Race, 5, position, registration_number, laps, total_time);
362
363/// Practice / Qualification position information
364///
365/// Contains the current position of a competitor in the standings for a Practice or Qualifying
366/// session.
367///
368/// # Note
369///
370/// As with a `Race` record, the timing software should issue multiple `PracticeQual` messages when
371/// the standings change.
372#[derive(Debug, Clone)]
373pub struct PracticeQual {
374    /// The competitor's position in the fastest-lap standings
375    pub position: u16,
376    pub registration_number: String,
377    /// The lap number of the best lap
378    pub best_lap: u32,
379    /// The laptime of the best lap
380    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/// Indicates that the scoreboard should be reset
393///
394/// The timing software may send an `Init` message immediately prior to the start of a new session,
395/// or when it has determined the data is stale and should be completely refreshed.
396#[derive(Debug, Clone)]
397pub struct Init {
398    pub time: String,
399    pub date: String,
400}
401
402decode_impl!(Init, 3, time, date);
403
404/// Passing information
405///
406/// Sent each time a competitor crosses the main timeline.
407#[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/// Corrected finish time
417///
418/// Sent each time a passing time is corrected (this can be due to a photocell time being
419/// associated with a competitor after the `Passing` message was already sent).
420#[derive(Debug, Clone)]
421pub struct Correction {
422    pub registration_number: String,
423    pub number: String,
424    pub laps: u32,
425    /// The corrected total time
426    pub total_time: String,
427    /// The total time corrections from the previous passing message
428    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/// Timeline crossing message
442///
443/// Sent each time a competitor crosses a timeline, this message type is part of the IMSA Enhanced
444/// specification.
445#[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    // The following fields are referenced in the IMSA protocol document
453    // but don't appear in any of the sample data.
454    pub driver_id: Option<u8>,
455    pub class_name: Option<String>,
456}
457
458// Manual implementation to support the variadic fields
459impl 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/// Track and timeline description message
488///
489/// Contains track information as well as a variable number of [`TrackSection`]s, which define the
490/// distance between two timelines.
491///
492/// This message type is part of the IMSA Enhanced specification.
493///
494/// [`TrackSection`]: crate::protocol::TrackSection
495#[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/// Track section field
504///
505/// Describes a section of track between two timelines.
506#[derive(Debug, Clone)]
507pub struct TrackSection {
508    /// Section name
509    pub name: String,
510    /// Timeline number at section start
511    pub start: String,
512    /// Timeline number at section end
513    pub end: String,
514    /// Sector distance, given in whole inches
515    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) // Discard short sections
528            .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        // Two samples provided for this protocol record
645        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        // Fields seen in protocol spec
748        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        // Fields seen in sample data
765        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}