1use std::fmt::Display;
2
3use crate::board::{Board, BoardError, TileType};
4use serde::de::Error;
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Serialize, PartialEq)]
9pub struct BoardConfig {
10 #[serde(rename = "minPlayers")]
12 pub min_players: u64,
13
14 #[serde(rename = "maxPlayers")]
16 pub max_players: u64,
17 pub board: Board,
18}
19
20impl BoardConfig {
21 fn from_json(json: json::BoardConfigJson) -> Result<Self, BoardConfigError> {
22 Ok(Self {
23 min_players: {
24 if json.minPlayers < 2 || json.minPlayers > 8 {
25 return Err(BoardConfigError::MinPlayersOutOfRange);
26 }
27 json.minPlayers
28 },
29 max_players: {
30 if json.maxPlayers < 2 || json.maxPlayers > 8 {
31 return Err(BoardConfigError::MaxPlayersOutOfRange);
32 }
33 if json.minPlayers > json.maxPlayers {
34 return Err(BoardConfigError::MinGreaterThanMax);
35 }
36 json.maxPlayers
37 },
38 board: {
39 let height = json.board.len();
40 if height < 1 {
41 return Err(BoardConfigError::ThereIsNoBoard);
42 }
43 let width = json.board[0].len();
44 for (i, row) in json.board.iter().enumerate().skip(1) {
45 if row.len() != width {
46 return Err(BoardConfigError::InvalidRowWidth {
47 row: i,
48 found_width: row.len(),
49 expected_width: width,
50 });
51 }
52 }
53 match Board::new(
54 json.board.into_iter().flatten().collect::<Vec<TileType>>(),
55 width,
56 height,
57 ) {
58 Err(e) => {
59 return Err(BoardConfigError::InvalidBoard(e));
60 }
61 Ok(board) => board,
62 }
63 },
64 })
65 }
66}
67
68#[derive(PartialEq)]
69pub enum BoardConfigError {
70 MinPlayersOutOfRange,
71 MaxPlayersOutOfRange,
72 MinGreaterThanMax,
73 ThereIsNoBoard,
74 InvalidRowWidth {
75 row: usize,
76 found_width: usize,
77 expected_width: usize,
78 },
79 InvalidBoard(BoardError),
80}
81impl Display for BoardConfigError {
82 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
83 match self {
84 Self::MinPlayersOutOfRange => write!(f, "minPlayers must be between 2 and 8"),
85 Self::MaxPlayersOutOfRange => write!(f, "maxPlayers must be between 2 and 8"),
86 Self::MinGreaterThanMax => {
87 write!(f, "maxPlayers must be greater than or equal to minPlayers")
88 }
89 Self::ThereIsNoBoard => write!(f, "there is no board"),
90 Self::InvalidRowWidth {
91 row,
92 found_width,
93 expected_width,
94 } => {
95 write!(
96 f,
97 "board row {} has invalid width {}, expected to be {} like in row 1",
98 *row + 1,
99 *found_width,
100 *expected_width
101 )
102 }
103 Self::InvalidBoard(e) => write!(f, "Invalid board: {}", e),
104 }
105 }
106}
107impl std::fmt::Debug for BoardConfigError {
108 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
109 write!(f, "{self}")
110 }
111}
112impl std::error::Error for BoardConfigError {}
113
114impl<'de> Deserialize<'de> for BoardConfig {
115 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
116 where
117 D: serde::Deserializer<'de>,
118 {
119 let json = json::BoardConfigJson::deserialize(deserializer)?;
120
121 Self::from_json(json).map_err(|e| Error::custom(e.to_string()))
122 }
123}
124
125#[allow(non_snake_case)]
126mod json {
127 use super::*;
128
129 #[derive(Deserialize)]
130 pub struct BoardConfigJson {
131 pub minPlayers: u64,
132 pub maxPlayers: u64,
133 pub board: Vec<Vec<TileType>>,
134 }
135}
136
137#[cfg(test)]
138mod test {
139 use crate::{
140 board::TileType,
141 config::{
142 BoardConfig,
143 board::{BoardConfigError, json::BoardConfigJson},
144 },
145 };
146
147 macro_rules! assert_err_matches {
148 ($pat:pat, $json:expr) => {
149 match BoardConfig::from_json($json) {
150 Err(e) => {
151 if matches!(e, $pat) {
152 eprintln!("correctly got error {e:?}");
153 } else {
154 panic!("expected {}, but got '{e}'", stringify!($pat));
155 }
156 }
157 Ok(_) => panic!("expected {}, but got Ok(_)", stringify!($pat)),
158 }
159 };
160 }
161
162 #[test]
163 fn from_json() {
164 assert!(
165 BoardConfig::from_json(BoardConfigJson {
166 minPlayers: 2,
167 maxPlayers: 8,
168 board: vec![
169 vec![
170 TileType::Grass,
171 TileType::Grass,
172 TileType::Grass,
173 TileType::Grass,
174 ],
175 vec![
176 TileType::Grass,
177 TileType::Grass,
178 TileType::Grass,
179 TileType::Grass,
180 ]
181 ]
182 })
183 .is_ok()
184 );
185 assert_err_matches!(
186 BoardConfigError::MinPlayersOutOfRange,
187 BoardConfigJson {
188 minPlayers: 1,
189 maxPlayers: 6,
190 board: vec![]
191 }
192 );
193 assert_err_matches!(
194 BoardConfigError::MaxPlayersOutOfRange,
195 BoardConfigJson {
196 minPlayers: 2,
197 maxPlayers: 9,
198 board: vec![]
199 }
200 );
201 assert_err_matches!(
202 BoardConfigError::MinGreaterThanMax,
203 BoardConfigJson {
204 minPlayers: 6,
205 maxPlayers: 3,
206 board: vec![]
207 }
208 );
209 assert_err_matches!(
210 BoardConfigError::ThereIsNoBoard,
211 BoardConfigJson {
212 minPlayers: 2,
213 maxPlayers: 8,
214 board: vec![]
215 }
216 );
217 assert_err_matches!(
218 BoardConfigError::InvalidRowWidth {
219 row: 1,
220 found_width: 5,
221 expected_width: 4
222 },
223 BoardConfigJson {
224 minPlayers: 2,
225 maxPlayers: 8,
226 board: vec![
227 vec![
228 TileType::Grass,
229 TileType::Grass,
230 TileType::Grass,
231 TileType::Grass
232 ],
233 vec![
234 TileType::Grass,
235 TileType::Grass,
236 TileType::Grass,
237 TileType::Grass,
238 TileType::Grass
239 ],
240 ]
241 }
242 );
243 assert_err_matches!(
244 BoardConfigError::InvalidBoard(_),
245 BoardConfigJson {
246 minPlayers: 2,
247 maxPlayers: 8,
248 board: vec![
249 vec![
250 TileType::Rock,
251 TileType::Rock,
252 TileType::Rock,
253 TileType::Rock,
254 ],
255 vec![
256 TileType::Rock,
257 TileType::Rock,
258 TileType::Rock,
259 TileType::Rock,
260 ],
261 ]
262 }
263 );
264 }
265}