mavinspect 0.1.0-alpha2

MAVInspect is a CLI tool and a library to parse and inspect MAVLink protocol XML definitions
Documentation
mod cli {
    use std::path::Path;

    #[cfg(feature = "serde")]
    use std::{fs::File, io::BufWriter, time::Instant};

    use clap::{Parser, Subcommand};
    use thiserror::Error;

    #[cfg(feature = "serde")]
    use serde::Serialize;

    use mavinspect::parser::MAVInspector;

    #[cfg(feature = "serde")]
    use mavinspect::proto::mavlink_messages_v1 as proto;
    #[cfg(feature = "serde")]
    use mavinspect::protocol;

    /// Default server host
    const DEFAULT_HOST: &str = "[::1]";
    /// Default server port
    const DEFAULT_PORT: u16 = 50051;
    /// Default directory path for MAVLink message definitions
    pub const DEFAULT_SRC_PATH: &str = ".";
    /// Default directory path for serialized MAVLink message definitions.
    pub const DEFAULT_OUT_PATH: &str = ".";

    /// CLI errors.
    #[derive(Error, Debug)]
    pub enum CliError {
        #[error("this functionality requires `server` feature")]
        ServerFeatureRequired,
        #[cfg(not(feature = "serde"))]
        #[error("this functionality requires `serde` feature")]
        SerdeFeatureRequired,
    }

    /// MAVInspect, a MAVLink protocol parser
    #[derive(Parser, Debug)]
    #[command(author, version, about, long_about = None)]
    pub struct Cli {
        #[command(subcommand)]
        pub command: Commands,

        /// Path to MAVLink XML definitions
        #[arg(long, default_value = DEFAULT_SRC_PATH)]
        pub src: String,
    }

    /// CLI commands.
    #[derive(Debug, Subcommand)]
    pub enum Commands {
        /// List dialects
        Ls {},
        /// Parse MAVLink protocol XML definitions
        Parse {
            /// Path to the directory where parsed definitions will be stored
            #[arg(long, short = 'O', default_value = DEFAULT_OUT_PATH)]
            out: String,

            /// Parse as Protobuf
            ///
            /// Protobuf definitions can be found in
            /// https://gitlab.com/mavka/spec/protocols/mavlink/message-definitions-v1.
            #[arg(long, default_value_t = false)]
            as_proto: bool,

            /// Prettify
            #[arg(long, default_value_t = false)]
            pretty: bool,
        },
        /// Serves MAVLink protocol
        Serve {
            /// Server host
            #[arg(long, short = 'H', default_value = DEFAULT_HOST)]
            host: String,

            /// Server port
            #[arg(long, short = 'P', default_value_t = DEFAULT_PORT)]
            port: u16,
        },
    }

    pub fn realpath(path: &String) -> String {
        Path::new(path)
            .canonicalize()
            .unwrap()
            .to_str()
            .unwrap()
            .to_string()
    }

    pub fn list_dialect_definitions(inspector: &MAVInspector) {
        if inspector.definitions().is_empty() {
            println!("No dialects were parsed.");
            return;
        }

        println!("\r\nDialects:");

        for dialect in inspector.definitions() {
            println!("\t{}", dialect.name());
            println!("\t\tpath: {}", dialect.path());
            println!("\t\tversion: {:?}", dialect.version());
            println!("\t\tdialect: {:?}", dialect.dialect());
            println!(
                "\t\tincludes: {:?}",
                dialect
                    .includes()
                    .iter()
                    .map(|i| { i.name().to_string() })
                    .collect::<Vec<String>>()
            );
        }
    }

    #[cfg(feature = "serde")]
    pub fn parse_dialect_definitions(
        out: &str,
        as_proto: bool,
        pretty: bool,
        inspector: &MAVInspector,
    ) {
        println!("\r\nParsing MAVLink protocol XML definitions");
        let protocol = inspector.parse().unwrap();
        println!("\r\nParsed {} dialects.", protocol.dialects().len());

        println!("\r\nWriting dialects");
        println!("\tpath: {out}");

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

        println!("\r\nDialects:");

        for (name, dialect) in protocol.dialects() {
            let (writer, display_path) = open_file_writer(out, format!("{}.json", name).as_str());
            println!("\t{name}");
            println!("\t\tpath: {display_path}");

            if pretty {
                println!("\t\tUse pretty JSON output.");
            }

            if as_proto {
                let proto_specs = dialect.to_proto();
                println!("\t\tas Protobuf: true");

                write_data::<proto::Dialect>(writer, &proto_specs, pretty);
            } else {
                write_data::<protocol::Dialect>(writer, dialect, pretty);
            }
        }

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

        println!("\n\rWriting completed");
        println!("\tduration: {}s", (duration.as_micros() as f64) / 1000000.0);
    }

