mavinspect 0.1.0-alpha2

MAVInspect is a CLI tool and a library to parse and inspect MAVLink protocol XML definitions
Documentation
use std::collections::HashMap;
use std::fs;
use std::time::Instant;

use quick_xml::reader::Reader;

use crate::protocol::{Dialect, Protocol};

use super::errors::XmlParseError;
use super::xml::XmlParser;
use super::xml_definition::MavLinkXmlDialectDefinition;

/// Discovers and parses MAVLink XML definitions.
#[derive(Debug)]
pub struct MAVInspector {
    src: String,
    definitions: Vec<MavLinkXmlDialectDefinition>,
}

/// MAVLink protocol parser.
impl MAVInspector {
    /// Default constructor.
    ///
    /// # Arguments
    ///
    /// * `src` - path to message definition directory.
    ///
    /// # Examples
    ///
    /// Load dialects from `./message_definitions` and get `HEARTBEAT_MESSAGE` from `minimal`
    /// dialect:
    ///
    /// ```rust
    /// use mavinspect::parser::MAVInspector;    ///
    ///
    /// // Instantiate inspector and load list of XML definitions
    /// let inspector = MAVInspector::new("./message_definitions").unwrap();
    /// // Parse all XML definitions
    /// let protocol = inspector.parse().unwrap();
    ///   
    /// // Get `minimal` dialect
    /// let minimal = protocol.dialects().get("minimal").unwrap();
    /// // Get heartbit message  
    /// let heartbeat_message = minimal.messages().get(&0u32).unwrap();
    ///   
    /// assert_eq!(heartbeat_message.name(), "HEARTBEAT");
    /// println!("`HEARTBEAT` message: {:?}", heartbeat_message);
    /// ```
    pub fn new(src: &str) -> std::io::Result<Self> {
        Ok(MAVInspector {
            src: src.to_string(),
            definitions: Self::discover_definitions(src)?,
        })
    }

    /// Returns path to MAVLink message definitions.
    pub fn src(&self) -> &String {
        &self.src
    }

    /// Returns a list of dialect definitions or [`None`] if nothing was parsed.
    pub fn definitions(&self) -> &Vec<MavLinkXmlDialectDefinition> {
        &self.definitions
    }

    /// Discovers MAVLink dialects XML definitions within provided path.
    pub fn discover_definitions(path: &str) -> std::io::Result<Vec<MavLinkXmlDialectDefinition>> {
        let mut dialects: Vec<MavLinkXmlDialectDefinition> = Vec::new();

        for entry in fs::read_dir(path)? {
            let entry_path = entry?.path();

            if entry_path.is_file()
                && entry_path
                    .extension()
                    .unwrap()
                    .to_str()
                    .unwrap()
                    .to_lowercase()
                    .eq("xml")
            {
                let path = entry_path.to_str().unwrap().to_string();
                dialects.push(MavLinkXmlDialectDefinition::new(&path));
            }
        }

        Ok(dialects)
    }

    /// Parses XML definitions.
    pub fn parse(&self) -> Result<Protocol, XmlParseError> {
        let mut dialects: HashMap<String, Dialect> = HashMap::new();

        println!("\n\rDialects:");

        // Parsing started
        let started_at = Instant::now();

        // Iterate through definitions and parse dialects
        for def in &self.definitions {
            // Do nothing if this dialect has already been parsed
            if dialects.contains_key(def.name()) {
                break;
            }

            Self::parse_definition(def, &mut dialects)?;
        }

        // Calculate parsing duration
        let ended_at = Instant::now();
        let duration = ended_at - started_at;

        println!("\n\rParsed:");
        println!("\tdialects: {:?}", dialects.keys());
        println!("\tduration: {}s", (duration.as_micros() as f64) / 1000000.0);

        Ok(Protocol::new(dialects))
    }

    /// Parses MAVLink XML message definition.
    ///
    /// This function will update provided dialects map potentially loading and parsing dialect
    /// dependencies.
    ///
    /// If dialect has been already parsed, this function just returns parsed dialect.
    ///
    /// # Arguments
    ///
    /// * `definition` - MAVLink XML definition.
    /// * `dialects` - mutable map for dialects.
    pub fn parse_definition<'a>(
        definition: &MavLinkXmlDialectDefinition,
        dialects: &'a mut HashMap<String, Dialect>,
    ) -> Result<&'a Dialect, XmlParseError> {
        // Return already parsed dialect
        if dialects.contains_key(definition.name()) {
            // The following `unwrap` is OK as we know that this dialect exists
            return Ok(dialects.get(definition.name()).unwrap());
        }

        // Load all dependencies
        for dependency in definition.includes() {
            Self::parse_definition(dependency, dialects)?;
        }

        let mut enums = HashMap::new();
        let mut messages = HashMap::new();

        // Collect all enums from dependencies
        for dependency in definition.includes() {
            for (name, enm) in dialects.get(dependency.name()).unwrap().enums() {
                enums.insert(name.clone(), enm.clone());
            }
        }

        // Collect all messages from dependencies
        for dependency in definition.includes() {
            for (&id, message) in dialects.get(dependency.name()).unwrap().messages() {
                messages.insert(id, message.clone());
            }
        }

        // Parsing started
        let started_at = Instant::now();

        // Parse dialect entries from definition
        let mut parser: XmlParser = XmlParser::new(&mut enums, &mut messages);
        let mut file_reader = Reader::from_file(definition.path()).unwrap();
        parser.parse(definition.name(), &mut file_reader)?;

        // Construct and save dialect
        dialects.insert(
            definition.name().clone(),
            Dialect::new(
                definition.name().clone(),
                definition.version(),
                definition.dialect(),
                messages,
                enums,
            ),
        );

        // Calculate parsing duration
        let ended_at = Instant::now();
        let duration = ended_at - started_at;

        println!("\r\t{}", definition.name());
        println!("\t\tpath: {}", definition.path());
        println!("\t\tversion: {:?}", definition.version());
        println!("\t\tdialect #: {:?}", definition.dialect());
        println!(
            "\t\tduration: {}s",
            (duration.as_micros() as f64) / 1000000.0
        );

        // The following `unwrap` is OK as we've just inserted this dialect
        Ok(dialects.get(definition.name()).unwrap())
    }
}

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

    const DEFAULT_DIALECTS_PATH: &str = "./mavlink/message_definitions/v1.0";

    /// Tests that MAVLink message definitions available ot default path
    #[test]
    fn dialects_are_available() {
        let parser = MAVInspector::new(DEFAULT_DIALECTS_PATH).unwrap();
        assert!(!parser.definitions().is_empty())
    }
}