sokoban-elements 0.2.1

Parsing and writing Sokoban, Hexoban, Trioban and Octoban file formats.
Documentation
use std::error::Error;
use std::fmt::{Display, Formatter};

use crate::rle::RleSyntaxError;
use crate::utilities::is_blank;
use crate::{BoardSyntaxError, SavedGameSyntaxError, SokobanPlusSyntaxError, Tessellation};

/// Variants of [`IoReadError`].
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum IoReadErrorKind {
    BoardSyntaxError { ln: usize },
    SavedGameSyntaxError { ln: usize },
    SokobanPlusSyntaxError { ln: usize },
    InvalidFileExtension { ext: String },
    UnknownTessellationName { ln: usize, name: String },
    XMLSyntaxError { ln: usize },
    JsonSyntaxError { ln: usize },
    IoError,
}

#[derive(Debug)]
struct IoReadErrorPayload {
    kind:   IoReadErrorKind,
    source: Option<Box<dyn Error>>,
    msg:    Option<String>,
}

/// [`std::Error`](std::error::Error) when reading [`FileFormat`](crate::FileFormat).
#[derive(Debug)]
pub struct IoReadError(Box<IoReadErrorPayload>);

impl IoReadError {
    pub fn kind(&self) -> &IoReadErrorKind { &self.0.kind }

    pub(in crate::io) fn new_xml_syntax_error(
        ln: usize, source: Option<Box<dyn Error>>, msg: String,
    ) -> Self {
        Self(Box::new(IoReadErrorPayload {
            kind: IoReadErrorKind::XMLSyntaxError { ln },
            source,
            msg: Some(msg),
        }))
    }

    pub(in crate::io) fn new_puzzle_syntax_error(ln: usize, source: BoardSyntaxError) -> Self {
        Self(Box::new(IoReadErrorPayload {
            kind:   IoReadErrorKind::BoardSyntaxError { ln },
            source: Some(Box::new(source)),
            msg:    None,
        }))
    }

    pub(in crate::io) fn new_saved_game_syntax_error(
        ln: usize, source: SavedGameSyntaxError,
    ) -> Self {
        Self(Box::new(IoReadErrorPayload {
            kind:   IoReadErrorKind::SavedGameSyntaxError { ln },
            source: Some(Box::new(source)),
            msg:    None,
        }))
    }

    pub(in crate::io) fn new_sokoban_plus_syntax_error(
        ln: usize, source: SokobanPlusSyntaxError,
    ) -> Self {
        Self(Box::new(IoReadErrorPayload {
            kind:   IoReadErrorKind::SokobanPlusSyntaxError { ln },
            source: Some(Box::new(source)),
            msg:    None,
        }))
    }

    /// Wraps a `serde_json` failure, be it malformed JSON or valid JSON that this crate's types
    /// rejected.
    ///
    /// Distinguishing the two is up to the caller: downcast `source()` to
    /// `serde_json::Error` and inspect its `classify()`.
    #[cfg(feature = "json")]
    pub(in crate::io) fn new_json_syntax_error(source: serde_json::Error) -> Self {
        Self(Box::new(IoReadErrorPayload {
            kind:   IoReadErrorKind::JsonSyntaxError { ln: source.line() },
            msg:    Some(source.to_string()),
            source: Some(Box::new(source)),
        }))
    }

    pub(in crate::io) fn new_invalid_extension_error(ext: &str) -> Self {
        Self(Box::new(IoReadErrorPayload {
            kind:   IoReadErrorKind::InvalidFileExtension { ext: ext.into() },
            source: None,
            msg:    None,
        }))
    }

    pub(in crate::io) fn new_unknown_tessellation_error(ln: usize, name: &str) -> Self {
        Self(Box::new(IoReadErrorPayload {
            kind:   IoReadErrorKind::UnknownTessellationName {
                ln,
                name: name.into(),
            },
            source: None,
            msg:    None,
        }))
    }
}

impl Display for IoReadError {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        match &self.0.kind {
            IoReadErrorKind::BoardSyntaxError { ln } => {
                write!(f, "syntax error at line {ln} of Puzzle data")
            }

            IoReadErrorKind::SavedGameSyntaxError { ln } => {
                write!(f, "syntax error at line {ln} of SavedGame data")
            }

            IoReadErrorKind::SokobanPlusSyntaxError { ln } => {
                write!(f, "syntax error at line {ln} of SokobanPlus data")
            }

            IoReadErrorKind::InvalidFileExtension { ext } => {
                write!(
                    f,
                    "file extension \".{ext}\" can't be matched to any known FileFormat"
                )
            }