    #[cfg(feature = "serde")]
    fn open_file_writer(base: &str, file_path: &str) -> (BufWriter<File>, String) {
        // Create a path to the desired file
        let path = Path::new(base).join(Path::new(file_path));
        let display = path.display();

        // Open a file in write-only mode, returns `io::Result<File>`
        let file = match File::create(&path) {
            Err(why) => panic!("couldn't create file {}: {}", display, why),
            Ok(file) => file,
        };

        (
            BufWriter::new(file),
            path.canonicalize().unwrap().display().to_string(),
        )
    }

    #[cfg(feature = "serde")]
    fn write_data<T: Serialize>(writer: BufWriter<File>, data: &T, pretty: bool) {
        if pretty {
            serde_json::to_writer_pretty(writer, data).unwrap();
        } else {
            serde_json::to_writer(writer, data).unwrap();
        }
    }

    pub fn setup() -> (Cli, MAVInspector) {
        // Parse arguments
        let cli: Cli = Cli::parse();

        println!("[MAVInspect]");

        let src_path = realpath(&cli.src);

        println!(
            "\nLoading XML definitions\n\
        \tsource: {}",
            &src_path
        );

        let inspector = MAVInspector::new(&src_path).unwrap();

        (cli, inspector)
    }

    pub fn process(cli: &Cli, inspector: &MAVInspector) -> Result<(), CliError> {
        match &cli.command {
            Commands::Ls {} => {
                list_dialect_definitions(inspector);
            }
            #[cfg(feature = "serde")]
            Commands::Parse {
                out,
                as_proto,
                pretty,
            } => {
                parse_dialect_definitions(out, *as_proto, *pretty, inspector);
            }
            #[cfg(not(feature = "serde"))]
            Commands::Parse { .. } => {
                // We can't save parsed results if `serde` is not available.
                // Therefore, aborting.
                return Err(CliError::SerdeFeatureRequired);
            }
            Commands::Serve { .. } => return Err(CliError::ServerFeatureRequired),
        }

        Ok(())
    }
}

#[cfg(feature = "server")]
mod server {
    use mavinspect::parser::MAVInspector;
    use tonic::transport::Server;

    use mavinspect::server::MavInspectServerImplementation;
    use mavinspect::server::MavInspectServiceServer;

    #[cfg(feature = "reflection")]
    /// Protobuf descriptor set (necessary for gRPC reflection).
    mod proto_descriptor {
        pub(crate) const FILE_DESCRIPTOR_SET: &[u8] =
            tonic::include_file_descriptor_set!("proto_mavinspect_descriptor");
    }

    pub async fn serve(
        host: &str,
        port: u16,
        inspector: &MAVInspector,
    ) -> Result<(), Box<dyn std::error::Error>> {
        let addr = format!("{host}:{port}").parse().unwrap();
        let protocol = inspector.parse().unwrap().to_proto();
        let mavinspect = MavInspectServerImplementation::new(protocol);

        // Construct a
        let router = Server::builder().add_service(MavInspectServiceServer::new(mavinspect));

        // Setup reflection service if `reflection` feature is enabled
        #[cfg(feature = "reflection")]
        let router = {
            let reflection_service = tonic_reflection::server::Builder::configure()
                .register_encoded_file_descriptor_set(proto_descriptor::FILE_DESCRIPTOR_SET)
                .build()
                .unwrap();
            router.add_service(reflection_service)
        };

        // Setup health check service if `health` feature is enabled
        #[cfg(feature = "health")]
        let router = {
            let (mut health_reporter, health_service) = tonic_health::server::health_reporter();
            health_reporter
                .set_serving::<MavInspectServiceServer<MavInspectServerImplementation>>()
                .await;
            router.add_service(health_service)
        };

        println!("\n\rMAVInspect gRPC server is listening on {}\n", addr);
        router.serve(addr).await?;

        Ok(())
    }
}

#[cfg(not(feature = "server"))]
fn main() {
    use std::process::exit;

    use cli::*;

    // Setup
    let (cli, inspector) = setup();

    match process(&cli, &inspector) {
        Ok(_) => {}
        Err(CliError::ServerFeatureRequired) => {
            eprint!("\n\r[ERROR] Server is not available! Build MAVInspect with 'server' feature.");
            exit(-1);
        }
        #[cfg(not(feature = "serde"))]
        Err(CliError::SerdeFeatureRequired) => {
            eprint!("\n\r[ERROR] Parser is not available! Build MAVInspect with 'serde' feature.");
            exit(-1);
        }
    }
}

#[cfg(feature = "server")]
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    use cli::*;

    // Setup
    let (cli, inspector) = setup();

    // Process default cli commands, then check for server-related commands
    match process(&cli, &inspector) {
        Ok(_) => Ok(()),
        Err(_) => match &cli.command {
            Commands::Serve { host, port } => {
                server::serve(host, *port, &inspector).await?;
                Ok(())
            }
            &_ => panic!("Incorrect command {:?} in server context!", &cli.command),
        },
    }
}