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
156pub 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
247pub 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}