mavinspect 0.1.0-alpha2

MAVInspect is a CLI tool and a library to parse and inspect MAVLink protocol XML definitions
Documentation
use regex::Regex;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};

use crate::proto::mavlink_messages_v1 as proto;
use crate::protocol::Value;

use super::super::errors::ProtoImportError;
use super::errors::MessageFieldParsingError;

const VALUE_AS_INVALID_REGEX: &str = r"^((-?\d+)(\.\d+)?)$";
const FIRST_LIST_VALUE_AS_INVALID_REGEX: &str = r"^\[((-?\d+)(\.\d+)?)\]$";
const FIRST_LIST_MAX_VALUE_AS_INVALID_REGEX: &str = r"^\[(.+_MAX)\]$";
const ENUM_ENTRY_VALUE_AS_INVALID_REGEX: &str = r"^[A-Z]+[A-Z_]+[A-Z]+$";

/// Describes how to specify invalid value for [`crate::protocol::MessageField`]
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum MessageFieldInvalidValue {
    /// Field has a specific value.
    Value(Value),
    /// All array items are set to a specific value.
    AllItems(Value),
    /// First array item is set to a specific value.
    FirstItem(Value),
    /// Value from enum entry specified by name
    EnumEntryValue(String),
}

// use proto::message_field_invalid_value as spec;

impl MessageFieldInvalidValue {
    /// Parses from string specification
    pub fn parse(s: &str) -> Result<MessageFieldInvalidValue, MessageFieldParsingError> {
        let normalized = s.trim();
        let first_list_value_re = Regex::new(FIRST_LIST_VALUE_AS_INVALID_REGEX).unwrap();
        let list_max_re = Regex::new(FIRST_LIST_MAX_VALUE_AS_INVALID_REGEX).unwrap();
        let value_re = Regex::new(VALUE_AS_INVALID_REGEX).unwrap();
        let enum_entry_name_re = Regex::new(ENUM_ENTRY_VALUE_AS_INVALID_REGEX).unwrap();

        Ok(match normalized {
            // NaN
            "NaN" => MessageFieldInvalidValue::Value(Value::FloatValue(f64::NAN)),
            "NAN" => MessageFieldInvalidValue::Value(Value::FloatValue(f64::NAN)),
            // First array item is NaN
            "[NaN:]" => MessageFieldInvalidValue::FirstItem(Value::FloatValue(f64::NAN)),
            // All array items are NaN
            "[NaN]" => MessageFieldInvalidValue::AllItems(Value::FloatValue(f64::NAN)),
            // Maximum value of a type
            _ if list_max_re.is_match(normalized) => {
                MessageFieldInvalidValue::FirstItem(Value::MaxValue)
            }
            // Maximum value of a type
            _ if normalized.ends_with("_MAX") => MessageFieldInvalidValue::Value(Value::MaxValue),
            // Hexadecimal value
            _ if normalized.starts_with("0x") => {
                let radix: u32 = (u32::try_from(normalized.chars().count()).unwrap() - 2) * 4;
                MessageFieldInvalidValue::Value(Value::IntValue(
                    i128::from_str_radix(normalized.trim_start_matches("0x"), radix)
                        .map_err(MessageFieldParsingError::InvalidValueSpecIntParseError)?,
                ))
            }
            // Particular value marks scalar data as invalid
            _ if value_re.is_match(normalized) => {
                let captures = value_re.captures(normalized).unwrap();

                if captures.get(3).is_none() {
                    MessageFieldInvalidValue::Value(Value::IntValue(
                        normalized
                            .parse::<i128>()
                            .map_err(MessageFieldParsingError::InvalidValueSpecIntParseError)?,
                    ))
                } else {
                    MessageFieldInvalidValue::Value(Value::FloatValue(
                        normalized
                            .parse::<f64>()
                            .map_err(MessageFieldParsingError::InvalidValueSpecFloatParseError)?,
                    ))
                }
            }
            // All element of array should be set to a specific value
            _ if first_list_value_re.is_match(normalized) => {
                let captures = first_list_value_re.captures(normalized).unwrap();
                let value = captures.get(1).unwrap().as_str();

                if captures.get(3).is_none() {
                    MessageFieldInvalidValue::AllItems(Value::IntValue(
                        value
                            .parse::<i128>()
                            .map_err(MessageFieldParsingError::InvalidValueSpecIntParseError)?,
                    ))
                } else {
                    MessageFieldInvalidValue::AllItems(Value::FloatValue(
                        value
                            .parse::<f64>()
                            .map_err(MessageFieldParsingError::InvalidValueSpecFloatParseError)?,
                    ))
                }
            }
            _ if enum_entry_name_re.is_match(normalized) => {
                MessageFieldInvalidValue::EnumEntryValue(normalized.to_string())
            }
            // Return error if nothing works
            &_ => {
                return Err(MessageFieldParsingError::InvalidValueSpecParseError(
                    normalized.to_string(),
                ))
            }
        })
    }

