1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259
//! Provides a simplistic [`Parser`] that converts
//! the [PlayOnBSD Database](https://github.com/playonbsd/OpenBSD-Games-Database)
//! (either provided as a string or as a file) into a vector of [`Game`]s.
//!
//! ### Examples
//! Here is a first example loading a file in relaxed mode (by default).
//! ```no_run
//! use libpobsd::{Parser, ParserResult};
//!
//! // Create a parser
//! let parser = Parser::default();
//! // Load the database
//! let parser_result = parser.load_from_file("/path/to/games.db")
//! .expect("Problem trying to open the file");
//! let games = match parser_result {
//! ParserResult::WithoutError(games) => games,
//! ParserResult::WithError(games, _) => games,
//! };
//! ```
//! The parser can also use a strict mode in which it will stop when encountering
//! a parsing error and returning the games it has processed.
//! ```no_run
//! use libpobsd::{Parser, ParserResult, ParsingMode};
//!
//! // Create a parser in strict mode
//! let parser = Parser::new(ParsingMode::Strict);
//! // Load the database
//! let parser_result = parser.load_from_file("/path/to/games.db")
//! .expect("Problem trying to open the file");
//! let games = match parser_result {
//! ParserResult::WithoutError(games) => games,
//! ParserResult::WithError(games, _) => games,
//! };
//! ```
//! The parser can also load from a [`&str`] or a [`String`].
//! ```
//! use libpobsd::{Parser, ParserResult, ParsingMode, Game};
//!
//! let games = r#"Game AaaaaAAaaaAAAaaAAAAaAAAAA!!! for the Awesome
//! Cover AaaaaA_for_the_Awesome_Cover.jpg
//! Engine
//! Setup
//! Runtime HumblePlay
//! Store https://www.humblebundle.com/store/aaaaaaaaaaaaaaaaaaaaaaaaa-for-the-awesome
//! Hints Demo on HumbleBundle store page
//! Genre
//! Tags
//! Year 2011
//! Dev
//! Pub
//! Version
//! Status
//! Added 1970-01-01
//! Updated 1970-01-01
//! IgdbId 12
//! Game The Adventures of Mr. Hat
//! Cover
//! Engine godot
//! Setup
//! Runtime godot
//! Store https://store.steampowered.com/app/1869200/The_Adventures_of_Mr_Hat/
//! Hints
//! Genre Puzzle Platformer
//! Tags indie
//! Year
//! Dev AX-GAME
//! Pub Fun Quarter
//! Version Early Access
//! Status runs (2022-05-13)
//! Added 2022-05-13
//! Updated 2022-05-13
//! IgdbId 13"#;
//!
//! let parser = Parser::default();
//! let games = match parser.load_from_string(games) {
//! ParserResult::WithoutError(games) => games,
//! // Should not panic since the data are fine
//! ParserResult::WithError(_, _) => panic!(),
//! };
//! let game1: &Game = games.get(1).unwrap();
//! assert_eq!(Some(String::from("godot")), game1.engine);
//!
//! ```
#[macro_use]
pub(crate) mod parser_macros;
use crate::models::field::Field;
use crate::Game;
use hash32::{FnvHasher, Hasher};
use std::fs;
use std::hash::Hash;
use std::path::Path;
enum ParserState {
Parsing,
Error,
}
/// Represents the two parsing modes supported by [`Parser`].
pub enum ParsingMode {
/// In **strict mode**, the parsing will stop if a parsing error occurs
/// returning the games processed before the error as well as the line
/// number at which the error occurred.
Strict,
/// In **relaxed mode**, the parsing will continue even after encountering
/// an error, and returning all the games that have been parsed as well as
/// the line numbers that were ignored due to parsing errors.
Relaxed,
}
/// Represents the result of the parsing. When in strict mode,
/// only the games parsed before a parsing error occurred will
/// be returned. In relaxed mode, the parser will do its best
/// to continue parsing games.
pub enum ParserResult {
/// Result of the parsing when an error occurred. It holds a vector
/// of [`Game`] parsed from the database and a vector of the lines where
/// errors occurred.
WithError(Vec<Game>, Vec<usize>),
/// Result of the parsing when no error occurred. It holds a vector
/// of [`Game`] parsed from the database.
WithoutError(Vec<Game>),
}
impl From<ParserResult> for Vec<Game> {
fn from(val: ParserResult) -> Self {
match val {
ParserResult::WithError(games, _) => games,
ParserResult::WithoutError(games) => games,
}
}
}
/// Parses the PlayOnBSD database provided as a [`&str`] or from
/// a file and returns a [`ParserResult`] holding a vector of
/// [`Game`] contained in the PlayOnBSD database.
pub struct Parser {
state: ParserState,
games: Vec<Game>,
current_line: usize,
error_lines: Vec<usize>,
mode: ParsingMode,
}
impl Default for Parser {
fn default() -> Self {
Self {
state: ParserState::Parsing,
games: Vec::new(),
current_line: 0,
error_lines: Vec::new(),
mode: ParsingMode::Relaxed,
}
}
}
impl Parser {
/// Creates a [`Parser`] set to the given [`ParsingMode`].
pub fn new(mode: ParsingMode) -> Self {
Self {
state: ParserState::Parsing,
games: Vec::new(),
current_line: 0,
error_lines: Vec::new(),
mode,
}
}
/// Load the PlayOnBSD database from a file.
pub fn load_from_file(self, file: impl AsRef<Path>) -> Result<ParserResult, std::io::Error> {
let file: &Path = file.as_ref();
if file.is_file() {
let data = fs::read_to_string(file)?;
Ok(self.load_from_string(&data))
} else {
Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"This is not a file",
))
}
}
/// Load the database from a [`&str`].
pub fn load_from_string(mut self, data: &str) -> ParserResult {
for line in data.lines() {
self.current_line += 1;
self.parse(line);
if let ParserState::Error = self.state {
self.error_lines.push(self.current_line);
if let ParsingMode::Strict = self.mode {
break;
}
self.state = ParserState::Parsing;
};
}
for game in &mut self.games {
let mut fnv = FnvHasher::default();
// This is ugly but for compatibility
// uid should not change while updating
// libpobsd
let added = game.added.format("%Y-%m-%d").to_string();
Some(added).hash(&mut fnv);
game.name.hash(&mut fnv);
game.uid = fnv.finish32();
}
match self.error_lines.is_empty() {
false => ParserResult::WithError(self.games, self.error_lines),
true => ParserResult::WithoutError(self.games),
}
}
impl_parse![Field::Game, name;
(Field::Cover, cover);
(Field::Engine, engine);
(Field::Setup, setup);
(Field::Runtime, runtime);
(Field::Store, stores);
(Field::Hints, hints);
(Field::Genres, genres);
(Field::Tags, tags);
(Field::Year, year);
(Field::Dev, devs);
(Field::Publi, publis);
(Field::Version, version);
(Field::Status, status);
(Field::Added, added);
(Field::Updated, updated);
(Field::IgdbId, igdb_id)
];
}
#[cfg(test)]
mod game_tests {
use super::*;
#[test]
fn test_from_parse_result_without_error_to_vec() {
let game = Game::new();
let game_bis = Game::new();
let games1 = vec![game, game_bis];
let games2 = games1.clone();
let parse_result = ParserResult::WithoutError(games2);
let games_test: Vec<Game> = parse_result.into();
assert_eq!(games1, games_test);
}
#[test]
fn test_from_parse_result_with_error_to_vec() {
let game = Game::new();
let game_bis = Game::new();
let games1 = vec![game, game_bis];
let games2 = games1.clone();
let parse_result = ParserResult::WithError(games2, vec![]);
let games_test: Vec<Game> = parse_result.into();
assert_eq!(games1, games_test);
}
#[test]
fn load_from_file_fail() {
let re = match Parser::default().load_from_file("nothere") {
Ok(_) => panic!(),
Err(e) => e,
};
let error_type = std::io::ErrorKind::InvalidInput;
assert_eq!(re.kind(), error_type);
}
}