implement command parser fully

This commit is contained in:
Taylor C. Richberger 2020-04-21 00:07:03 -06:00
parent 90ae9c142c
commit 649aec531e
6 changed files with 111 additions and 119 deletions

View File

@ -1,14 +1,16 @@
use axfive_matrix_dicebot::dice::parser::parse_element_expression;
use axfive_matrix_dicebot::roll::{Roll, Rolled};
use axfive_matrix_dicebot::commands::Command;
use axfive_matrix_dicebot::commands::parse_command;
use std::error::Error;
use std::string::ToString;
fn main() -> Result<(), String> {
let command = std::env::args().skip(1).collect::<Vec<String>>().join(" ");
let command: Command = match Command::parse(&command) {
Ok(command) => command.1,
Err(e) => return Err(format!("{}", e)),
let command = match parse_command(&command) {
Some(Ok(command)) => command,
Some(Err(e)) => return Err(format!("Error parsing command: {}", e)),
None => return Err("Command not recognized".into()),
};
println!("{}", command.execute());
println!("{}", command.execute().plain());
Ok(())
}

View File

@ -4,27 +4,44 @@ use nom::error::ErrorKind;
use nom::IResult;
pub mod parser;
pub struct RollCommand(ElementExpression);
pub enum Command {
Roll(RollCommand),
pub struct Execution {
plain: String,
html: String,
}
impl Command {
pub fn parse<'a>(input: &'a str) -> IResult<&'a str, Command> {
parser::parse_command(input)
impl Execution {
pub fn plain(&self) -> &str {
&self.plain
}
// Type subject to change
pub fn execute(self) -> String {
match self {
Command::Roll(command) => command.execute(),
pub fn html(&self) -> &str {
&self.html
}
}
pub struct RollCommand(ElementExpression);
pub trait Command {
fn execute(&self) -> Execution;
}
impl Command for RollCommand {
fn execute(&self) -> Execution {
let roll = self.0.roll();
let plain = format!("Dice: {}\nResult: {}", self.0, roll);
let html = format!("<strong>Dice:</strong> {}<br><strong>Result</strong>: {}", self.0, roll);
Execution {
plain,
html,
}
}
}
impl RollCommand {
pub fn execute(self) -> String {
self.0.roll().to_string()
pub fn parse_command(s: &str) -> Option<Result<Box<dyn Command>, String>> {
// Ignore trailing input, if any.
match parser::parse_command(s) {
Ok((_, Some(command))) => Some(Ok(command)),
Ok((_, None)) => None,
Err(err) => Some(Err(err.to_string())),
}
}

View File

@ -1,7 +1,10 @@
use nom::{
alt,
tuple,
switch,
take_while,
bytes::complete::{tag, take_while},
character::complete::digit1,
character::is_alphabetic,
complete, many0, named,
sequence::tuple,
tag, IResult,
@ -12,104 +15,30 @@ use crate::commands::{Command, RollCommand};
use crate::dice::parser::parse_element_expression;
// Parse a roll expression.
fn parse_roll(input: &str) -> IResult<&str, Command> {
named!(invocation(&str) -> &str, alt!(complete!(tag!("!r")) | complete!(tag!("!roll"))));
let (input, _) = eat_whitespace(input)?;
let (input, _) = invocation(input)?;
fn parse_roll(input: &str) -> IResult<&str, RollCommand> {
let (input, _) = eat_whitespace(input)?;
let (input, expression) = parse_element_expression(input)?;
Ok((input, Command::Roll(RollCommand(expression))))
Ok((input, RollCommand(expression)))
}
// Parse a command expression.
pub fn parse_command(input: &str) -> IResult<&str, Command> {
// Add new commands to alt!
named!(command(&str) -> Command, alt!(parse_roll));
command(input)
// Potentially parse a command expression. If we recognize the command, an error should be raised
// if the command is misparsed. If we don't recognize the command, ignore it.
pub fn parse_command(original_input: &str) -> IResult<&str, Option<Box<dyn Command>>> {
let (input, _) = eat_whitespace(original_input)?;
named!(command(&str) -> (&str, &str), tuple!(complete!(tag!("!")), complete!(take_while!(char::is_alphabetic))));
let (input, command) = match command(input) {
// Strip the exclamation mark
Ok((input, (_, result))) => (input, result),
Err(e) => return Ok((original_input, None)),
};
let (input, command): (&str, Box<dyn Command>) = match command {
"r" | "roll" => {
let (input, command) = parse_roll(input)?;
let command: Box<dyn Command> = Box::new(command);
(input, command)
},
// No recognized command, ignore this.
_ => return Ok((original_input, None)),
};
Ok((input, Some(command)))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn dice_test() {
assert_eq!(parse_dice("2d4"), Ok(("", Dice::new(2, 4))));
assert_eq!(parse_dice("20d40"), Ok(("", Dice::new(20, 40))));
assert_eq!(parse_dice("8d7"), Ok(("", Dice::new(8, 7))));
}
#[test]
fn element_test() {
assert_eq!(
parse_element(" \t\n\r\n 8d7 \n"),
Ok((" \n", Element::Dice(Dice::new(8, 7))))
);
assert_eq!(
parse_element(" \t\n\r\n 8 \n"),
Ok((" \n", Element::Bonus(8)))
);
}
#[test]
fn signed_element_test() {
assert_eq!(
parse_signed_element("+ 7"),
Ok(("", SignedElement::Positive(Element::Bonus(7))))
);
assert_eq!(
parse_signed_element(" \t\n\r\n- 8 \n"),
Ok((" \n", SignedElement::Negative(Element::Bonus(8))))
);
assert_eq!(
parse_signed_element(" \t\n\r\n- 8d4 \n"),
Ok((
" \n",
SignedElement::Negative(Element::Dice(Dice::new(8, 4)))
))
);
assert_eq!(
parse_signed_element(" \t\n\r\n+ 8d4 \n"),
Ok((
" \n",
SignedElement::Positive(Element::Dice(Dice::new(8, 4)))
))
);
}
#[test]
fn element_expression_test() {
assert_eq!(
parse_element_expression("8d4"),
Ok((
"",
ElementExpression(vec![SignedElement::Positive(Element::Dice(Dice::new(
8, 4
)))])
))
);
assert_eq!(
parse_element_expression(" - 8d4 \n "),
Ok((
" \n ",
ElementExpression(vec![SignedElement::Negative(Element::Dice(Dice::new(
8, 4
)))])
))
);
assert_eq!(
parse_element_expression("\t3d4 + 7 - 5 - 6d12 + 1d1 + 53 1d5 "),
Ok((
" 1d5 ",
ElementExpression(vec![
SignedElement::Positive(Element::Dice(Dice::new(3, 4))),
SignedElement::Positive(Element::Bonus(7)),
SignedElement::Negative(Element::Bonus(5)),
SignedElement::Negative(Element::Dice(Dice::new(6, 12))),
SignedElement::Positive(Element::Dice(Dice::new(1, 1))),
SignedElement::Positive(Element::Bonus(53)),
])
))
);
}
}

View File

@ -1,5 +1,6 @@
pub mod parser;
use std::fmt;
use std::ops::{Deref, DerefMut};
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
@ -8,6 +9,12 @@ pub struct Dice {
pub(crate) sides: u32,
}
impl fmt::Display for Dice {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}d{}", self.count, self.sides)
}
}
impl Dice {
fn new(count: u32, sides: u32) -> Dice {
Dice { count, sides }
@ -20,12 +27,30 @@ pub enum Element {
Bonus(u32),
}
impl fmt::Display for Element {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Element::Dice(d) => write!(f, "{}", d),
Element::Bonus(b) => write!(f, "{}", b),
}
}
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum SignedElement {
Positive(Element),
Negative(Element),
}
impl fmt::Display for SignedElement {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SignedElement::Positive(e) => write!(f, "{}", e),
SignedElement::Negative(e) => write!(f, "-{}", e),
}
}
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct ElementExpression(Vec<SignedElement>);
@ -42,3 +67,20 @@ impl DerefMut for ElementExpression {
&mut self.0
}
}
impl fmt::Display for ElementExpression {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut iter = self.0.iter();
if let Some(first) = iter.next() {
write!(f, "{}", first)?;
for roll in iter {
match roll {
SignedElement::Positive(e) => write!(f, " + {}", e)?,
SignedElement::Negative(e) => write!(f, " - {}", e)?,
}
}
}
Ok(())
}
}

View File

@ -4,3 +4,4 @@ pub mod matrix;
pub mod roll;
pub mod commands;
mod parser;

View File

@ -7,7 +7,8 @@ fn is_whitespace(input: char) -> bool {
input == ' ' || input == '\n' || input == '\t' || input == '\r'
}
pub fn eat_whitespace(input: &str) -> IResult<&str, ()> {
let (input, _) = take_while(is_whitespace)(input)?;
Ok((input, ()))
/// Eat whitespace, returning it
pub fn eat_whitespace(input: &str) -> IResult<&str, &str> {
let (input, whitespace) = take_while(is_whitespace)(input)?;
Ok((input, whitespace))
}