pobsd_parser/
parser.rs

1//! Provides a simplistic [`Parser`] that converts
2//! the [PlayOnBSD Database](https://github.com/playonbsd/OpenBSD-Games-Database)
3//! (either provided as a string or as a file) into a vector of [`Game`] objects.
4//!
5use hash32::{FnvHasher, Hasher};
6use std::fs;
7use std::hash::Hash;
8use std::path::Path;
9
10use super::field::Field;
11use super::game::Game;
12
13pub trait State {}
14
15enum ParserState {
16    Game,
17    Cover,
18    Engine,
19    Setup,
20    Runtime,
21    Store,
22    Hints,
23    Genre,
24    Tags,
25    Year,
26    Dev,
27    Pub,
28    Version,
29    Status,
30    Added,
31    Updated,
32    IgdbId,
33    Error,
34    Recovering,
35}
36
37/// The [`ParsingMode`] enum is used to represent the two parsing modes
38/// supported by [`Parser`]:
39/// * a **strict mode** in which the parsing
40///  will stop if a parsing error occurs returning the games processed
41/// before the error as well as the line in the input (file or string)
42/// where the error occured;
43/// * a **relaxed mode** in which the parsing
44/// will continue even after an error is encountered, the parsing
45/// resuming when reaching the next game after the parsing error
46/// ; it returns all the games that have been parsed as well as
47/// the lines that were ignored due to parsing errors.
48pub enum ParsingMode {
49    Strict,
50    Relaxed,
51}
52
53/// Represent the result of the parsing.
54///
55/// If there is no error, the [`ParserResult::WithoutError`] variant
56/// is returned holding a vector of the games in the database. If there
57/// is at least one error, the [`ParserResult::WithError`] variant is
58/// returned holding a vector of the games in the database and a vector
59/// of the lines where errors occured. If in strict mode only the games
60/// parsed before the error occured will be returned.
61pub enum ParserResult {
62    WithError(Vec<Game>, Vec<usize>),
63    WithoutError(Vec<Game>),
64}
65
66impl From<ParserResult> for Vec<Game> {
67    fn from(val: ParserResult) -> Self {
68        match val {
69            ParserResult::WithError(games, _) => games,
70            ParserResult::WithoutError(games) => games,
71        }
72    }
73}
74/// Parser provides a parser that can be created using the [`Parser::new`] method
75/// which takes a [`ParsingMode`] enum as only argument.
76pub struct Parser {
77    state: ParserState,
78    games: Vec<Game>,
79    current_line: usize,
80    error_lines: Vec<usize>,
81    mode: ParsingMode,
82}
83
84impl Default for Parser {
85    fn default() -> Self {
86        Self {
87            state: ParserState::Game,
88            games: Vec::new(),
89            current_line: 0,
90            error_lines: Vec::new(),
91            mode: ParsingMode::Relaxed,
92        }
93    }
94}
95impl Parser {
96    /// Crate a parser with a given parsing mode
97    pub fn new(mode: ParsingMode) -> Self {
98        Self {
99            state: ParserState::Game,
100            games: Vec::new(),
101            current_line: 0,
102            error_lines: Vec::new(),
103            mode,
104        }
105    }
106    /// Load the database from a file.
107    pub fn load_from_file(self, file: impl AsRef<Path>) -> Result<ParserResult, std::io::Error> {
108        let file: &Path = file.as_ref();
109        if file.is_file() {
110            let data = fs::read_to_string(file)?;
111            Ok(self.load_from_string(&data))
112        } else {
113            Err(std::io::Error::new(
114                std::io::ErrorKind::InvalidInput,
115                "This is not a file",
116            ))
117        }
118    }
119    /// Load the database from a string.
120    pub fn load_from_string(mut self, data: &str) -> ParserResult {
121        for line in data.lines() {
122            self.current_line += 1;
123            self.parse(line);
124            if let ParserState::Error = self.state {
125                self.error_lines.push(self.current_line);
126                if let ParsingMode::Strict = self.mode {
127                    break;
128                }
129            };
130        }
131        for game in &mut self.games {
132            let mut fnv = FnvHasher::default();
133            game.added.hash(&mut fnv);
134            game.name.hash(&mut fnv);
135            game.uid = fnv.finish32();
136        }
137        match self.error_lines.is_empty() {
138            false => ParserResult::WithError(self.games, self.error_lines),
139            true => ParserResult::WithoutError(self.games),
140        }
141    }
142    impl_parse![ParserState::Game, Field::Game, name, ParserState::Cover;
143         (ParserState::Cover, Field::Cover, cover, ParserState::Engine);
144         (ParserState::Engine, Field::Engine, engine, ParserState::Setup);
145         (ParserState::Setup, Field::Setup, setup, ParserState::Runtime);
146         (ParserState::Runtime, Field::Runtime, runtime, ParserState::Store);
147         (ParserState::Store, Field::Store, stores, ParserState::Hints);
148         (ParserState::Hints, Field::Hints, hints, ParserState::Genre);
149         (ParserState::Genre, Field::Genres, genres, ParserState::Tags);
150         (ParserState::Tags, Field::Tags, tags, ParserState::Year);
151         (ParserState::Year, Field::Year, year, ParserState::Dev);
152         (ParserState::Dev, Field::Dev, dev, ParserState::Pub);
153         (ParserState::Pub, Field::Publi, publi, ParserState::Version);
154         (ParserState::Version, Field::Version, version, ParserState::Status);
155         (ParserState::Status, Field::Status, status, ParserState::Added);
156         (ParserState::Added, Field::Added, added, ParserState::Updated);
157         (ParserState::Updated, Field::Updated, updated, ParserState::IgdbId);
158         (ParserState::IgdbId, Field::IgdbId, igdb_id, ParserState::Game)
159    ];
160}