team04_server/config/
board.rs

1use std::fmt::Display;
2
3use crate::board::{Board, BoardError, TileType};
4use serde::de::Error;
5use serde::{Deserialize, Serialize};
6
7/// A board configuration.
8#[derive(Debug, Serialize, PartialEq)]
9pub struct BoardConfig {
10    /// The minimum number of players necessary to start a game.
11    #[serde(rename = "minPlayers")]
12    pub min_players: u64,
13
14    /// The maximum number of players allowed to join the lobby.
15    #[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}