Skip to main content

sokoban_elements/io/
errors.rs

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/// Variants of [`IoReadError`].
9#[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/// [`std::Error`](std::error::Error) when reading [`FileFormat`](crate::FileFormat).
29#[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    /// Wraps a `serde_json` failure, be it malformed JSON or valid JSON that this crate's types
74    /// rejected.
75    ///
76    /// Distinguishing the two is up to the caller: downcast `source()` to
77    /// `serde_json::Error` and inspect its `classify()`.
78    #[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            // `serde_json` messages already end in "at line N column M", so `ln` is not repeated
152            // here.
153            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/// Variants of [`IoWriteError`].
188#[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/// [`std::Error`](std::error::Error) when writing [`FileFormat`](crate::FileFormat).
204#[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}