    /// Converts to Protobuf [`proto::MessageFieldInvalidValue`]
    pub fn to_proto(&self) -> proto::MessageFieldInvalidValue {
        use proto::message_field_invalid_value as spec;

        proto::MessageFieldInvalidValue {
            invalid: Some(match self {
                MessageFieldInvalidValue::Value(value) => spec::Invalid::Value(value.to_proto()),
                MessageFieldInvalidValue::AllItems(value) => {
                    spec::Invalid::AllItems(value.to_proto())
                }
                MessageFieldInvalidValue::FirstItem(value) => {
                    spec::Invalid::FirstItem(value.to_proto())
                }
                MessageFieldInvalidValue::EnumEntryValue(value) => {
                    spec::Invalid::EnumEntryValue(spec::EnumEntryValue {
                        name: value.clone(),
                    })
                }
            }),
        }
    }

    /// Constructs from Protobuf [`proto::MessageFieldInvalidValue`]
    pub fn from_proto(proto: &proto::MessageFieldInvalidValue) -> Result<Self, ProtoImportError> {
        use crate::proto::mavlink_messages_v1::message_field_invalid_value as spec;

        Ok(match &proto.invalid {
            Some(spec::Invalid::Value(value)) => Self::Value(Value::from_proto(value.clone())?),
            Some(spec::Invalid::AllItems(value)) => {
                Self::AllItems(Value::from_proto(value.clone())?)
            }
            Some(spec::Invalid::FirstItem(value)) => {
                Self::FirstItem(Value::from_proto(value.clone())?)
            }
            Some(spec::Invalid::EnumEntryValue(val)) => Self::EnumEntryValue(val.name.clone()),
            None => return Err(ProtoImportError::MessageFieldInvalidValueValueIsNone),
        })
    }
}

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

    #[test]
    fn first_list_value_as_invalid_pattern_is_correct() {
        let re = Regex::new(FIRST_LIST_VALUE_AS_INVALID_REGEX).unwrap();

        assert!(re.is_match("[1234]"));
        assert!(re.is_match("[-1234]"));
        assert!(re.is_match("[12.34]"));
        assert!(re.is_match("[-12.34]"));

        // Integer value
        {
            let captures = re.captures("[-1234]").unwrap();
            assert_eq!(captures.get(1).unwrap().as_str(), "-1234");
            assert_eq!(captures.get(2).unwrap().as_str(), "-1234");
            // Fractional part is empty
            assert_eq!(captures.get(3), None);
            assert_eq!(captures.len(), 4);
        }
        // Float value
        {
            let captures = re.captures("[-12.34]").unwrap();
            assert_eq!(captures.get(1).unwrap().as_str(), "-12.34");
            assert_eq!(captures.get(2).unwrap().as_str(), "-12");
            assert_eq!(captures.get(3).unwrap().as_str(), ".34");
            assert_eq!(captures.len(), 4);
        }
    }

    #[test]
    fn message_field_invalid_value_parser_is_correct() {
        // Scalar: maximum value
        assert!(matches!(
            MessageFieldInvalidValue::parse("INT16_MAX").unwrap(),
            MessageFieldInvalidValue::Value(Value::MaxValue),
        ));

        // Scalar: integer value
        assert!(matches!(
            MessageFieldInvalidValue::parse("-1234").unwrap(),
            MessageFieldInvalidValue::Value(Value::IntValue(-1234)),
        ));

        // Array: all items have integer value
        assert!(matches!(
            MessageFieldInvalidValue::parse("[-1234]").unwrap(),
            MessageFieldInvalidValue::AllItems(Value::IntValue(-1234)),
        ));

        // Array: all items are NaN
        {
            let parsed = MessageFieldInvalidValue::parse("[NaN]").unwrap();
            if let MessageFieldInvalidValue::AllItems(Value::FloatValue(value)) = parsed {
                assert!(value.is_nan());
            } else {
                panic!("Invalid parsing result: {:?}", parsed);
            }
        }

        // Array: all items have floating point value
        {
            let parsed = MessageFieldInvalidValue::parse("[-12.34]").unwrap();
            if let MessageFieldInvalidValue::AllItems(Value::FloatValue(value)) = parsed {
                assert_eq!(value, -12.34);
            } else {
                panic!("Invalid parsing result: {:?}", parsed);
            }
        }
    }

    #[test]
    fn conversion_to_proto_basics_are_correct() {
        use proto::message_field_invalid_value as spec;

        // NaN
        {
            let expected = proto::MessageFieldInvalidValue {
                invalid: Some(spec::Invalid::Value(proto::Value {
                    value: Some(proto::value::Value::FloatValue(proto::value::FloatValue {
                        value: Some(proto::value::float_value::Value::Nan(proto::value::NaN {})),
                    })),
                })),
            };
            let converted = MessageFieldInvalidValue::Value(Value::FloatValue(f64::NAN)).to_proto();

            if let proto::MessageFieldInvalidValue {
                invalid:
                    Some(spec::Invalid::Value(proto::Value {
                        value:
                            Some(proto::value::Value::FloatValue(proto::value::FloatValue {
                                value:
                                    Some(proto::value::float_value::Value::Nan(proto::value::NaN {})),
                            })),
                    })),
            } = converted
            {
            } else {
                panic!("Invalid conversion to proto. Expected: {expected:?}. Got: {converted:?}.")
            }
        }

        // Integer value
        {
            let expected = proto::MessageFieldInvalidValue {
                invalid: Some(spec::Invalid::Value(proto::Value {
                    value: Some(proto::value::Value::IntValue(-12)),
                })),
            };
            let converted = MessageFieldInvalidValue::Value(Value::IntValue(-12)).to_proto();

            if let proto::MessageFieldInvalidValue {
                invalid:
                    Some(spec::Invalid::Value(proto::Value {
                        value: Some(proto::value::Value::IntValue(-12)),
                    })),
            } = converted
            {
            } else {
                panic!("Invalid conversion to proto. Expected: {expected:?}. Got: {converted:?}.")
            }
        }

        // FloatValue
        {
            let expected = proto::MessageFieldInvalidValue {
                invalid: Some(spec::Invalid::Value(proto::Value {
                    value: Some(proto::value::Value::FloatValue(proto::value::FloatValue {
                        value: Some(proto::value::float_value::Value::Data(1.2)),
                    })),
                })),
            };
            let converted = MessageFieldInvalidValue::Value(Value::FloatValue(1.2)).to_proto();

            if let proto::MessageFieldInvalidValue {
                invalid:
                    Some(spec::Invalid::Value(proto::Value {
                        value:
                            Some(proto::value::Value::FloatValue(proto::value::FloatValue {
                                value: Some(proto::value::float_value::Value::Data(val)),
                            })),
                    })),
            } = converted
            {
                if val != 1.2 {
                    panic!(
                        "Invalid float value during conversion to proto. Expected: {expected:?}. Got: {converted:?}."
                    )
                }
            } else {
                panic!("Invalid conversion to proto. Expected: {expected:?}. Got: {converted:?}.")
            }
        }
    }

    #[test]
    fn conversion_to_proto_is_reversible() {
        // NaN
        {
            let original = MessageFieldInvalidValue::Value(Value::FloatValue(f64::NAN));
            let reversed = MessageFieldInvalidValue::from_proto(&original.to_proto()).unwrap();
            if let MessageFieldInvalidValue::Value(Value::FloatValue(val)) = reversed {
                assert!(val.is_nan());
            } else {
                panic!(
                    "Message was damaged during reverse. Expected: {original:?}. Got: {reversed:?}"
                );
            }
        }

        // Integer value
        {
            let original = MessageFieldInvalidValue::Value(Value::IntValue(-12));
            let reversed = MessageFieldInvalidValue::from_proto(&original.to_proto()).unwrap();
            if let MessageFieldInvalidValue::Value(Value::IntValue(-12)) = reversed {
            } else {
                panic!(
                    "Message was damaged during reverse. Expected: {original:?}. Got: {reversed:?}"
                );
            }
        }

        // Floating point value
        {
            let value: f64 = 1.2;
            let original = MessageFieldInvalidValue::Value(Value::FloatValue(value));
            let reversed = MessageFieldInvalidValue::from_proto(&original.to_proto()).unwrap();
            if let MessageFieldInvalidValue::Value(Value::FloatValue(val)) = reversed {
                if val != value {
                    panic!(
                        "Message float value was damaged during reverse. Expected: {original:?}. Got: {reversed:?}"
                    );
                }
            } else {
                panic!(
                    "Message was damaged during reverse. Expected: {original:?}. Got: {reversed:?}"
                );
            }
        }
    }
}