clapi 0.1.2

A framework for create command-line applications
Documentation
#![allow(clippy::len_zero)]
use crate::args::{Argument, ArgumentList};
use std::fmt::{Debug, Display};
use std::hash::{Hash, Hasher};
use std::ops::Index;
use std::str::FromStr;
use crate::{Error, ErrorKind, Result};

/// Represents a command-line option.
#[derive(Debug, Clone)]
pub struct CommandOption {
    name: String,
    aliases: Vec<String>,
    description: Option<String>,
    args: ArgumentList,
    is_required: bool,
    is_hidden: bool,
    is_global: bool,
    allow_multiple: bool,
    requires_assign: bool,
}

impl CommandOption {
    /// Constructs a new `CommandOption`.
    ///
    /// # Panics:
    /// Panics if the `name` is empty.
    ///
    /// # Example
    /// ```
    /// use clapi::{Command, CommandOption};
    ///
    /// let result = Command::new("MyApp")
    ///     .option(CommandOption::new("enable"))
    ///     .parse_from(vec!["--enable"])
    ///     .unwrap();
    ///
    /// assert!(result.options().contains("enable"));
    /// ```
    pub fn new<S: Into<String>>(name: S) -> Self {
        let name = name.into();
        assert!(!name.is_empty(), "option `name` cannot be empty");

        CommandOption {
            name,
            aliases: Vec::new(),
            description: None,
            args: ArgumentList::new(),
            is_required: false,
            is_hidden: false,
            is_global: false,
            allow_multiple: false,
            requires_assign: false,
        }
    }

    /// Returns the name of this option.
    pub fn get_name(&self) -> &str {
        self.name.as_str()
    }

