cmdtree 0.10.0

(Rust) commands tree
Documentation
use super::*;
use colored::*;
use std::io::{self, Write};

const PATH_SEP: char = '.';

#[derive(Debug, PartialEq)]
enum WordResult<'a, R> {
    Help(&'a SubClass<R>),
    Cancel,
    Exit,
    Class(&'a Arc<SubClass<R>>),
    Action(&'a Action<R>),
    Unrecognized,
}

/// The result variants of `parse_line`.
#[derive(Debug, PartialEq)]
pub enum LineResult<R> {
    /// `help` command reached.
    Help,
    /// `cancel` commannd reached.
    Cancel,
    /// `exit` command reached.
    Exit,
    /// Navigated to a class.
    Class,
    /// Action invoked.
    /// The inner data is the returned data from invocation.
    Action(R),
    /// No commands recognised.
    Unrecognized,
}

impl<R> LineResult<R> {
    /// Converts the `LineResult` into the data returned from invoking an action.
    /// If the result was not an action, `None` is returned.
    pub fn action_result(self) -> Option<R> {
        match self {
            LineResult::Action(r) => Some(r),
            _ => None,
        }
    }
}

impl<R> Commander<R> {
    /// Parse a line of commands and updates the `Commander` state.
    ///
    /// Parsing a line is akin to sending an input line to the commander in the run loop.
    /// Commands are space separated, and executed within this function, any actions that are specified will be invoked.
    ///
    /// Most branches result in a `LineResult::Continue` apart from an exit command which will result in a `LineResult::Exit`.
    /// It is up to the developer to decide on the behaviour.
    ///
    /// # Example
    /// ```rust
    /// use cmdtree::*;
    /// let mut cmder = Builder::default_config("base")
    ///		.begin_class("one", "")
    ///		.begin_class("two", "")
    /// 	.add_action("echo", "", |_wtr, args| println!("{}", args.join(" ")))
    ///		.into_commander().unwrap();
    ///
    ///	assert_eq!(cmder.path(), "base");
    ///	cmder.parse_line("one two", true,  &mut std::io::sink());
    ///	assert_eq!(cmder.path(), "base.one.two");
    /// cmder.parse_line("echo Hello, world!", true, &mut std::io::sink());	// should print "Hello, world!"
    /// ```
    pub fn parse_line<W: Write>(
        &mut self,
        line: &str,
        colourise: bool,
        writer: &mut W,
    ) -> LineResult<R> {
        let line = line.replace("\n", "").replace("\r", "");
        let words: Vec<_> = line.trim().split(' ').collect();
        let mut idx = 0;
        let mut words_iter = words.iter();
        let mut next_word = words_iter.next();

        // if there is no current class, use the root
        let start_class = Arc::clone(&self.current);
        let start_path = self.path.clone();

        while let Some(word) = next_word {
            idx += 1;
            next_word = match parse_word(&self.current, word) {
                WordResult::Help(sc) => {
                    if colourise {
                        write_help_coloured(&sc, writer).expect("failed writing output to writer");
                    } else {
                        write_help(&sc, writer).expect("failed writing output to writer");
                    }
                    self.current = Arc::clone(&start_class);
                    self.path = start_path.clone();
                    return LineResult::Help;
                }
                WordResult::Cancel => {
                    self.current = Arc::clone(&self.root);
                    self.path = self.root.name.clone();
                    return LineResult::Cancel;
                }
                WordResult::Exit => {
                    return LineResult::Exit;
                }
                WordResult::Class(sc) => {
                    self.path.push_str(&format!("{}{}", PATH_SEP, sc.name));
                    self.current = Arc::clone(&sc);
                    words_iter.next()
                }
                WordResult::Action(a) => {
                    let slice = &words[idx..];
                    let r = a.call(writer, slice);
                    self.current = Arc::clone(&start_class);
                    self.path = start_path.clone();
                    return LineResult::Action(r);
                }
                WordResult::Unrecognized => {
                    let mut s = format!(
                        "'{}' does not match any keywords, classes, or actions",
                        word
                    )
                    .bright_red();

                    if !colourise {
                        s = s.white();
                    }

                    writeln!(writer, "{}", s).expect("failed writing output to writer");
                    self.current = Arc::clone(&start_class);
                    self.path = start_path.clone();
                    return LineResult::Unrecognized;
                }
            };
        }

        LineResult::Class // default
    }
}

fn parse_word<'a, R>(subclass: &'a SubClass<R>, word: &str) -> WordResult<'a, R> {
    let lwr = word.to_lowercase();
    match lwr.as_str() {
        "help" => WordResult::Help(subclass),
        "cancel" | "c" => WordResult::Cancel,
        "exit" => WordResult::Exit,
        word => {
            if let Some(c) = subclass.classes.iter().find(|c| c.name.as_str() == word) {
                WordResult::Class(c)
            } else if let Some(a) = subclass.actions.iter().find(|a| a.name.as_str() == word) {
                WordResult::Action(a)
            } else {
                WordResult::Unrecognized
            }
        }
    }
}

