genesys_dice_command_parser/
lib.rs

1#![warn(clippy::all, clippy::pedantic)]
2#![allow(clippy::pedantic::module_name_repetitions)]
3#![warn(missing_docs)]
4#![warn(missing_doc_code_examples)]
5
6//! This crate provides functionality for the basic parsing of dice roll commands e.g. `yyypp`, `2g1y2p`, `difficulty difficulty ability proficiency`.
7//! Given some input it will produce a `DiceRoll` struct which can be used to then calculate a result.
8
9
10use std::collections::HashMap;
11
12use nom::{branch, bytes, multi, character, combinator, sequence, Err};
13
14/// Provides access to the `DiceRoll` struct.
15pub mod dice_roll;
16/// Provides access to the `ParserError` struct.
17pub mod error;
18/// Provices access to the `Dice` enum.
19pub mod dice;
20
21
22use crate::dice_roll::{DiceRoll};
23use crate::dice::Dice;
24use crate::error::ParserError;
25
26
27// boost or blue or b
28fn parse_dice_as_value(i: &str) -> nom::IResult<&str, Dice> {
29    branch::alt((
30        combinator::value(Dice::Ability, branch::alt((bytes::complete::tag_no_case("green"), bytes::complete::tag_no_case("g"), bytes::complete::tag_no_case("ability"), bytes::complete::tag_no_case("abil")))),
31        combinator::value(Dice::Challenge, branch::alt((bytes::complete::tag_no_case("challenge"), bytes::complete::tag_no_case("cha"), bytes::complete::tag_no_case("red"), bytes::complete::tag_no_case("r"), ))),
32        combinator::value(Dice::Proficiency, branch::alt((bytes::complete::tag_no_case("proficiency"), bytes::complete::tag_no_case("prof"), bytes::complete::tag_no_case("yellow"), bytes::complete::tag_no_case("y"),))),
33        combinator::value(Dice::Difficulty, branch::alt((bytes::complete::tag_no_case("difficulty"), bytes::complete::tag_no_case("purple"), bytes::complete::tag_no_case("p"), bytes::complete::tag_no_case("diff"), bytes::complete::tag_no_case("dif")))),
34        combinator::value(Dice::Setback, branch::alt((bytes::complete::tag_no_case("black"), bytes::complete::tag_no_case("k"), bytes::complete::tag_no_case("setback"), bytes::complete::tag_no_case("s"), ))),
35        combinator::value(Dice::Force, branch::alt((bytes::complete::tag_no_case("force"), bytes::complete::tag_no_case("white"), bytes::complete::tag_no_case("w"), ))),
36        combinator::value(Dice::Boost, branch::alt((bytes::complete::tag_no_case("blue"), bytes::complete::tag_no_case("boost"), bytes::complete::tag_no_case("b")))),
37    ))(i)
38}
39
40// Matches: 2g or ggbbfd
41fn parse_dice(i: &str) -> nom::IResult<&str, DiceRoll> {
42    let result = sequence::tuple((
43        combinator::opt(character::complete::digit1), parse_dice_as_value ))(i);
44    match result {
45        Ok((remaining, (number_of_dice, dice))) => Ok((
46            remaining,
47            DiceRoll::new(dice, number_of_dice.map_or(Ok(1), str::parse).unwrap()),
48        )),
49        Err(e) => Err(e),
50    }
51}
52
53fn parse_group(i: &str) -> nom::IResult<&str, Vec<DiceRoll>> {
54    let (remaining, rolls) = multi::many1(parse_dice)(i)?;
55    
56    let mut dice_counts: HashMap<Dice, u32> = HashMap::new();
57
58    rolls.into_iter().for_each(|roll| {
59        let group = dice_counts.entry(roll.die).or_insert(0);
60       *group += roll.number_of_dice_to_roll;
61    });
62
63    let rolls = dice_counts.into_iter().map(|(key, value)| DiceRoll::new(key, value)).collect();
64    Ok((remaining, rolls))
65}
66
67fn parse_groups(i: &str) -> nom::IResult<&str, Vec<Vec<DiceRoll>>> {
68    let (remaining, (group_rolls, other_groups)) = sequence::tuple((
69        parse_group,
70        combinator::opt(sequence::tuple((
71            character::complete::char(','),
72            parse_groups,
73        ))),
74    ))(i)?;
75
76    let other_groups_size = match &other_groups {
77        Some((_, rolls)) => rolls.len(),
78        None => 0,
79    };
80
81    let mut rolls: Vec<Vec<DiceRoll>> = Vec::with_capacity(other_groups_size + 1);
82    rolls.push(group_rolls);
83    if other_groups.is_some() {
84        let (_, other_groups_rolls) = other_groups.unwrap();
85        rolls.extend(other_groups_rolls);
86    }
87    Ok((remaining, rolls))
88}
89
90/// Takes a string of dice input and returns a `Result<DiceRoll, ParserError>`
91///
92/// The string will be consumed in the process and must strictly match the format of the parser.
93///
94/// # Examples
95///
96/// Standard usage:
97///
98/// ```
99/// use dice_command_parser::{parse_line, error::ParserError};
100///
101/// let input = "2rkyyg";
102/// let dice_roll = parse_line(&input)?;
103/// # Ok::<(), ParserError>(())
104/// ```
105///
106/// # Errors
107/// This function can fail when one of the following occurs
108/// 1. The line failed to parse.
109/// 2. An error occurred parsing the numbers provided. This will likely be an overflow or underflow error.
110///
111/// For more information see `ParserError`.
112pub fn parse_line(i: &str) -> Result<Vec<Vec<DiceRoll>>, ParserError> {
113    let whitespaceless: String = i.replace(" ", "");
114
115    match parse_groups(&whitespaceless) {
116        Ok((remaining, dice_rolls)) => {
117            if !remaining.trim().is_empty() {
118                return Err(ParserError::ParseError(format!(
119                    "Expected remaining input to be empty, found: {0}",
120                    remaining
121                )));
122            }
123            return Ok(dice_rolls);
124        }
125        Err(Err::Error(e)) | Err(Err::Failure(e)) => {
126            return Err(ParserError::ParseError(format!("{0}", e)));
127        }
128        Err(Err::Incomplete(_)) => {
129            return Err(ParserError::Unknown);
130        }
131    }
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137    use crate::Dice;
138
139    #[test]
140    fn test_parse_dice_as_value() {
141        // Ability dice
142        assert_eq!(
143            parse_dice_as_value("green"),
144            Ok(("", Dice::Ability))
145        );
146        assert_eq!(
147            parse_dice_as_value("g"),
148            Ok(("", Dice::Ability))
149        );
150        assert_eq!(
151            parse_dice_as_value("ability"),
152            Ok(("", Dice::Ability))
153        );
154        assert_eq!(
155            parse_dice_as_value("abil"),
156            Ok(("", Dice::Ability))
157        );
158
159        assert_eq!(
160            parse_dice_as_value("challenge"),
161            Ok(("", Dice::Challenge))
162        );
163        assert_eq!(
164            parse_dice_as_value("cha"),
165            Ok(("", Dice::Challenge))
166        );
167        assert_eq!(
168            parse_dice_as_value("red"),
169            Ok(("", Dice::Challenge))
170        );
171        assert_eq!(
172            parse_dice_as_value("r"),
173            Ok(("", Dice::Challenge))
174        );
175
176        assert_eq!(
177            parse_dice_as_value("Proficiency"),
178            Ok(("", Dice::Proficiency))
179        );
180        assert_eq!(
181            parse_dice_as_value("prof"),
182            Ok(("", Dice::Proficiency))
183        );
184        assert_eq!(
185            parse_dice_as_value("yellow"),
186            Ok(("", Dice::Proficiency))
187        );
188        assert_eq!(
189            parse_dice_as_value("y"),
190            Ok(("", Dice::Proficiency))
191        );
192        
193        assert_eq!(
194            parse_dice_as_value("difficulty"),
195            Ok(("", Dice::Difficulty))
196        );
197        assert_eq!(
198            parse_dice_as_value("diff"),
199            Ok(("", Dice::Difficulty))
200        );
201        assert_eq!(
202            parse_dice_as_value("purple"),
203            Ok(("", Dice::Difficulty))
204        );
205        assert_eq!(
206            parse_dice_as_value("p"),
207            Ok(("", Dice::Difficulty))
208        );
209
210        assert_eq!(
211            parse_dice_as_value("black"),
212            Ok(("", Dice::Setback))
213        );
214        assert_eq!(
215            parse_dice_as_value("setback"),
216            Ok(("", Dice::Setback))
217        );
218        assert_eq!(
219            parse_dice_as_value("k"),
220            Ok(("", Dice::Setback))
221        );
222
223        assert_eq!(
224            parse_dice_as_value("force"),
225            Ok(("", Dice::Force))
226        );
227        assert_eq!(
228            parse_dice_as_value("white"),
229            Ok(("", Dice::Force))
230        );
231        assert_eq!(
232            parse_dice_as_value("w"),
233            Ok(("", Dice::Force))
234        );
235
236        assert_eq!(
237            parse_dice_as_value("blue"),
238            Ok(("", Dice::Boost))
239        );
240        assert_eq!(
241            parse_dice_as_value("b"),
242            Ok(("", Dice::Boost))
243        );
244        assert_eq!(
245            parse_dice_as_value("boost"),
246            Ok(("", Dice::Boost))
247        );
248        assert!(parse_dice_as_value("6 + 2").is_err());
249    }
250
251    #[test]
252    fn test_parse_dice() {
253        assert_eq!(
254            parse_dice("2p"),
255            Ok(("", DiceRoll::new(Dice::Difficulty, 2)))
256        );
257        assert_eq!(
258            parse_dice("6ryyy"),
259            Ok(("yyy", DiceRoll::new(Dice::Challenge, 6)))
260        );
261        assert_eq!(
262            parse_dice("ggbpp"),
263            Ok(("gbpp", DiceRoll::new(Dice::Ability, 1)))
264        );
265
266        assert!(parse_dice("*1").is_err());
267    }
268
269    #[test]
270    fn test_parse_group() {
271        assert_eq!(parse_group("6ryyy"), Ok(("", vec![DiceRoll::new(Dice::Challenge, 6), DiceRoll::new(Dice::Proficiency, 3)])));
272        assert_eq!(parse_group("d"), Ok(("", vec![DiceRoll::new(Dice::Difficulty, 1)])));
273        assert_eq!(parse_group("ddkb"), Ok(("", vec![DiceRoll::new(Dice::Difficulty, 2), DiceRoll::new(Dice::Setback, 1), DiceRoll::new(Dice::Boost, 1)])));
274    }
275}