    /// Returns an `Iterator` over the aliases of this option.
    pub fn get_aliases(&self) -> Aliases<'_> {
        Aliases {
            iter: self.aliases.iter(),
        }
    }

    /// Returns a short description of this option or `None` if not set.
    pub fn get_description(&self) -> Option<&str> {
        self.description.as_ref().map(|s| s.as_ref())
    }

    /// Returns `true` if this option is required.
    pub fn is_required(&self) -> bool {
        self.is_required
    }

    /// Returns `true` if this option is no visible for `help`.
    pub fn is_hidden(&self) -> bool {
        self.is_hidden
    }

    /// Returns `true` if this is an global option.
    pub fn is_global(&self) -> bool {
        self.is_global
    }

    /// Returns `true` if this option is allowed to appear multiple times.
    pub fn allow_multiple(&self) -> bool {
        self.allow_multiple
    }

    /// Returns `true` if the option requires an assign operator.
    pub fn is_assign_required(&self) -> bool {
        self.requires_assign
    }

    /// Returns the `Argument` this option takes or `None` if have more than 1 argument.
    pub fn get_arg(&self) -> Option<&Argument> {
        if self.args.len() > 1 {
            None
        } else {
            Some(&self.args[0])
        }
    }

    /// Returns the `Arguments` of this option.
    pub fn get_args(&self) -> &ArgumentList {
        &self.args
    }

    /// Returns `true` if this option take arguments.
    pub fn take_args(&self) -> bool {
        self.args.len() > 0
    }

    /// Returns `true` if option contains the specified alias.
    pub fn has_alias<S: AsRef<str>>(&self, alias: S) -> bool {
        self.aliases.iter().any(|s| s == alias.as_ref())
    }

    /// Adds a new alias to this option.
    ///
    /// # Panics:
    /// Panics if the `alias` is empty.
    ///
    /// # Example
    /// ```
    /// use clapi::{Command, CommandOption};
    ///
    /// let result = Command::new("MyApp")
    ///     .option(CommandOption::new("test").alias("t"))
    ///     .parse_from(vec!["-t"])
    ///     .unwrap();
    ///
    /// assert!(result.options().contains("test"));
    /// ```
    pub fn alias<S: Into<String>>(mut self, alias: S) -> Self {
        let alias = alias.into();
        assert!(!alias.is_empty(), "option `alias` cannot be empty");
        self.aliases.push(alias);
        self
    }

    /// Sets a short description of this option.
    ///
    /// # Example
    /// ```
    /// use clapi::CommandOption;
    ///
    /// let option = CommandOption::new("test")
    ///     .description("Enable tests");
    ///
    /// assert_eq!(option.get_description(), Some("Enable tests"));
    /// ```
    pub fn description<S: Into<String>>(mut self, description: S) -> Self {
        self.description = Some(description.into());
        self
    }

    /// Specify if this option is required, by default is `false`.
    ///
    /// # Examples
    /// ```
    /// use clapi::{Command, CommandOption, Argument};
    /// use clapi::validator::validate_type;
    ///
    /// let result = Command::new("MyApp")
    ///     .option(CommandOption::new("test"))
    ///     .option(CommandOption::new("number")
    ///         .required(true)
    ///         .arg(Argument::new()
    ///             .validator(validate_type::<i64>())))
    ///     .parse_from(vec!["--test", "--number", "10"])
    ///     .unwrap();
    ///
    /// assert!(result.options().get_arg("number").unwrap().contains("10"));
    /// assert!(result.options().contains("test"));
    /// ```
    ///
    /// Other example where the option is ommited
    /// ```
    /// use clapi::{Command, CommandOption, Argument};
    /// use clapi::validator::validate_type;
    ///
    /// let result = Command::new("MyApp")
    ///     .option(CommandOption::new("test"))
    ///     .option(CommandOption::new("number")
    ///         .required(true)
    ///         .arg(Argument::new()
    ///             .validator(validate_type::<i64>())))
    ///     .parse_from(vec!["--test"]);
    ///
    /// assert!(result.is_err());
    /// ```
    pub fn required(mut self, is_required: bool) -> Self {
        self.is_required = is_required;
        self
    }

    /// Specify if this option is hidden for the `help`.
    ///
    /// # Example
    /// ```
    /// use clapi::CommandOption;
    ///
    /// let option = CommandOption::new("enable").hidden(true);
    /// assert!(option.is_hidden());
    /// ```
    pub fn hidden(mut self, is_hidden: bool) -> Self {
        self.is_hidden = is_hidden;
        self
    }

    /// Specify if this option can appear multiple times.
    ///
    /// # Example
    /// ```
    /// use clapi::{Command, CommandOption, Argument};
    ///
    /// let result = Command::new("MyApp")
    ///     .option(CommandOption::new("numbers")
    ///         .multiple(true)
    ///         .arg(Argument::new().min_values(1)))
    ///     .parse_from(vec!["--numbers", "10", "--numbers", "20", "--numbers", "30"])
    ///     .unwrap();
    ///
    /// assert!(result.options().get_arg("numbers").unwrap().contains("10"));
    /// assert!(result.options().get_arg("numbers").unwrap().contains("20"));
    /// assert!(result.options().get_arg("numbers").unwrap().contains("30"));
    /// ```
    pub fn multiple(mut self, allow_multiple: bool) -> Self {
        self.allow_multiple = allow_multiple;
        self
    }

    /// Specify if this is a global option.
    pub fn global(mut self, is_global: bool) -> Self {
        self.is_global = is_global;
        self
    }

    /// Specify if this option requires an assign operator.
    ///
    /// # Example
    /// ```
    /// use clapi::{Command, CommandOption, Argument};
    ///
    /// let result = Command::new("MyApp")
    ///     .option(CommandOption::new("numbers")
    ///         .requires_assign(true)
    ///         .arg(Argument::new().min_values(1)))
    ///     .parse_from(vec!["--numbers=10,20,30"])
    ///     .unwrap();
    ///
    /// assert!(result.options().get_arg("numbers").unwrap().contains("10"));
    /// assert!(result.options().get_arg("numbers").unwrap().contains("20"));
    /// assert!(result.options().get_arg("numbers").unwrap().contains("30"));
    /// ```
    ///
    /// Using it like this will fail
    /// ```
    /// use clapi::{Command, CommandOption, Argument};
    ///
    /// let result = Command::new("MyApp")
    ///     .option(CommandOption::new("numbers")
    ///         .requires_assign(true)
    ///         .arg(Argument::new().min_values(1)))
    ///     .parse_from(vec!["--numbers", "10", "20", "30"]);
    ///
    /// assert!(result.is_err());
    /// ```
    pub fn requires_assign(mut self, requires_assign: bool) -> Self {
        self.requires_assign = requires_assign;
        self
    }

    /// Adds a new `Argument` to this option.
    ///
    /// # Example
    /// ```
    /// use clapi::{Command, CommandOption, Argument};
    ///
    /// let result = Command::new("MyApp")
    ///     .option(CommandOption::new("copy")
    ///         .arg(Argument::with_name("from"))
    ///         .arg(Argument::with_name("to")))
    ///     .parse_from(vec!["--copy", "/src/file.txt", "/src/utils/"])
    ///     .unwrap();
    ///
    /// assert!(result.options().get_args("copy").unwrap().get("from").unwrap().contains("/src/file.txt"));
    /// assert!(result.options().get_args("copy").unwrap().get("to").unwrap().contains("/src/utils/"));
    /// ```
    pub fn arg(mut self, mut arg: Argument) -> Self {
        arg.set_name_and_description_if_none(self.get_name(), self.get_description());

        if let Err(duplicated) = self.args.add(arg) {
            panic!(
                "`{}` already contains an argument named: `{}`",
                self.name,
                duplicated.get_name()
            );
        }
        self
    }

    /// Sets the arguments of this option.
    ///
    /// # Example
    /// ```
    /// use clapi::{ArgumentList, Argument, CommandOption};
    ///
    /// let mut args = ArgumentList::new();
    /// args.add(Argument::with_name("from")).unwrap();
    /// args.add(Argument::with_name("to")).unwrap();
    ///
    /// let option = CommandOption::new("copy").args(args);
    /// assert!(option.get_args().contains("from"));
    /// assert!(option.get_args().contains("to"));
    /// ```
    pub fn args(mut self, args: ArgumentList) -> Self {
        self.args = args;
        self
    }
}

