nmeasis 26.4.1

A memory-safe NMEA 0183 parser with a C FFI
Documentation
use crate::{
    encoder::NmeaEncode,
    macros::{write_byte, write_str},
    message::NmeaMessageError,
    parser::NmeaParse,
};

/// TXT - Text Transmission
///
/// Used to transmit short text messages, typically device info on startup.
#[derive(Debug)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
pub struct Txt<'a> {
    /// 1. Total number of messages
    pub total_messages: &'a str,
    /// 2. Number of this message
    pub message_number: &'a str,
    /// 3. Message Type
    pub message_type: &'a str,
    /// 4. Text (ASCII)
    pub text: &'a str,
}

impl<'a> NmeaParse<'a> for Txt<'a> {
    fn parse(fields: &'a str) -> Result<Self, NmeaMessageError> {
        let mut f = fields.splitn(4, ',');

        Ok(Self {
            total_messages: f.next().ok_or(NmeaMessageError::MissingField)?,
            message_number: f.next().ok_or(NmeaMessageError::MissingField)?,
            message_type: f.next().ok_or(NmeaMessageError::MissingField)?,
            text: f.next().ok_or(NmeaMessageError::MissingField)?,
        })
    }
}

impl NmeaEncode for Txt<'_> {
    fn encoded_len(&self) -> usize {
        self.total_messages.len()
            + self.message_number.len()
            + self.message_type.len()
            + self.text.len()
            + 3
    }

    fn encode(&self, buf: &mut [u8]) -> usize {
        let mut pos = 0;
        write_str!(buf, pos, self.total_messages);
        write_byte!(buf, pos, b',');
        write_str!(buf, pos, self.message_number);
        write_byte!(buf, pos, b',');
        write_str!(buf, pos, self.message_type);
        write_byte!(buf, pos, b',');
        write_str!(buf, pos, self.text);
        pos
    }
}

#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TxtMessageType {
    Error = 0,
    Warning = 1,
    Notice = 2,
    Unknown = 7,
}

impl TxtMessageType {
    #[must_use]
    pub fn parse(raw: u8) -> Option<Self> {
        match raw {
            0 => Some(TxtMessageType::Error),
            1 => Some(TxtMessageType::Warning),
            2 => Some(TxtMessageType::Notice),
            7 => Some(TxtMessageType::Unknown),
            _ => None,
        }
    }
}

impl Txt<'_> {
    #[must_use]
    pub fn total_messages(&self) -> Option<u8> {
        self.total_messages.parse().ok()
    }

    #[must_use]
    pub fn message_number(&self) -> Option<u8> {
        self.message_number.parse().ok()
    }

    #[must_use]
    pub fn message_type(&self) -> Option<TxtMessageType> {
        TxtMessageType::parse(self.message_type.parse().ok()?)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    const FIELDS: &str = "01,01,02,u-blox AG - www.u-blox.com";

    #[test]
    fn parses_raw_fields() {
        let txt = Txt::parse(FIELDS).unwrap();
        assert_eq!(txt.total_messages, "01");
        assert_eq!(txt.message_number, "01");
        assert_eq!(txt.message_type, "02");
        assert_eq!(txt.text, "u-blox AG - www.u-blox.com");
    }

    #[test]
    fn total_messages_parsed() {
        let txt = Txt::parse(FIELDS).unwrap();
        assert_eq!(txt.total_messages(), Some(1));
    }

    #[test]
    fn message_number_parsed() {
        let txt = Txt::parse(FIELDS).unwrap();
        assert_eq!(txt.message_number(), Some(1));
    }

    #[test]
    fn message_type_error() {
        let txt = Txt::parse("01,01,00,something went wrong").unwrap();
        assert_eq!(txt.message_type().unwrap(), TxtMessageType::Error);
    }

    #[test]
    fn message_type_warning() {
        let txt = Txt::parse("01,01,01,something to note").unwrap();
        assert_eq!(txt.message_type().unwrap(), TxtMessageType::Warning);
    }

    #[test]
    fn message_type_notice() {
        let txt = Txt::parse("01,01,02,informational").unwrap();
        assert_eq!(txt.message_type().unwrap(), TxtMessageType::Notice);
    }

    #[test]
    fn message_type_unknown() {
        let txt = Txt::parse("01,01,07,informational").unwrap();
        assert_eq!(txt.message_type().unwrap(), TxtMessageType::Unknown);
    }

    #[test]
    fn message_type_random() {
        let txt = Txt::parse("01,01,99,something").unwrap();
        assert!(txt.message_type().is_none());
    }

    #[test]
    fn text_with_commas() {
        let txt = Txt::parse("01,01,02,hello, world, this has commas").unwrap();
        assert_eq!(txt.text, "hello, world, this has commas");
    }

    #[test]
    fn multi_message_sequence() {
        let txt1 = Txt::parse("03,01,07,first part").unwrap();
        let txt2 = Txt::parse("03,02,07,second part").unwrap();
        let txt3 = Txt::parse("03,03,07,third part").unwrap();
        assert_eq!(txt1.total_messages(), Some(3));
        assert_eq!(txt2.message_number(), Some(2));
        assert_eq!(txt3.message_number(), Some(3));
    }

    #[test]
    fn missing_required_field() {
        assert!(Txt::parse("01,01").is_err());
        assert!(Txt::parse("01").is_err());
        assert!(Txt::parse("").is_err());
    }

    #[test]
    fn encode_round_trip() {
        let input = "01,01,02,u-blox AG - www.u-blox.com";
        let txt = Txt::parse(input).unwrap();
        let mut buf = [0u8; 64];
        let len = txt.encode(&mut buf);
        let encoded = core::str::from_utf8(&buf[..len]).unwrap();
        let txt2 = Txt::parse(encoded).unwrap();
        assert_eq!(txt.total_messages, txt2.total_messages);
        assert_eq!(txt.message_number, txt2.message_number);
        assert_eq!(txt.message_type, txt2.message_type);
        assert_eq!(txt.text, txt2.text);
    }

    #[test]
    fn encoded_len_matches_actual() {
        let txt = Txt::parse(FIELDS).unwrap();
        let mut buf = [0u8; 64];
        let len = txt.encode(&mut buf);
        assert_eq!(len, txt.encoded_len());
    }
}