            IoReadErrorKind::UnknownTessellationName { ln, name } => {
                write!(
                    f,
                    "at line {ln}, tessellation name: \"{name}\" can't be matched to any known Tessellation"
                )
            }

            IoReadErrorKind::XMLSyntaxError { ln } => {
                let msg = match &self.0.msg {
                    Some(v) => {
                        if is_blank(v) {
                            String::new()
                        } else {
                            format!(": {v}")
                        }
                    }

                    None => String::new(),
                };
                write!(f, "XML syntax error at line {ln} while reading Sokoban data{msg}")
            }

            // `serde_json` messages already end in "at line N column M", so `ln` is not repeated
            // here.
            IoReadErrorKind::JsonSyntaxError { .. } => {
                let msg = match &self.0.msg {
                    Some(v) if !is_blank(v) => format!(": {v}"),
                    _ => String::new(),
                };
                write!(f, "JSON syntax error while reading Sokoban data{msg}")
            }

            IoReadErrorKind::IoError => {
                write!(f, "I/O error while reading Sokoban data")
            }
        }
    }
}

impl Error for IoReadError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match &self.0.source {
            Some(ref_box_dyn) => Some(ref_box_dyn.as_ref()),
            None => None,
        }
    }
}

impl From<std::io::Error> for IoReadError {
    fn from(value: std::io::Error) -> Self {
        Self(Box::new(IoReadErrorPayload {
            kind:   IoReadErrorKind::IoError,
            source: Some(Box::new(value)),
            msg:    None,
        }))
    }
}

/// Variants of [`IoWriteError`].
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum IoWriteErrorKind {
    RleEncodingFailed,
    IoError,
    VariantNotAllowedByFileFormat { variant: Tessellation },
    InvalidFileExtension { ext: String },
}

#[derive(Debug)]
struct IoWriteErrorPayload {
    kind:       IoWriteErrorKind,
    puzzle_idx: usize,
    source:     Option<Box<dyn Error>>,
}

/// [`std::Error`](std::error::Error) when writing [`FileFormat`](crate::FileFormat).
#[derive(Debug)]
pub struct IoWriteError(Box<IoWriteErrorPayload>);

impl IoWriteError {
    pub fn puzzle_idx(&self) -> usize { self.0.puzzle_idx }

    pub(in crate::io) fn new_io_error(source: std::io::Error, puzzle_idx: usize) -> Self {
        Self(Box::new(IoWriteErrorPayload {
            kind: IoWriteErrorKind::IoError,
            puzzle_idx,
            source: Some(Box::new(source)),
        }))
    }

    pub(in crate::io) fn new_rle_encoding_error(source: RleSyntaxError, puzzle_idx: usize) -> Self {
        Self(Box::new(IoWriteErrorPayload {
            kind: IoWriteErrorKind::RleEncodingFailed,
            puzzle_idx,
            source: Some(Box::new(source)),
        }))
    }

    pub(in crate::io) fn new_variant_not_allowed_error(
        puzzle_idx: usize, variant: Tessellation,
    ) -> Self {
        Self(Box::new(IoWriteErrorPayload {
            kind: IoWriteErrorKind::VariantNotAllowedByFileFormat { variant },
            puzzle_idx,
            source: None,
        }))
    }

    pub(in crate::io) fn new_invalid_extension_error(ext: &str) -> Self {
        Self(Box::new(IoWriteErrorPayload {
            kind:       IoWriteErrorKind::InvalidFileExtension { ext: ext.into() },
            puzzle_idx: 0,
            source:     None,
        }))
    }
}

impl Display for IoWriteError {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        match &self.0.kind {
            IoWriteErrorKind::IoError => {
                write!(f, "I/O error while writing Sokoban data")
            }

            IoWriteErrorKind::RleEncodingFailed => {
                write!(f, "failed to RLE encode Sokoban data before writing it")
            }

            IoWriteErrorKind::VariantNotAllowedByFileFormat { variant } => {
                write!(
                    f,
                    "writing \"{variant:?}\" game variant into this file format is not allowed"
                )
            }

            IoWriteErrorKind::InvalidFileExtension { ext } => {
                write!(
                    f,
                    "writing Sokoban data to file with extension \".{ext}\" is not supported"
                )
            }
        }
    }
}

impl Error for IoWriteError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match &self.0.source {
            Some(ref_box_dyn) => Some(ref_box_dyn.as_ref()),
            None => None,
        }
    }
}

impl From<std::io::Error> for IoWriteError {
    fn from(value: std::io::Error) -> Self { Self::new_io_error(value, 0) }
}