impl Eq for CommandOption {}

impl PartialEq for CommandOption {
    fn eq(&self, other: &Self) -> bool {
        // This implementation is enough for the purposes of the library
        // but don't reflect the true equality of this struct
        self.name == other.name
    }
}

impl Hash for CommandOption {
    fn hash<H: Hasher>(&self, state: &mut H) {
        state.write(self.name.as_bytes())
    }
}

/// An iterator over the aliases of `CommandOption`.
#[derive(Debug, Clone)]
pub struct Aliases<'a> {
    iter: std::slice::Iter<'a, String>
}

impl<'a> Iterator for Aliases<'a> {
    type Item = &'a String;

    fn next(&mut self) -> Option<Self::Item> {
        self.iter.next()
    }
}

impl<'a> ExactSizeIterator for Aliases<'a> {
    fn len(&self) -> usize {
        self.iter.len()
    }
}

/// Represents a collection of `CommandOption`s.
#[derive(Default, Debug, Clone, Eq, PartialEq)]
pub struct OptionList {
    inner: Vec<CommandOption>,
}

impl OptionList {
    /// Constructs a new empty `Options`.
    pub fn new() -> Self {
        OptionList {
            inner: vec![],
        }
    }

    /// Adds the specified `CommandOption`.
    ///
    /// # Returns
    /// `false` if there is an option with the same alias than the provided one.
    pub fn add(&mut self, option: CommandOption) -> std::result::Result<(), CommandOption> {
        if self.is_option_duplicate(&option) {
            return Err(option);
        }

        self.inner.push(option);
        Ok(())
    }

