pobsd_parser/
game.rs

1//! Provides a representations of the game in the PlayOnBSD database.
2use crate::store_links::StoreLinks;
3use std::cmp::{Ordering, PartialOrd};
4use std::fmt;
5
6/// Represents a game from the database.
7///
8/// It also includes an additional [`Game::uid`] field
9/// derived from the name of the game as well as the date to
10/// which the game was added to the database. It therefore
11/// provides an unique identifier under the assumption that no
12/// game with the same name will be added the same dat into
13/// the databas.
14///
15/// The name of some fields differs from the one used
16/// in the database itself: Genre and Store are plural
17/// since there can be more than one item for each
18/// and Pub translate to publi since pub is a reserved
19/// keyword in Rust.
20///
21/// All fields are optional strings or vectors of strings
22/// except for the name of the game which is mandatory.
23/// The parser does not try to be smart with dates and
24/// just store them as string.
25///
26/// ### Display
27/// The [`Game`] struct implement the [`core::fmt::Display`] trait
28/// and will be displayed as it would appear in the
29/// PlayOnBSD database.
30///
31/// ### PartialOrd
32/// The [`Game`] struct implements the [`core::cmp::PartialOrd`] trait
33/// and [`Game`] objects are ordered according to their name (without The or A).
34#[derive(Serialize, Clone, Default, Debug, PartialEq, Eq)]
35pub struct Game {
36    /// An unique identifier generated from the name and added fields
37    pub uid: u32,
38    /// The name of the game.
39    pub name: String,
40    /// The cover of the game.
41    pub cover: Option<String>,
42    /// The engine used by the game.
43    pub engine: Option<String>,
44    /// Step(s) to setup the game.
45    pub setup: Option<String>,
46    /// The executable in the package.
47    pub runtime: Option<String>,
48    /// A vector with store urls.
49    pub stores: Option<StoreLinks>,
50    /// Hints (as the name imply).
51    pub hints: Option<String>,
52    /// A vector of genres associated with the game.
53    pub genres: Option<Vec<String>>,
54    /// A vector of tags associated with the game.
55    pub tags: Option<Vec<String>>,
56    /// Released year (can be text such as "early access".
57    pub year: Option<String>,
58    /// Developer.
59    pub dev: Option<String>,
60    /// Publisher.
61    #[serde(rename = "pub")]
62    pub publi: Option<String>,
63    /// Version of the game.
64    pub version: Option<String>,
65    /// When tested on -current.
66    pub status: Option<String>,
67    /// When added
68    pub added: Option<String>,
69    /// When updated
70    pub updated: Option<String>,
71    /// The IGDB Id of the game
72    pub igdb_id: Option<String>,
73}
74
75impl<'a> Game {
76    #[allow(dead_code)]
77    pub fn new() -> Self {
78        Self::default()
79    }
80    fn get_ordering_name(&'a self) -> String {
81        if let Some(name) = self.name.to_lowercase().strip_prefix("the ") {
82            return name.to_string();
83        }
84        if let Some(name) = self.name.to_lowercase().strip_prefix("a ") {
85            return name.to_string();
86        }
87        self.name.to_lowercase()
88    }
89}
90
91impl PartialOrd for Game {
92    fn partial_cmp(&self, other: &Game) -> Option<Ordering> {
93        self.get_ordering_name()
94            .partial_cmp(&other.get_ordering_name())
95    }
96    fn lt(&self, other: &Game) -> bool {
97        self.get_ordering_name().lt(&other.get_ordering_name())
98    }
99    fn le(&self, other: &Game) -> bool {
100        self.get_ordering_name().le(&other.get_ordering_name())
101    }
102    fn gt(&self, other: &Game) -> bool {
103        self.get_ordering_name().gt(&other.get_ordering_name())
104    }
105    fn ge(&self, other: &Game) -> bool {
106        self.get_ordering_name().ge(&other.get_ordering_name())
107    }
108}
109
110impl Ord for Game {
111    fn cmp(&self, other: &Game) -> Ordering {
112        self.get_ordering_name().cmp(&other.get_ordering_name())
113    }
114}
115
116/// Display the game as it would appears in the database.
117/// See <https://github.com/playonbsd/OpenBSD-Games-Database>
118/// for details.
119impl fmt::Display for Game {
120    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
121        let game = format!("Game\t{}", self.name);
122        let cover = match &self.cover {
123            Some(cover) => format!("Cover\t{}", cover),
124            None => "Cover".to_string(),
125        };
126        let engine = match &self.engine {
127            Some(engine) => format!("Engine\t{}", engine),
128            None => "Engine".to_string(),
129        };
130        let setup = match &self.setup {
131            Some(setup) => format!("Setup\t{}", setup),
132            None => "Setup".to_string(),
133        };
134        let runtime = match &self.runtime {
135            Some(runtime) => format!("Runtime\t{}", runtime),
136            None => "Runtime".to_string(),
137        };
138        let stores = match &self.stores {
139            Some(stores) => format!(
140                "Store\t{}",
141                stores
142                    .inner_ref()
143                    .into_iter()
144                    .map(|a| a.url.to_string())
145                    .collect::<Vec<String>>()
146                    .join(" ")
147            ),
148            None => "Store".to_string(),
149        };
150        let hints = match &self.hints {
151            Some(hints) => format!("Hints\t{}", hints),
152            None => "Hints".to_string(),
153        };
154        let genres = match &self.genres {
155            Some(genres) => format!("Genre\t{}", genres.join(", ")),
156            None => "Genre".to_string(),
157        };
158        let tags = match &self.tags {
159            Some(tags) => format!("Tags\t{}", tags.join(", ")),
160            None => "Tags".to_string(),
161        };
162        let year = match &self.year {
163            Some(year) => format!("Year\t{}", year),
164            None => "Year".to_string(),
165        };
166        let dev = match &self.dev {
167            Some(dev) => format!("Dev\t{}", dev),
168            None => "Dev".to_string(),
169        };
170        let publi = match &self.publi {
171            Some(publi) => format!("Pub\t{}", publi),
172            None => "Pub".to_string(),
173        };
174        let version = match &self.version {
175            Some(version) => format!("Version\t{}", version),
176            None => "Version".to_string(),
177        };
178        let status = match &self.status {
179            Some(status) => format!("Status\t{}", status),
180            None => "Status".to_string(),
181        };
182        let added = match &self.added {
183            Some(added) => format!("Added\t{}", added),
184            None => "Added".to_string(),
185        };
186        let updated = match &self.updated {
187            Some(updated) => format!("Updated\t{}", updated),
188            None => "Updated".to_string(),
189        };
190        let igdb_id = match &self.igdb_id {
191            Some(runtime) => format!("IgdbId\t{}", runtime),
192            None => "IgdbId".to_string(),
193        };
194        write!(
195            f,
196            "{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}",
197            game,
198            cover,
199            engine,
200            setup,
201            runtime,
202            stores,
203            hints,
204            genres,
205            tags,
206            year,
207            dev,
208            publi,
209            version,
210            status,
211            added,
212            updated,
213            igdb_id,
214        )
215    }
216}
217
218/* ------------------------- TESTS --------------------------*/
219
220#[cfg(test)]
221mod game_tests {
222    use crate::store_links::StoreLink;
223
224    use super::*;
225    fn create_game() -> Game {
226        let mut game = Game::default();
227        let tags: Vec<String> = vec!["tag1".to_string(), "tag2".to_string()];
228        let genres: Vec<String> = vec!["genre1".to_string(), "genre2".to_string()];
229        let stores: Vec<String> = vec!["store1".to_string(), "store2".to_string()];
230        let store_links: Vec<StoreLink> = stores.into_iter().map(|a| StoreLink::from(&a)).collect();
231        let stores = StoreLinks(store_links);
232        game.uid = 1221;
233        game.name = "game name".to_string();
234        game.cover = Some("cover.jpg".to_string());
235        game.engine = Some("game engine".to_string());
236        game.setup = Some("game setup".to_string());
237        game.runtime = Some("game runtime".to_string());
238        game.stores = Some(stores);
239        game.hints = Some("game hints".to_string());
240        game.genres = Some(genres);
241        game.tags = Some(tags);
242        game.year = Some("1980".to_string());
243        game.dev = Some("game dev".to_string());
244        game.publi = Some("game publi".to_string());
245        game.version = Some("game version".to_string());
246        game.status = Some("game status".to_string());
247        game.added = Some("2012-12-03".to_string());
248        game.updated = Some("2014-12-03".to_string());
249        game
250    }
251    #[test]
252    fn test_default_equivalent_to_new() {
253        let game = Game::default();
254        let game_bis = Game::new();
255        assert!(game == game_bis);
256    }
257    #[test]
258    fn test_get_ordering_name_with_a() {
259        let mut game = create_game();
260        game.name = "A champion".into();
261        assert_eq!(game.get_ordering_name(), "champion");
262        game.name = "a champion".into();
263        assert_eq!(game.get_ordering_name(), "champion");
264    }
265    #[test]
266    fn test_get_ordering_name_with_the() {
267        let mut game = create_game();
268        game.name = "The champion".into();
269        assert_eq!(game.get_ordering_name(), "champion");
270        game.name = "the champion".into();
271        assert_eq!(game.get_ordering_name(), "champion");
272    }
273    #[test]
274    fn test_ordering() {
275        let mut game1 = create_game();
276        let mut game2 = create_game();
277        game1.name = "Abc".into();
278        game2.name = "Def".into();
279        assert!(game2.gt(&game1));
280        assert!(game2.ge(&game1));
281        assert!(game1.le(&game2));
282        assert!(game1.lt(&game2));
283        game1.name = "The Abc".into();
284        game2.name = "def".into();
285        assert!(game2.gt(&game1));
286        assert!(game2.ge(&game1));
287        assert!(game1.le(&game2));
288        assert!(game1.lt(&game2));
289        game1.name = "The Abc".into();
290        game2.name = "A def".into();
291        assert!(game2.gt(&game1));
292        assert!(game2.ge(&game1));
293        assert!(game1.le(&game2));
294        assert!(game1.lt(&game2));
295    }
296    #[test]
297    fn test_display() {
298        let game_str = "Game\tAaaaaAAaaaAAAaaAAAAaAAAAA!!! for the Awesome
299Cover\tAaaaaA_for_the_Awesome_Cover.jpg
300Engine
301Setup
302Runtime\tHumblePlay
303Store\thttps://www.humblebundle.com/store/aaaaaaaaaaaaaaaaaaaaaaaaa-for-the-awesome
304Hints\tDemo on HumbleBundle store page
305Genre
306Tags
307Year\t2011
308Dev
309Pub
310Version
311Status
312Added
313Updated
314IgdbId";
315        let game = Game {
316            uid: 12,
317            name: "AaaaaAAaaaAAAaaAAAAaAAAAA!!! for the Awesome".to_string(),
318            cover: Some("AaaaaA_for_the_Awesome_Cover.jpg".to_string()),
319            engine: None,
320            setup: None,
321            runtime: Some("HumblePlay".to_string()),
322            stores: Some(StoreLinks(vec![StoreLink::from(
323                "https://www.humblebundle.com/store/aaaaaaaaaaaaaaaaaaaaaaaaa-for-the-awesome",
324            )])),
325            hints: Some("Demo on HumbleBundle store page".to_string()),
326            genres: None,
327            tags: None,
328            year: Some("2011".to_string()),
329            dev: None,
330            publi: None,
331            version: None,
332            status: None,
333            added: None,
334            updated: None,
335            igdb_id: None,
336        };
337        assert_eq!(format!("{}", game), game_str);
338    }
339}