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
//! [<img alt="github" src="https://img.shields.io/badge/github-8da0cb?style=for-the-badge&labelColor=555555&logo=github" height="20">](https://github.com/20jasper/gcg-parser)
//! [<img alt="crates.io" src="https://img.shields.io/badge/crates.io-fc8d62?style=for-the-badge&labelColor=555555&logo=rust" height="20">](https://crates.io/crates/gcg-parser)
//! [<img alt="docs.rs" src="https://img.shields.io/badge/docs.rs-66c2a5?style=for-the-badge&labelColor=555555&logo=docs.rs" height="20">](https://docs.rs/gcg-parser)
//! <br>
//! gcg-parser converts "generic crossword game" (GCG) files into more convenient formats
//!
//! GCG files are used as a standard for a variety of crossword games, most notably Scrabble
//!
//! ## Specification
//!
//! This parser abides by the [Poslfit GCG specification](https://www.poslfit.com/scrabble/gcg/)

pub mod error;
pub mod events;
mod player;

use error::{GcgError, Result};
use player::Player;

#[derive(Debug, PartialEq)]
pub struct Gcg {
	pub player1: Player,
	pub player2: Player,
	/// A long description of the game. May contain HTML entities
	pub description: Option<String>,
}

impl Gcg {
	pub fn build(text: &str) -> Result<Gcg> {
		let mut player1 = None::<Player>;
		let mut player2 = None::<Player>;
		let mut description = None::<String>;

		for (i, line) in text.lines().enumerate() {
			if line.starts_with("#player1") {
				let player = Player::build(line, i)?;
				player1 = Some(player);
			} else if line.starts_with("#player2") {
				let player = Player::build(line, i)?;
				player2 = Some(player);
			} else if line.starts_with("#description") {
				let (_, desc) = line.split_once(' ').unwrap_or_default();

				description = Some(desc.to_string());
			} else {
				return Err(GcgError::UnknownPragma {
					line: text.to_string(),
					line_index: i.saturating_add(1),
				});
			}
		}

		let gcg = Gcg {
			player1: player1.ok_or_else(|| GcgError::MissingPragma {
				keyword: "player1".to_string(),
			})?,
			player2: player2.ok_or_else(|| GcgError::MissingPragma {
				keyword: "player2".to_string(),
			})?,
			description,
		};

		Ok(gcg)
	}
}

#[cfg(test)]
mod tests {
	use super::*;
	use anyhow::{Ok, Result};

	#[test]
	fn should_parse_player_names() -> Result<()> {
		let text = [
			"#player1 20jasper Jacob Asper",
			"#player2 xXFerrisXx Ferris The Crab",
		]
		.join("\n");

		let gcg = Gcg::build(&text)?;

		assert_eq!(
			gcg.player1,
			Player {
				nickname: "20jasper".to_string(),
				full_name: "Jacob Asper".to_string(),
			},
		);
		assert_eq!(
			gcg.player2,
			Player {
				nickname: "xXFerrisXx".to_string(),
				full_name: "Ferris The Crab".to_string(),
			},
		);

		Ok(())
	}

	#[test]
	fn should_error_when_missing_player() {
		let text = ["#player2 20jasper Jacob Asper"].join("\n");

		let error = Gcg::build(&text)
			.unwrap_err()
			.to_string()
			.to_lowercase();

		assert!(error.contains("player1"));
	}

	#[test]
	fn should_error_with_unknown_pragma() {
		let text = ["#whatisthispragma what idk"].join("\n");

		let error = Gcg::build(&text)
			.unwrap_err()
			.to_string()
			.to_lowercase();

		assert!(error.contains("unknown pragma"));
		assert!(error.contains("#whatisthispragma"));
	}

	#[test]
	fn should_parse_description() -> Result<()> {
		let text = [
			"#player1 20jasper Jacob Asper",
			"#player2 xXFerrisXx Ferris The Crab",
			"#description 20jasper vs xXFerrisXx",
		]
		.join("\n");

		let gcg = Gcg::build(&text)?;

		assert_eq!(gcg.description, Some("20jasper vs xXFerrisXx".to_string()));

		Ok(())
	}
}