    /// Adds the specified `CommandOption` or replace it it already exists,
    pub fn add_or_replace(&mut self, option: CommandOption) {
        if self.inner.contains(&option) {
            let pos = self.inner.iter().position(|o| o.get_name() == option.get_name()).unwrap();
            self.inner[pos] = option;
        } else {
            self.add(option).unwrap();
        }
    }

    /// Returns the `CommandOption` with the given name or alias or `None`
    /// if not found.
    pub fn get<S: AsRef<str>>(&self, name_or_alias: S) -> Option<&CommandOption> {
        self.inner.iter().find(|o| {
            o.name == name_or_alias.as_ref() || o.get_aliases().any(|s| s == name_or_alias.as_ref())
        })
    }

    /// Returns the `CommandOption` with the given name or `None` if not found.
    pub fn get_by_name<S: AsRef<str>>(&self, name: S) -> Option<&CommandOption> {
        self.inner
            .iter()
            .find(|opt| opt.get_name() == name.as_ref())
    }

    /// Returns the `CommandOption` with the given alias or `None` if not found.
    pub fn get_by_alias<S: AsRef<str>>(&self, alias: S) -> Option<&CommandOption> {
        self.inner.iter().find(|opt| opt.has_alias(alias.as_ref()))
    }

    /// Converts the argument value of the given option to the type `T` or results `Err` if:
    /// * The option is not found.
    /// * The option takes no arguments.
    /// * The option takes more than 1 argument.
    /// * The argument value parse fail.
    pub fn convert<T>(&self, option: &str) -> Result<T>
    where
        T: FromStr + 'static,
        <T as FromStr>::Err: Display {
        match self.get(option) {
            Some(opt) => {
                opt.get_arg()
                    .unwrap_or_else(|| panic!("`{}` takes no arguments", option))
                    .convert()
            },
            None => Err(Error::new(
                ErrorKind::Other,
                format!("cannot find option named '{}'", option))
            )
        }
    }

    /// Converts all the argument values of the given option to the type `T` or results `Err` if:
    /// * The option is not found.
    /// * The option takes no arguments.
    /// * The option takes more than 1 argument.
    /// * The argument values parse fail.
    pub fn convert_all<T>(&self, option: &str) -> Result<Vec<T>>
        where
            T: FromStr + 'static,
            <T as FromStr>::Err: Display {
        match self.get(option) {
            Some(opt) => {
                opt.get_arg()
                    .unwrap_or_else(|| panic!("`{}` takes no arguments", option))
                    .convert_all()
            },
            None => Err(Error::new(
                ErrorKind::Other,
                format!("cannot find option named '{}'", option))
            )
        }
    }

    /// Returns the `Argument` of the option with the given name or alias or
    /// `None` if the option cannot be found or have more than 1 argument.
    pub fn get_arg<S: AsRef<str>>(&self, option: S) -> Option<&Argument> {
        self.get(option.as_ref()).map(|o| o.get_arg()).flatten()
    }

    /// Returns the `ArgumentList` of the option with the given name or alias, or `None`
    /// if the option canno tbe found.
    pub fn get_args<S: AsRef<str>>(&self, option: S) -> Option<&ArgumentList> {
        self.get(option.as_ref()).map(|o| o.get_args())
    }

    /// Returns `true` if there is an option with the given name or alias.
    pub fn contains<S: AsRef<str>>(&self, option: S) -> bool {
        self.get(option).is_some()
    }

    /// Returns the number of options in this collection.
    pub fn len(&self) -> usize {
        self.inner.len()
    }

    /// Returns `true` if there is no options.
    pub fn is_empty(&self) -> bool {
        self.inner.is_empty()
    }

    /// Removes all the `Option`s.
    pub fn clear(&mut self) {
        self.inner.clear();
    }

