1use std::error::Error;
2use std::fmt::{Display, Formatter};
3
4use crate::rle::RleSyntaxError;
5use crate::utilities::is_blank;
6use crate::{BoardSyntaxError, SavedGameSyntaxError, SokobanPlusSyntaxError, Tessellation};
7
8#[derive(Debug, Clone, PartialEq, Eq, Hash)]
10pub enum IoReadErrorKind {
11 BoardSyntaxError { ln: usize },
12 SavedGameSyntaxError { ln: usize },
13 SokobanPlusSyntaxError { ln: usize },
14 InvalidFileExtension { ext: String },
15 UnknownTessellationName { ln: usize, name: String },
16 XMLSyntaxError { ln: usize },
17 JsonSyntaxError { ln: usize },
18 IoError,
19}
20
21#[derive(Debug)]
22struct IoReadErrorPayload {
23 kind: IoReadErrorKind,
24 source: Option<Box<dyn Error>>,
25 msg: Option<String>,
26}
27
28#[derive(Debug)]
30pub struct IoReadError(Box<IoReadErrorPayload>);
31
32impl IoReadError {
33 pub fn kind(&self) -> &IoReadErrorKind { &self.0.kind }
34
35 pub(in crate::io) fn new_xml_syntax_error(
36 ln: usize, source: Option<Box<dyn Error>>, msg: String,
37 ) -> Self {
38 Self(Box::new(IoReadErrorPayload {
39 kind: IoReadErrorKind::XMLSyntaxError { ln },
40 source,
41 msg: Some(msg),
42 }))
43 }
44
45 pub(in crate::io) fn new_puzzle_syntax_error(ln: usize, source: BoardSyntaxError) -> Self {
46 Self(Box::new(IoReadErrorPayload {
47 kind: IoReadErrorKind::BoardSyntaxError { ln },
48 source: Some(Box::new(source)),
49 msg: None,
50 }))
51 }
52
53 pub(in crate::io) fn new_saved_game_syntax_error(
54 ln: usize, source: SavedGameSyntaxError,
55 ) -> Self {
56 Self(Box::new(IoReadErrorPayload {
57 kind: IoReadErrorKind::SavedGameSyntaxError { ln },
58 source: Some(Box::new(source)),
59 msg: None,
60 }))
61 }
62
63 pub(in crate::io) fn new_sokoban_plus_syntax_error(
64 ln: usize, source: SokobanPlusSyntaxError,
65 ) -> Self {
66 Self(Box::new(IoReadErrorPayload {
67 kind: IoReadErrorKind::SokobanPlusSyntaxError { ln },
68 source: Some(Box::new(source)),
69 msg: None,
70 }))
71 }
72
73 #[cfg(feature = "json")]
79 pub(in crate::io) fn new_json_syntax_error(source: serde_json::Error) -> Self {
80 Self(Box::new(IoReadErrorPayload {
81 kind: IoReadErrorKind::JsonSyntaxError { ln: source.line() },
82 msg: Some(source.to_string()),
83 source: Some(Box::new(source)),
84 }))
85 }
86
87 pub(in crate::io) fn new_invalid_extension_error(ext: &str) -> Self {
88 Self(Box::new(IoReadErrorPayload {
89 kind: IoReadErrorKind::InvalidFileExtension { ext: ext.into() },
90 source: None,
91 msg: None,
92 }))
93 }
94
95 pub(in crate::io) fn new_unknown_tessellation_error(ln: usize, name: &str) -> Self {
96 Self(Box::new(IoReadErrorPayload {
97 kind: IoReadErrorKind::UnknownTessellationName {
98 ln,
99 name: name.into(),
100 },
101 source: None,
102 msg: None,
103 }))
104 }
105}
106
107impl Display for IoReadError {
108 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
109 match &self.0.kind {
110 IoReadErrorKind::BoardSyntaxError { ln } => {
111 write!(f, "syntax error at line {ln} of Puzzle data")
112 }
113
114 IoReadErrorKind::SavedGameSyntaxError { ln } => {
115 write!(f, "syntax error at line {ln} of SavedGame data")
116 }
117
118 IoReadErrorKind::SokobanPlusSyntaxError { ln } => {
119 write!(f, "syntax error at line {ln} of SokobanPlus data")
120 }
121
122 IoReadErrorKind::InvalidFileExtension { ext } => {
123 write!(
124 f,
125 "file extension \".{ext}\" can't be matched to any known FileFormat"
126 )
127 }
128
129 IoReadErrorKind::UnknownTessellationName { ln, name } => {
130 write!(
131 f,
132 "at line {ln}, tessellation name: \"{name}\" can't be matched to any known Tessellation"
133 )
134 }
135
136 IoReadErrorKind::XMLSyntaxError { ln } => {
137 let msg = match &self.0.msg {
138 Some(v) => {
139 if is_blank(v) {
140 String::new()
141 } else {
142 format!(": {v}")
143 }
144 }
145
146 None => String::new(),
147 };
148 write!(f, "XML syntax error at line {ln} while reading Sokoban data{msg}")
149 }
150
151 IoReadErrorKind::JsonSyntaxError { .. } => {
154 let msg = match &self.0.msg {
155 Some(v) if !is_blank(v) => format!(": {v}"),
156 _ => String::new(),
157 };
158 write!(f, "JSON syntax error while reading Sokoban data{msg}")
159 }
160
161 IoReadErrorKind::IoError => {
162 write!(f, "I/O error while reading Sokoban data")
163 }
164 }
165 }
166}
167
168impl Error for IoReadError {
169 fn source(&self) -> Option<&(dyn Error + 'static)> {
170 match &self.0.source {
171 Some(ref_box_dyn) => Some(ref_box_dyn.as_ref()),
172 None => None,
173 }
174 }
175}
176
177impl From<std::io::Error> for IoReadError {
178 fn from(value: std::io::Error) -> Self {
179 Self(Box::new(IoReadErrorPayload {
180 kind: IoReadErrorKind::IoError,
181 source: Some(Box::new(value)),
182 msg: None,
183 }))
184 }
185}
186
187#[derive(Debug, Clone, PartialEq, Eq, Hash)]
189pub enum IoWriteErrorKind {
190 RleEncodingFailed,
191 IoError,
192 VariantNotAllowedByFileFormat { variant: Tessellation },
193 InvalidFileExtension { ext: String },
194}
195
196#[derive(Debug)]
197struct IoWriteErrorPayload {
198 kind: IoWriteErrorKind,
199 puzzle_idx: usize,
200 source: Option<Box<dyn Error>>,
201}
202
203#[derive(Debug)]
205pub struct IoWriteError(Box<IoWriteErrorPayload>);
206
207impl IoWriteError {
208 pub fn puzzle_idx(&self) -> usize { self.0.puzzle_idx }
209
210 pub(in crate::io) fn new_io_error(source: std::io::Error, puzzle_idx: usize) -> Self {
211 Self(Box::new(IoWriteErrorPayload {
212 kind: IoWriteErrorKind::IoError,
213 puzzle_idx,
214 source: Some(Box::new(source)),
215 }))
216 }
217
218 pub(in crate::io) fn new_rle_encoding_error(source: RleSyntaxError, puzzle_idx: usize) -> Self {
219 Self(Box::new(IoWriteErrorPayload {
220 kind: IoWriteErrorKind::RleEncodingFailed,
221 puzzle_idx,
222 source: Some(Box::new(source)),
223 }))
224 }
225
226 pub(in crate::io) fn new_variant_not_allowed_error(
227 puzzle_idx: usize, variant: Tessellation,
228 ) -> Self {
229 Self(Box::new(IoWriteErrorPayload {
230 kind: IoWriteErrorKind::VariantNotAllowedByFileFormat { variant },
231 puzzle_idx,
232 source: None,
233 }))
234 }
235
236 pub(in crate::io) fn new_invalid_extension_error(ext: &str) -> Self {
237 Self(Box::new(IoWriteErrorPayload {
238 kind: IoWriteErrorKind::InvalidFileExtension { ext: ext.into() },
239 puzzle_idx: 0,
240 source: None,
241 }))
242 }
243}
244
245impl Display for IoWriteError {
246 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
247 match &self.0.kind {
248 IoWriteErrorKind::IoError => {
249 write!(f, "I/O error while writing Sokoban data")
250 }
251
252 IoWriteErrorKind::RleEncodingFailed => {
253 write!(f, "failed to RLE encode Sokoban data before writing it")
254 }
255
256 IoWriteErrorKind::VariantNotAllowedByFileFormat { variant } => {
257 write!(
258 f,
259 "writing \"{variant:?}\" game variant into this file format is not allowed"
260 )
261 }
262
263 IoWriteErrorKind::InvalidFileExtension { ext } => {
264 write!(
265 f,
266 "writing Sokoban data to file with extension \".{ext}\" is not supported"
267 )
268 }
269 }
270 }
271}
272
273impl Error for IoWriteError {
274 fn source(&self) -> Option<&(dyn Error + 'static)> {
275 match &self.0.source {
276 Some(ref_box_dyn) => Some(ref_box_dyn.as_ref()),
277 None => None,
278 }
279 }
280}
281
282impl From<std::io::Error> for IoWriteError {
283 fn from(value: std::io::Error) -> Self { Self::new_io_error(value, 0) }
284}