fn write_help_coloured<W: Write, R>(class: &SubClass<R>, writer: &mut W) -> io::Result<()> {
    writeln!(
        writer,
        "{} -- prints the help messages",
        "help".bright_yellow()
    )?;
    writeln!(
        writer,
        "{} | {} -- returns to the root class",
        "cancel".bright_yellow(),
        "c".bright_yellow()
    )?;
    writeln!(
        writer,
        "{} -- sends the exit signal to end the interactive loop",
        "exit".bright_yellow()
    )?;
    if class.classes.len() > 0 {
        writeln!(writer, "{}", "Classes:".bright_purple())?;
        for class in class.classes.iter() {
            writeln!(writer, "\t{} -- {}", class.name.bright_yellow(), class.help)?;
        }
    }

    if class.actions.len() > 0 {
        writeln!(writer, "{}", "Actions:".bright_purple())?;
        for action in class.actions.iter() {
            writeln!(
                writer,
                "\t{} -- {}",
                action.name.bright_yellow(),
                action.help
            )?;
        }
    }

    Ok(())
}

fn write_help<W: Write, R>(class: &SubClass<R>, writer: &mut W) -> io::Result<()> {
    writeln!(writer, "help -- prints the help messages",)?;
    writeln!(writer, "cancel | c -- returns to the root class",)?;
    writeln!(
        writer,
        "exit -- sends the exit signal to end the interactive loop",
    )?;
    if class.classes.len() > 0 {
        writeln!(writer, "{}", "Classes:")?;
        for class in class.classes.iter() {
            writeln!(writer, "\t{} -- {}", class.name, class.help)?;
        }
    }

    if class.actions.len() > 0 {
        writeln!(writer, "{}", "Actions:")?;
        for action in class.actions.iter() {
            writeln!(writer, "\t{} -- {}", action.name, action.help)?;
        }
    }

    Ok(())
}

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

    #[test]
    fn parse_line_test() {
        let mut cmder = Builder::default_config("test")
            .begin_class("class1", "class1 help")
            .begin_class("class1-class1", "adsf")
            .add_action("action1", "adf", |_, _| ())
            .end_class()
            .begin_class("class1-class2", "adsf")
            .add_action("action2", "adsf", |_, _| ())
            .end_class()
            .end_class()
            .begin_class("class2", "asdf")
            .end_class()
            .add_action("test-args", "", |_wtr, args| {
                assert_eq!(&args, &["one", "two", "three"])
            })
            .into_commander()
            .unwrap();

        let w = &mut std::io::sink();

        assert_eq!(cmder.parse_line("adsf", true, w), LineResult::Unrecognized); // unrecognised branch
        assert_eq!(cmder.current, cmder.root);
        assert_eq!(cmder.parse_line("adsf", false, w), LineResult::Unrecognized); // unrecognised branch
        assert_eq!(cmder.current, cmder.root);

        assert_eq!(cmder.parse_line("class1", true, w), LineResult::Class);
        assert_ne!(cmder.current, cmder.root);
        assert_eq!(cmder.current.name, "class1");

        // should be able to action here
        assert_eq!(
            cmder.parse_line("class1-class1 action1", true, w),
            LineResult::Action(())
        );
        assert_eq!(cmder.current.name, "class1");
        assert_eq!(
            cmder.parse_line("class1-class2 action2", true, w),
            LineResult::Action(())
        );
        assert_eq!(cmder.current.name, "class1");

        // get back to root
        assert_eq!(cmder.parse_line("cancel", true, w), LineResult::Cancel);
        assert_eq!(cmder.current.name, "test");

        // test args
        assert_eq!(
            cmder.parse_line("test-args one two three", true, w),
            LineResult::Action(())
        );
        assert_eq!(cmder.current.name, "test");

        // test help
        assert_eq!(cmder.parse_line("help", true, w), LineResult::Help);
        assert_eq!(cmder.current.name, "test");
        assert_eq!(cmder.parse_line("help", false, w), LineResult::Help);
        assert_eq!(cmder.current.name, "test");

        // test exit
        assert_eq!(cmder.parse_line("exit", true, w), LineResult::Exit);
    }

    #[test]
    fn parse_word_test() {
        let mut sc = SubClass::with_name("Class-Name", "help msg");
        assert_eq!(parse_word(&sc, "HELP"), WordResult::Help(&sc));
        assert_eq!(parse_word(&sc, "EXIT"), WordResult::Exit);
        assert_eq!(parse_word(&sc, "CANCEL"), WordResult::Cancel);
        assert_eq!(parse_word(&sc, "C"), WordResult::Cancel);
        assert_eq!(parse_word(&sc, "asdf"), WordResult::Unrecognized);

        sc.classes
            .push(Arc::new(SubClass::with_name("name", "asdf")));
        sc.actions.push(Action::blank_fn("action", "adsf"));
        assert_eq!(parse_word(&sc, "NAME"), WordResult::Class(&sc.classes[0]));
        assert_eq!(
            parse_word(&sc, "aCtIoN"),
            WordResult::Action(&sc.actions[0])
        );
    }

    #[test]
    fn write_help_coloured_test() {
        let mut sc = SubClass::with_name("Class-Name", "root class");
        sc.classes
            .push(Arc::new(SubClass::with_name("class1", "class 1 help")));
        sc.classes
            .push(Arc::new(SubClass::with_name("class2", "class 2 help")));
        sc.actions
            .push(Action::blank_fn("action1", "action 1 help"));
        sc.actions
            .push(Action::blank_fn("action2", "action 2 help"));

        let mut help = Vec::new();
        write_help_coloured(&sc, &mut help).unwrap();
        let help = String::from_utf8_lossy(&help);

        assert_eq!(
            &help,
            &format!(
                r#"{} -- prints the help messages
{} | {} -- returns to the root class
{} -- sends the exit signal to end the interactive loop
{}
	{} -- class 1 help
	{} -- class 2 help
{}
	{} -- action 1 help
	{} -- action 2 help
"#,
                "help".bright_yellow(),
                "cancel".bright_yellow(),
                "c".bright_yellow(),
                "exit".bright_yellow(),
                "Classes:".bright_purple(),
                "class1".bright_yellow(),
                "class2".bright_yellow(),
                "Actions:".bright_purple(),
                "action1".bright_yellow(),
                "action2".bright_yellow()
            )
        );
    }

    #[test]
    fn write_help_test() {
        let mut sc = SubClass::with_name("Class-Name", "root class");
        sc.classes
            .push(Arc::new(SubClass::with_name("class1", "class 1 help")));
        sc.classes
            .push(Arc::new(SubClass::with_name("class2", "class 2 help")));
        sc.actions
            .push(Action::blank_fn("action1", "action 1 help"));
        sc.actions
            .push(Action::blank_fn("action2", "action 2 help"));

        let mut help = Vec::new();
        write_help(&sc, &mut help).unwrap();
        let help = String::from_utf8_lossy(&help);

        assert_eq!(
            &help,
            r#"help -- prints the help messages
cancel | c -- returns to the root class
exit -- sends the exit signal to end the interactive loop
Classes:
	class1 -- class 1 help
	class2 -- class 2 help
Actions:
	action1 -- action 1 help
	action2 -- action 2 help
"#
        );
    }

}