    /// Returns an `ExactSizeIterator` over the `CommandOption` of this collection.
    pub fn iter(&self) -> Iter<'_> {
        Iter {
            iter: self.inner.iter(),
        }
    }

    fn is_option_duplicate(&self, option: &CommandOption) -> bool {
        // Check if there if any option that match the new option `alias` or `name`
        self.contains(&option.name) || option.get_aliases().any(|alias| self.contains(alias))
    }
}

/// An iterator over the `CommandOption`s of an option list.
#[derive(Debug, Clone)]
pub struct Iter<'a> {
    iter: std::slice::Iter<'a, CommandOption>,
}

/// An owning iterator over the `CommandOption`s of an option list.
pub struct IntoIter {
    iter: std::vec::IntoIter<CommandOption>
}

impl<'a> Iterator for Iter<'a> {
    type Item = &'a CommandOption;

    fn next(&mut self) -> Option<Self::Item> {
        self.iter.next()
    }
}

impl Iterator for IntoIter {
    type Item = CommandOption;

    fn next(&mut self) -> Option<Self::Item> {
        self.iter.next()
    }
}

impl<'a> ExactSizeIterator for Iter<'a> {
    fn len(&self) -> usize {
        self.iter.len()
    }
}

impl ExactSizeIterator for IntoIter {
    fn len(&self) -> usize {
        self.iter.len()
    }
}

impl<'a> IntoIterator for &'a OptionList {
    type Item = &'a CommandOption;
    type IntoIter = Iter<'a>;

    #[inline]
    fn into_iter(self) -> Self::IntoIter {
        self.iter()
    }
}

impl IntoIterator for OptionList {
    type Item = CommandOption;
    type IntoIter = IntoIter;

    #[inline]
    fn into_iter(self) -> Self::IntoIter {
        IntoIter {
            iter: self.inner.into_iter(),
        }
    }
}

impl Index<&str> for OptionList {
    type Output = CommandOption;

