qair 0.7.0

Send/receive files with builtin HTTP server and terminal QR code.
Documentation
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

//! Defines the command line arguments

use std::path::PathBuf;
use structopt::StructOpt;

/// Send/receive files with builtin HTTP server and terminal QR code
#[derive(StructOpt, Debug)]
#[structopt(
    setting = structopt::clap::AppSettings::ColoredHelp
)]
pub struct Arguments {
    /// Do not deeplink in URL in QR code, link to an index.html page
    #[structopt(short = "i", long = "index")]
    pub index: bool,

    /// Allow receiving files (clients can upload). Implies '-i'.
    #[structopt(short = "u")]
    pub receive: bool,

    /// Use given hostname/IP in URL instead of autodetecting
    #[structopt(long = "host")]
    pub host: Option<String>,

    /// Listen on given port
    #[structopt(short = "p", long = "port", default_value = "1234")]
    pub port: u16,

    /// Show QR of given text (do not send/receive files)
    #[structopt(long = "show")]
    pub show: Option<String>,

    /// File to send
    #[structopt(name = "FILE", parse(from_os_str))]
    pub file: Option<PathBuf>,
}

impl Arguments {
    /// Checks for inconsistencies in command line arguments.
    ///
    /// Returns error describing the inconsistency, or `Ok(())` if consistent.
    ///
    /// We have this function instead of letting clap check constraints, because
    /// clap's constraint checking is a bit rough, see for example
    /// https://github.com/clap-rs/clap/issues/1464
    ///
    /// The following constraints are checked:
    /// - `show` cannot be used together with: `index`, `receive`, `host`,
    ///   `port` (*), `file`.
    /// - an action must be specified (`file`, `show` or `receive`)
    ///
    /// (*) Because clap/structopt does not allow differentiating between the
    /// default argument value and not specifying the argument value, we cannot
    /// disallow e.g. `qair --port 1234 --show hi`, so that is not checked here.
    pub fn check_constraints(&self) -> Result<(), String> {
        if self.file == None && !self.receive && self.show == None {
            Err(
                "Error: nothing to do. Try 'qair -u' to receive, or 'qair FILE' to send."
                    .to_string(),
            )
        } else if self.show != None && self.file != None {
            Err("Error: '--show' cannot be used together with sending files. Try 'qair --show \"my text\"'.".to_string())
        } else if self.show != None && self.receive {
            Err("Error: '--show' cannot be used together with receiving files. Try 'qair --show \"my text\"'.".to_string())
        } else if self.show != None && self.host != None {
            Err("Error: '--show' cannot be used together with a custom hostname. Try 'qair --show \"my text\"'.".to_string())
        } else if self.show != None && self.index {
            Err("Error: '--show' cannot be used together with index ('-i'). Try 'qair --show \"my text\"'.".to_string())
        } else {
            Ok(())
        }
    }
}

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

    /// Returns true iff the given arguments are parseable and in addition considered consistent.
    fn is_valid_arguments(arguments: &[&str]) -> bool {
        let arguments_with_exec_name = [&["executable name"], arguments].concat();
        let parsed_result = Arguments::from_iter_safe(arguments_with_exec_name);
        match parsed_result {
            Err(err) if err.kind == structopt::clap::ErrorKind::UnknownArgument => panic!(
                "Unknown argument. We fail on unknown arguments to catch typos in arguments in unit tests themselves. Clap error: {}", err
            ),
            Err(_) => false,
            Ok(parsed) => parsed.check_constraints().is_ok(),
        }
    }

    #[test]
    fn test_parseable_and_consistent() {
        // Given: command line arguments that are not parseable or are inconsistent.
        // When: parsing and consistency-checking these valid arguments.
        // Then: they are parseable and considered consistent.
        assert!(is_valid_arguments(&["file.txt"]));
        assert!(is_valid_arguments(&["-u", "file.txt"]));
        assert!(is_valid_arguments(&["-i", "file.txt"]));
        assert!(is_valid_arguments(&["--index", "file.txt"]));
        assert!(is_valid_arguments(&["file.txt", "-u"]));
        assert!(is_valid_arguments(&["--show", "hi"]));
        assert!(is_valid_arguments(&["-p", "1234", "file.txt"]));
        assert!(is_valid_arguments(&["--port", "1234", "-u"]));
        assert!(is_valid_arguments(&["--host", "192.168.5.4", "file.txt"]));
        assert!(is_valid_arguments(&[
            "-i",
            "file.txt",
            "--host",
            "example.org"
        ]));
        assert!(is_valid_arguments(&["-i", "-u", "file.txt"]));
        assert!(is_valid_arguments(&[
            "--host",
            "192.168.5.4",
            "-u",
            "-i",
            "file.txt"
        ]));
        assert!(is_valid_arguments(&[
            "-p", "1234", "--host", "10.1.2.3", "file.txt"
        ]));
        assert!(is_valid_arguments(&[
            "--host",
            "example.org",
            "-p",
            "1234",
            "-u"
        ]));
    }

    #[test]
    fn test_show_with_incompatible_argument() {
        // Given: command line arguments with `--show` and an argument incompatible with `--show`.
        // When: parsing&consistency-checking the arguments.
        // Then: the arguments are not considered parseable-and-consistent.
        assert!(!is_valid_arguments(&["-i", "--show", "hi"]));
        assert!(!is_valid_arguments(&["--index", "--show", "hi"]));
        assert!(!is_valid_arguments(&["-u", "--show", "hi"]));
        assert!(!is_valid_arguments(&["file.txt", "--show", "hi"]));
        assert!(!is_valid_arguments(&[
            "--host",
            "192.168.1.2",
            "--show",
            "hi"
        ]));
    }

    #[test]
    fn test_show_with_multiple_incompatible_arguments() {
        // Given: command line arguments containing `--show` and muiltple
        //        arguments incompatible with `--show`.
        // When: parsing&consistency-checking the arguments.
        // Then: the arguments are not considered parseable-and-consistent.
        assert!(!is_valid_arguments(&["-u", "-i", "--show", "hi"]));
        assert!(!is_valid_arguments(&["-u", "--index", "--show", "hi"]));
        assert!(!is_valid_arguments(&[
            "--host", "here", "-u", "-i", "--show", "hi"
        ]));
        assert!(!is_valid_arguments(&[
            "-p",
            "1235",
            "--host",
            "192.168.1.2",
            "--show",
            "hi"
        ]));
    }

    #[test]
    fn test_no_action() {
        // Given: command line arguments that do not specify an action.
        // When: parsing&consistency-checking the arguments.
        // Then: the arguments are not considered parseable-and-consistent.
        assert!(!is_valid_arguments(&[]));
        assert!(!is_valid_arguments(&["-p", "1234"]));
        assert!(!is_valid_arguments(&["-p", "1235"]));
        assert!(!is_valid_arguments(&["--host", "here"]));
        assert!(!is_valid_arguments(&["-i"]));
        assert!(!is_valid_arguments(&["--index"]));
        assert!(!is_valid_arguments(&[
            "--port",
            "1234",
            "--host",
            "192.168.1.2"
        ]));
    }

    #[test]
    fn test_argument_without_value() {
        // Given: command line arguments where a parameter is specified
        //        without argument value
        // When: parsing&consistency-checking the arguments.
        // Then: the arguments are not considered parseable-and-consistent.
        assert!(!is_valid_arguments(&["--show"]));
        assert!(!is_valid_arguments(&["file.txt", "--host"]));

        // Apparently clap allows this (and uses default value for `-p`).
        // Might be personal preference, so let's just leave it.
        // assert!(!is_valid_arguments(&["-u", "-p"]));
        // assert!(!is_valid_arguments(&["-p", "-u"]));
    }
}