2021-05-14 22:07:16 +00:00
|
|
|
/**
|
|
|
|
* In addition to the terms of the AGPL, portions of this file are
|
|
|
|
* governed by the terms of the MIT license, from the original
|
|
|
|
* axfive-matrix-dicebot project.
|
|
|
|
*/
|
2020-11-04 20:34:57 +00:00
|
|
|
use crate::basic::parser::parse_element_expression;
|
2020-08-21 21:49:22 +00:00
|
|
|
use crate::cofd::parser::{create_chance_die, parse_dice_pool};
|
2020-10-15 16:52:08 +00:00
|
|
|
use crate::commands::{
|
2020-10-31 12:40:44 +00:00
|
|
|
basic_rolling::RollCommand,
|
|
|
|
cofd::PoolRollCommand,
|
2020-10-31 14:03:18 +00:00
|
|
|
cthulhu::{CthAdvanceRoll, CthRoll},
|
2020-11-22 21:30:24 +00:00
|
|
|
management::ResyncCommand,
|
2020-10-31 12:40:44 +00:00
|
|
|
misc::HelpCommand,
|
|
|
|
variables::{
|
|
|
|
DeleteVariableCommand, GetAllVariablesCommand, GetVariableCommand, SetVariableCommand,
|
|
|
|
},
|
|
|
|
Command,
|
2020-10-15 16:52:08 +00:00
|
|
|
};
|
2020-10-31 14:03:18 +00:00
|
|
|
use crate::cthulhu::parser::{parse_advancement_roll, parse_regular_roll};
|
2020-10-04 21:32:50 +00:00
|
|
|
use crate::error::BotError;
|
2020-08-28 22:02:41 +00:00
|
|
|
use crate::help::parse_help_topic;
|
2021-05-21 14:44:03 +00:00
|
|
|
use crate::parser::variables::parse_set_variable;
|
2020-10-04 21:32:50 +00:00
|
|
|
use combine::parser::char::{char, letter, space};
|
|
|
|
use combine::{any, many1, optional, Parser};
|
2020-08-31 23:33:46 +00:00
|
|
|
use nom::Err as NomErr;
|
2020-10-31 13:52:46 +00:00
|
|
|
use thiserror::Error;
|
|
|
|
|
2020-10-31 21:03:17 +00:00
|
|
|
#[derive(Debug, Clone, PartialEq, Error)]
|
2020-10-31 13:52:46 +00:00
|
|
|
pub enum CommandParsingError {
|
2020-10-31 21:03:17 +00:00
|
|
|
#[error("unrecognized command: {0}")]
|
|
|
|
UnrecognizedCommand(String),
|
|
|
|
|
2020-10-31 13:52:46 +00:00
|
|
|
#[error("parser error: {0}")]
|
|
|
|
InternalParseError(#[from] combine::error::StringStreamError),
|
|
|
|
}
|
2020-04-21 03:15:13 +00:00
|
|
|
|
|
|
|
// Parse a roll expression.
|
2020-10-04 21:32:50 +00:00
|
|
|
fn parse_roll(input: &str) -> Result<Box<dyn Command>, BotError> {
|
|
|
|
let result = parse_element_expression(input);
|
|
|
|
match result {
|
|
|
|
Ok((rest, expression)) if rest.len() == 0 => Ok(Box::new(RollCommand(expression))),
|
|
|
|
//Legacy code boundary translates nom errors into BotErrors.
|
|
|
|
Ok(_) => Err(BotError::NomParserIncomplete),
|
|
|
|
Err(NomErr::Error(e)) => Err(BotError::NomParserError(e.1)),
|
|
|
|
Err(NomErr::Failure(e)) => Err(BotError::NomParserError(e.1)),
|
|
|
|
Err(NomErr::Incomplete(_)) => Err(BotError::NomParserIncomplete),
|
|
|
|
}
|
2020-04-21 03:15:13 +00:00
|
|
|
}
|
|
|
|
|
2020-10-15 16:52:08 +00:00
|
|
|
fn parse_get_variable_command(input: &str) -> Result<Box<dyn Command>, BotError> {
|
|
|
|
Ok(Box::new(GetVariableCommand(input.to_owned())))
|
|
|
|
}
|
|
|
|
|
|
|
|
fn parse_set_variable_command(input: &str) -> Result<Box<dyn Command>, BotError> {
|
|
|
|
let (variable_name, value) = parse_set_variable(input)?;
|
|
|
|
Ok(Box::new(SetVariableCommand(variable_name, value)))
|
|
|
|
}
|
|
|
|
|
|
|
|
fn parse_delete_variable_command(input: &str) -> Result<Box<dyn Command>, BotError> {
|
|
|
|
Ok(Box::new(DeleteVariableCommand(input.to_owned())))
|
|
|
|
}
|
|
|
|
|
2020-10-04 21:32:50 +00:00
|
|
|
fn parse_pool_roll(input: &str) -> Result<Box<dyn Command>, BotError> {
|
|
|
|
let pool = parse_dice_pool(input)?;
|
|
|
|
Ok(Box::new(PoolRollCommand(pool)))
|
2020-08-21 21:49:22 +00:00
|
|
|
}
|
|
|
|
|
2020-10-31 13:19:38 +00:00
|
|
|
fn parse_cth_roll(input: &str) -> Result<Box<dyn Command>, BotError> {
|
2020-10-31 14:03:18 +00:00
|
|
|
let roll = parse_regular_roll(input)?;
|
2020-10-31 13:19:38 +00:00
|
|
|
Ok(Box::new(CthRoll(roll)))
|
|
|
|
}
|
|
|
|
|
2020-10-31 14:03:18 +00:00
|
|
|
fn parse_cth_advancement_roll(input: &str) -> Result<Box<dyn Command>, BotError> {
|
|
|
|
let roll = parse_advancement_roll(input)?;
|
|
|
|
Ok(Box::new(CthAdvanceRoll(roll)))
|
|
|
|
}
|
|
|
|
|
2020-10-04 21:32:50 +00:00
|
|
|
fn chance_die() -> Result<Box<dyn Command>, BotError> {
|
|
|
|
let pool = create_chance_die()?;
|
|
|
|
Ok(Box::new(PoolRollCommand(pool)))
|
2020-08-21 21:49:22 +00:00
|
|
|
}
|
|
|
|
|
2020-10-22 20:29:37 +00:00
|
|
|
fn get_all_variables() -> Result<Box<dyn Command>, BotError> {
|
|
|
|
Ok(Box::new(GetAllVariablesCommand))
|
|
|
|
}
|
|
|
|
|
2020-11-22 21:30:24 +00:00
|
|
|
fn parse_resync() -> Result<Box<dyn Command>, BotError> {
|
|
|
|
Ok(Box::new(ResyncCommand))
|
|
|
|
}
|
|
|
|
|
2020-10-04 21:32:50 +00:00
|
|
|
fn help(topic: &str) -> Result<Box<dyn Command>, BotError> {
|
2020-08-31 20:16:43 +00:00
|
|
|
let topic = parse_help_topic(topic);
|
2020-10-04 21:32:50 +00:00
|
|
|
Ok(Box::new(HelpCommand(topic)))
|
2020-08-28 22:02:41 +00:00
|
|
|
}
|
|
|
|
|
2020-08-31 00:07:56 +00:00
|
|
|
/// Split an input string into its constituent command and "everything
|
|
|
|
/// else" parts. Extracts the command separately from its input (i.e.
|
|
|
|
/// rest of the line) and returns a tuple of (command_input, command).
|
2020-08-31 20:16:43 +00:00
|
|
|
/// Whitespace at the start and end of the command input is removed.
|
2020-10-31 13:52:46 +00:00
|
|
|
fn split_command(input: &str) -> Result<(String, String), CommandParsingError> {
|
2020-10-04 21:32:50 +00:00
|
|
|
let input = input.trim();
|
|
|
|
|
|
|
|
let exclamation = char('!');
|
|
|
|
let word = many1(letter()).map(|value: String| value);
|
|
|
|
let at_least_one_space = many1(space().silent()).map(|value: String| value);
|
|
|
|
let cmd_input = optional(at_least_one_space.and(many1(any()).map(|value: String| value)));
|
|
|
|
|
|
|
|
let mut parser = exclamation.and(word).and(cmd_input);
|
|
|
|
|
|
|
|
//TODO make less wacky, possibly by mapping it into a struct and
|
|
|
|
// making use of skip. This super-wacky tuple is:
|
|
|
|
// (parsed_input, rest)
|
|
|
|
//Where parsed_input is:
|
|
|
|
// (!command, option<arguments>)
|
|
|
|
//Where !command is:
|
|
|
|
// ('!', command)
|
|
|
|
//Were option<arguments> is:
|
|
|
|
// Option tuple of (whitespace, arguments)
|
|
|
|
let (command, command_input) = match parser.parse(input)? {
|
|
|
|
(((_, command), Some((_, command_input))), _) => (command, command_input),
|
|
|
|
(((_, command), None), _) => (command, "".to_string()),
|
|
|
|
};
|
|
|
|
|
|
|
|
Ok((command, command_input))
|
2020-08-31 00:07:56 +00:00
|
|
|
}
|
2020-08-21 21:49:22 +00:00
|
|
|
|
2020-08-31 00:07:56 +00:00
|
|
|
/// Potentially parse a command expression. If we recognize the
|
|
|
|
/// command, an error should be raised if the command is misparsed. If
|
2020-11-12 21:05:14 +00:00
|
|
|
/// we don't recognize the command, return an error.
|
2020-10-31 21:03:17 +00:00
|
|
|
pub fn parse_command(input: &str) -> Result<Box<dyn Command>, BotError> {
|
2020-08-31 23:33:46 +00:00
|
|
|
match split_command(input) {
|
2021-05-21 22:40:03 +00:00
|
|
|
Ok((cmd, cmd_input)) => match cmd.to_lowercase().as_ref() {
|
2020-10-31 21:03:17 +00:00
|
|
|
"variables" => get_all_variables(),
|
|
|
|
"get" => parse_get_variable_command(&cmd_input),
|
|
|
|
"set" => parse_set_variable_command(&cmd_input),
|
|
|
|
"del" => parse_delete_variable_command(&cmd_input),
|
2020-11-22 21:30:24 +00:00
|
|
|
"resync" => parse_resync(),
|
2020-10-31 21:03:17 +00:00
|
|
|
"r" | "roll" => parse_roll(&cmd_input),
|
|
|
|
"rp" | "pool" => parse_pool_roll(&cmd_input),
|
2021-05-21 22:40:03 +00:00
|
|
|
"cthroll" => parse_cth_roll(&cmd_input),
|
|
|
|
"cthadv" | "ctharoll" => parse_cth_advancement_roll(&cmd_input),
|
2020-10-31 21:03:17 +00:00
|
|
|
"chance" => chance_die(),
|
|
|
|
"help" => help(&cmd_input),
|
|
|
|
_ => Err(CommandParsingError::UnrecognizedCommand(cmd).into()),
|
2020-08-31 23:33:46 +00:00
|
|
|
},
|
|
|
|
//All other errors passed up.
|
2020-10-31 13:52:46 +00:00
|
|
|
Err(e) => Err(e.into()),
|
2020-08-31 00:07:56 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
|
|
|
use super::*;
|
|
|
|
|
2020-10-04 21:32:50 +00:00
|
|
|
//TODO these errors don't seem to implement the right traits to do
|
|
|
|
//eq checks or even unwrap_err!
|
|
|
|
|
2020-08-31 23:33:46 +00:00
|
|
|
#[test]
|
|
|
|
fn non_command_test() {
|
|
|
|
let result = parse_command("not a command");
|
2020-10-04 21:32:50 +00:00
|
|
|
assert!(result.is_err());
|
2020-08-31 23:33:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn empty_message_test() {
|
|
|
|
let result = parse_command("");
|
2020-10-04 21:32:50 +00:00
|
|
|
assert!(result.is_err());
|
2020-08-31 23:33:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
2020-09-01 08:17:59 +00:00
|
|
|
fn just_exclamation_mark_test() {
|
2020-08-31 23:33:46 +00:00
|
|
|
let result = parse_command("!");
|
|
|
|
assert!(result.is_err());
|
|
|
|
}
|
|
|
|
|
2020-11-05 23:03:22 +00:00
|
|
|
#[test]
|
|
|
|
fn newline_test() {
|
|
|
|
assert!(parse_command("\n!roll 1d4").is_ok());
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn whitespace_and_newline_test() {
|
|
|
|
assert!(parse_command(" \n!roll 1d4").is_ok());
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn newline_and_whitespace_test() {
|
|
|
|
assert!(parse_command("\n !cthroll 50").is_ok());
|
|
|
|
}
|
|
|
|
|
2020-09-01 08:17:59 +00:00
|
|
|
#[test]
|
|
|
|
fn word_with_exclamation_mark_test() {
|
|
|
|
let result1 = parse_command("hello !notacommand");
|
2020-10-04 21:32:50 +00:00
|
|
|
assert!(result1.is_err());
|
2020-09-01 08:17:59 +00:00
|
|
|
|
|
|
|
let result2 = parse_command("hello!");
|
2020-10-04 21:32:50 +00:00
|
|
|
assert!(result2.is_err());
|
2020-09-01 08:17:59 +00:00
|
|
|
|
|
|
|
let result3 = parse_command("hello!notacommand");
|
2020-10-04 21:32:50 +00:00
|
|
|
assert!(result3.is_err());
|
2020-09-01 08:17:59 +00:00
|
|
|
}
|
|
|
|
|
2020-08-31 00:07:56 +00:00
|
|
|
#[test]
|
|
|
|
fn basic_command_test() {
|
|
|
|
assert_eq!(
|
2020-10-04 21:32:50 +00:00
|
|
|
("roll".to_string(), "1d4".to_string()),
|
2020-08-31 00:07:56 +00:00
|
|
|
split_command("!roll 1d4").expect("got parsing error")
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn whitespace_at_start_test() {
|
|
|
|
assert_eq!(
|
2020-10-04 21:32:50 +00:00
|
|
|
("roll".to_string(), "1d4".to_string()),
|
2020-08-31 00:07:56 +00:00
|
|
|
split_command(" !roll 1d4").expect("got parsing error")
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn whitespace_at_end_test() {
|
|
|
|
assert_eq!(
|
2020-10-04 21:32:50 +00:00
|
|
|
("roll".to_string(), "1d4".to_string()),
|
2020-08-31 00:07:56 +00:00
|
|
|
split_command("!roll 1d4 ").expect("got parsing error")
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn whitespace_on_both_ends_test() {
|
|
|
|
assert_eq!(
|
2020-10-04 21:32:50 +00:00
|
|
|
("roll".to_string(), "1d4".to_string()),
|
2020-08-31 00:07:56 +00:00
|
|
|
split_command(" !roll 1d4 ").expect("got parsing error")
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn single_command_test() {
|
|
|
|
assert_eq!(
|
2020-10-04 21:32:50 +00:00
|
|
|
("roll".to_string(), "".to_string()),
|
2020-08-31 00:07:56 +00:00
|
|
|
split_command("!roll").expect("got parsing error")
|
|
|
|
);
|
|
|
|
|
|
|
|
assert_eq!(
|
2020-10-04 21:32:50 +00:00
|
|
|
("thisdoesnotexist".to_string(), "".to_string()),
|
2020-08-31 00:07:56 +00:00
|
|
|
split_command("!thisdoesnotexist").expect("got parsing error")
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn bad_command_test() {
|
|
|
|
assert!(split_command("roll 1d4").is_err());
|
|
|
|
assert!(split_command("roll").is_err());
|
2020-04-21 06:15:18 +00:00
|
|
|
}
|
2020-11-12 21:05:14 +00:00
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn chance_die_is_not_malformed() {
|
|
|
|
assert!(parse_command("!chance").is_ok());
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn roll_malformed_expression_test() {
|
|
|
|
assert!(parse_command("!roll 1d20asdlfkj").is_err());
|
|
|
|
assert!(parse_command("!roll 1d20asdlfkj ").is_err());
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn roll_dice_pool_malformed_expression_test() {
|
|
|
|
assert!(parse_command("!pool 8abc").is_err());
|
|
|
|
assert!(parse_command("!pool 8abc ").is_err());
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn pool_whitespace_test() {
|
|
|
|
parse_command("!pool ns3:8 ").expect("was error");
|
|
|
|
parse_command(" !pool ns3:8").expect("was error");
|
|
|
|
parse_command(" !pool ns3:8 ").expect("was error");
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn help_whitespace_test() {
|
|
|
|
parse_command("!help stuff ").expect("was error");
|
|
|
|
parse_command(" !help stuff").expect("was error");
|
|
|
|
parse_command(" !help stuff ").expect("was error");
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn roll_whitespace_test() {
|
|
|
|
parse_command("!roll 1d4 + 5d6 -3 ").expect("was error");
|
|
|
|
parse_command("!roll 1d4 + 5d6 -3 ").expect("was error");
|
|
|
|
parse_command(" !roll 1d4 + 5d6 -3 ").expect("was error");
|
|
|
|
}
|
2021-05-21 22:40:03 +00:00
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn case_insensitive_test() {
|
|
|
|
parse_command("!CTHROLL 40").expect("command parsing is not case sensitive.");
|
|
|
|
}
|
2020-04-21 03:15:13 +00:00
|
|
|
}
|