    fn index(&self, index: &str) -> &Self::Output {
        match self.get(index) {
            Some(option) => option,
            None => panic!("cannot find option named: `{}`", index),
        }
    }
}

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

    #[test]
    #[should_panic(expected = "option `name` cannot be empty")]
    fn option_empty_name_test() {
        CommandOption::new("");
    }

    #[test]
    #[should_panic(expected = "option `alias` cannot be empty")]
    fn option_empty_alias_test() {
        CommandOption::new("test").alias("");
    }

    #[test]
    fn option_name_with_whitespaces_test() {
        CommandOption::new("my option");
    }

    #[test]
    fn option_alias_with_whitespaces_test() {
        CommandOption::new("test").alias("m o");
    }

    #[test]
    fn alias_test() {
        let opt = CommandOption::new("name").alias("n").alias("nm");

        assert_eq!(opt.get_name(), "name");

        assert!(opt.has_alias("n"));
        assert!(opt.has_alias("nm"));

        assert!(opt.get_aliases().any(|s| s == "n"));
        assert!(opt.get_aliases().any(|s| s == "nm"));
        assert!(!opt.get_aliases().any(|s| s == "name"));
    }

    #[test]
    fn description_test() {
        let opt = CommandOption::new("date").description("Sets the date");

        assert_eq!(opt.get_description(), Some("Sets the date"));
    }

    #[test]
    fn is_required_test() {
        let opt1 = CommandOption::new("date");
        assert!(!opt1.is_required());

        let opt2 = opt1.clone().required(true);
        assert!(opt2.is_required());
    }

    #[test]
    fn is_hidden_test() {
        let opt1 = CommandOption::new("help");
        assert!(!opt1.is_hidden());

        let opt2 = CommandOption::new("help").hidden(true);
        assert!(opt2.is_hidden());
    }

    #[test]
    fn allow_multiple_test() {
        let opt1 = CommandOption::new("values");
        assert!(!opt1.allow_multiple());

        let opt2 = CommandOption::new("values").multiple(true);
        assert!(opt2.allow_multiple());
    }

    #[test]
    fn require_assign_test() {
        let opt1 = CommandOption::new("values");
        assert!(!opt1.is_assign_required());

        let opt2 = CommandOption::new("values").requires_assign(true);
        assert!(opt2.is_assign_required());
    }

    #[test]
    fn global_option_test() {
        let opt1 = CommandOption::new("values");
        assert!(!opt1.is_global());

        let opt2 = CommandOption::new("values").global(true);
        assert!(opt2.is_global());
    }

    #[test]
    fn args_test() {
        let opt1 = CommandOption::new("date");

        let opt2 = opt1
            .clone()
            .arg(Argument::with_name("value").valid_values(&["day", "hour", "minute"]));

        assert!(opt2.get_arg().unwrap().is_valid("day"));
        assert!(opt2.get_arg().unwrap().is_valid("hour"));
        assert!(opt2.get_arg().unwrap().is_valid("minute"));
        assert!(!opt2.get_arg().unwrap().is_valid("second"));
    }

    #[test]
    fn options_add_test() {
        let mut options = OptionList::new();
        assert!(options.is_empty());

        assert!(options
            .add(CommandOption::new("version").alias("v"))
            .is_ok());
        assert!(options.add(CommandOption::new("author").alias("a")).is_ok());
        assert!(options.add(CommandOption::new("verbose")).is_ok());
        assert_eq!(options.len(), 3);
    }

    #[test]
    fn options_get_test() {
        let mut options = OptionList::new();
        options
            .add(CommandOption::new("version").alias("v"))
            .unwrap();
        options
            .add(CommandOption::new("author").alias("a"))
            .unwrap();
        options.add(CommandOption::new("verbose")).unwrap();

        assert_eq!(options.get("version"), Some(&CommandOption::new("version")));
        assert_eq!(options.get("v"), Some(&CommandOption::new("version")));
        assert_eq!(options.get("author"), Some(&CommandOption::new("author")));
        assert_eq!(options.get("ve"), None);
    }

    #[test]
    fn options_contains_test() {
        let mut options = OptionList::new();
        options
            .add(CommandOption::new("version").alias("v"))
            .unwrap();
        options
            .add(CommandOption::new("author").alias("a"))
            .unwrap();
        options.add(CommandOption::new("verbose")).unwrap();

        assert!(options.contains("version"));
        assert!(options.contains("v"));
        assert!(options.contains("author"));
        assert!(options.contains("a"));
        assert!(options.contains("verbose"));
    }

    #[test]
    fn options_get_args_test() {
        let mut options = OptionList::new();

        let opt1 = CommandOption::new("version")
            .alias("v")
            .arg(Argument::with_name("version").values_count(1));

        let opt2 = CommandOption::new("author")
            .alias("a")
            .arg(Argument::with_name("x").values_count(0..));

        let opt3 = CommandOption::new("verbose").arg(Argument::with_name("x").values_count(1..3));

        options.add(opt1).unwrap();
        options.add(opt2).unwrap();
        options.add(opt3).unwrap();

        assert_eq!(options.len(), 3);
        assert!(options.get_arg("version").is_some());
        assert!(options.get_arg("author").is_some());
        assert!(options.get_arg("verbose").is_some());
    }

    #[test]
    fn options_add_duplicated_test() {
        let mut options = OptionList::new();
        options
            .add(CommandOption::new("version").alias("v"))
            .unwrap();

        assert!(options.add(CommandOption::new("version")).is_err());
        assert!(options.add(CommandOption::new("v")).is_err());
        assert!(options
            .add(CommandOption::new("V").alias("version"))
            .is_err());
        assert!(options.add(CommandOption::new("value").alias("v")).is_err());
    }

    #[test]
    fn option_list_indexer_test() {
        let mut options = OptionList::new();
        options.add(CommandOption::new("number")).unwrap();
        options.add(CommandOption::new("enable")).unwrap();

        assert_eq!(options["number"].get_name(), "number");
        assert_eq!(options["enable"].get_name(), "enable");
    }
}