shrek_deck/
parser.rs

1use std::{
2    fmt::Display,
3    fs::File,
4    io::{self, BufRead, BufReader},
5    num::ParseIntError,
6    path::PathBuf,
7};
8
9use crate::{CardEntry, GetCardInfo};
10
11pub enum Error {
12    UnexpectedChar {
13        obtained: char,
14        expected: Vec<String>,
15    },
16    AmountIsZero {
17        card_name: String,
18    },
19    NameIsEmpty,
20    NotANumber {
21        string: String,
22        error: ParseIntError,
23    },
24    CantOpenFile {
25        path: PathBuf,
26        error: io::Error,
27    },
28    NameMultipleTimes {
29        name: String,
30    },
31    CouldntReadLine {
32        path: PathBuf,
33        line: usize,
34        error: io::Error,
35    },
36}
37
38impl Display for Error {
39    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40        match self {
41            Self::UnexpectedChar { obtained, expected } => {
42                let obtained = if *obtained == '\n' || *obtained == '\r' {
43                    "<newline>".to_string()
44                } else if *obtained == '\t' {
45                    "<tab>".to_string()
46                } else {
47                    obtained.to_string()
48                };
49                write!(
50                    f,
51                    "\n Obtained character `{obtained}`, expected one of the following: "
52                )?;
53
54                for expected in expected {
55                    write!(f, "\n - {expected}")?;
56                }
57
58                Ok(())
59            }
60            Self::AmountIsZero { card_name } => write!(
61                f,
62                "Tried to create {card_name} with an amount of 0, which is frankly ridiculous"
63            ),
64            Self::NameIsEmpty => write!(f, "Tried to create a card with an empty name"),
65            Self::NotANumber { string, error } => {
66                write!(f, "Failed to parse `{string}` as a number:\n  {error}")
67            }
68            Self::CantOpenFile { path, error } => write!(
69                f,
70                "Failed to load file `{}`, with the following error: {error}",
71                path.display()
72            ),
73            Self::NameMultipleTimes { name } => write!(
74                f,
75                "The name `{name}` appears multiple times, which is not allowed."
76            ),
77            Self::CouldntReadLine { path, line, error } => {
78                write!(
79                    f,
80                    "Failed to read line {line} in file {}:\n  {error}",
81                    path.display()
82                )
83            }
84        }
85    }
86}
87
88pub struct ParseError {
89    position: LinePosition,
90    error: Error,
91}
92
93impl ParseError {
94    fn at_line(self, line: usize) -> Self {
95        Self {
96            position: LinePosition {
97                line: Some(line),
98                ..self.position
99            },
100            ..self
101        }
102    }
103}
104
105impl Display for ParseError {
106    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
107        match self.position {
108            LinePosition {
109                line: None,
110                column: None,
111            } => write!(f, "Error at unknown position: {}", self.error),
112            LinePosition {
113                line: Some(line),
114                column: Some(column),
115            } => {
116                write!(
117                    f,
118                    "Error at line {}, column {}: {}",
119                    line, column, self.error
120                )
121            }
122            LinePosition {
123                line: None,
124                column: Some(column),
125            } => {
126                write!(
127                    f,
128                    "Error at unknown line, column {}: {}",
129                    column, self.error
130                )
131            }
132            LinePosition {
133                line: Some(line),
134                column: None,
135            } => {
136                write!(f, "Error at line {}: {}", line, self.error)
137            }
138        }
139    }
140}
141
142pub struct LinePosition {
143    line: Option<usize>,
144    column: Option<usize>,
145}
146
147impl LinePosition {
148    const fn void() -> Self {
149        Self {
150            line: None,
151            column: None,
152        }
153    }
154}
155
156/// Parses a line of text
157/// # Errors
158/// - Whenever the supplied `GetCardInfo` implementation of `parse` fails.
159/// - Whenever a non-arabic digit character that is neither a space, a tab or an `x` is found during the parsing of the number.
160/// - If the characters found as the amount of copies of the card cannot be parsed into an i64.
161/// - If the characters found as the amount of copies of the card are parsed into the number 0.
162/// - If the characters found as the name of the card is empty after being trimmed of spaces.
163pub fn parse_line<T: GetCardInfo + Clone>(string: &str) -> Result<CardEntry<T>, ParseError> {
164    let mut parserstate = ParserState::Numbering;
165    let mut number_str = String::new();
166    let mut name = String::new();
167    for (idx, chr) in string.char_indices() {
168        match parserstate {
169            ParserState::Numbering => match chr {
170                chr @ ('0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9') => {
171                    number_str.push(chr);
172                }
173                ' ' | '\t' => parserstate = ParserState::Exing,
174                'x' => parserstate = ParserState::Naming,
175                chr => {
176                    let mut expected = vec!["a digit".to_string()];
177                    if !number_str.is_empty() {
178                        expected.push("a number separator (space, tab or `x`)".to_string());
179                        expected.push("a card name".to_string());
180                    }
181                    return Err(ParseError {
182                        error: Error::UnexpectedChar {
183                            obtained: chr,
184                            expected,
185                        },
186                        position: LinePosition {
187                            line: None,
188                            column: Some(idx + 1),
189                        },
190                    });
191                }
192            },
193            ParserState::Exing => match chr {
194                ' ' | '\t' => continue,
195                'x' => parserstate = ParserState::Naming,
196                chr => {
197                    name.push(chr);
198                    parserstate = ParserState::Naming;
199                }
200            },
201            ParserState::Naming => name.push(chr),
202        }
203    }
204    let name = name.trim().to_owned();
205
206    let number = number_str.parse().map_err(|error| ParseError {
207        position: LinePosition {
208            line: None,
209            column: None,
210        },
211        error: Error::NotANumber {
212            string: number_str,
213            error,
214        },
215    })?;
216
217    if number == 0 {
218        return Err(ParseError {
219            error: Error::AmountIsZero { card_name: name },
220            position: LinePosition {
221                line: None,
222                column: None,
223            },
224        });
225    } else if name.is_empty() {
226        return Err(ParseError {
227            error: Error::NameIsEmpty,
228            position: LinePosition {
229                line: None,
230                column: None,
231            },
232        });
233    }
234
235    Ok(CardEntry {
236        card: T::parse(&name)?,
237        amount: number,
238    })
239}
240
241enum ParserState {
242    Numbering,
243    Naming,
244    Exing,
245}
246
247/// Parses a file
248/// # Errors
249/// - If `parse_line` fails on any of the lines
250/// - If the same card name appears multiple times in the file
251/// - If the reader fails to read a line
252pub fn parse_file<T: GetCardInfo + Clone>(
253    path: &PathBuf,
254) -> Result<Vec<CardEntry<T>>, Vec<ParseError>> {
255    let file = File::open(path).map_err(|error| {
256        vec![ParseError {
257            position: LinePosition::void(),
258            error: Error::CantOpenFile {
259                path: path.clone(),
260                error,
261            },
262        }]
263    })?;
264    let mut reader = BufReader::new(file);
265    let mut cards = vec![];
266    let mut used_names = vec![];
267    let mut line_idx = 0;
268    let mut errors = vec![];
269    loop {
270        line_idx += 1;
271        let mut line = String::new();
272        match reader.read_line(&mut line) {
273            Ok(0) => break,
274            Ok(_) if !line.trim().is_empty() => match parse_line::<T>(&line) {
275                Ok(entry) => {
276                    let name = entry.card.get_name().to_owned();
277                    if used_names.contains(&name) {
278                        errors.push(ParseError {
279                            position: LinePosition {
280                                line: Some(line_idx),
281                                column: None,
282                            },
283                            error: Error::NameMultipleTimes { name },
284                        });
285                    } else {
286                        used_names.push(name);
287                        cards.push(entry);
288                    }
289                }
290                Err(error) => errors.push(error.at_line(line_idx)),
291            },
292            Ok(_) => continue,
293            Err(error) => errors.push(ParseError {
294                position: LinePosition {
295                    line: Some(line_idx),
296                    column: None,
297                },
298                error: Error::CouldntReadLine {
299                    path: path.clone(),
300                    line: line_idx,
301                    error,
302                },
303            }),
304        }
305    }
306    if errors.is_empty() {
307        Ok(cards)
308    } else {
309        Err(errors)
310    }
311}