sokoban-elements 0.2.0

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

use crate::{BoxID, GoalID, PusherID};

/// Variants of [`BoardSyntaxError`].
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum BoardSyntaxErrorKind {
    /// Failed to expand run length encoded game board string
    RleDecodingFailed,

    /// Couldn't convert `input` found on row `y` and column `x` into [`TileKind`](crate::TileKind).
    IllegalCharacter {
        input: char,
        x:     usize,
        y:     usize,
    },

    /// Layout of parsed [`Hexoban`](crate::Tessellation::Hexoban) game board doesn't match any of
    /// supported layout schemes.
    InvalidHexobanLayout { x: usize, y: usize },

    /// When creating game board from [`TileKind`](crate::TileKind) slice, length of provided slice
    /// doesn't match declared size of final game board.
    InputSizeNotMatchedByDeclaredSize {
        input:           usize,
        declared_width:  usize,
        declared_height: usize,
    },

    /// Game board contains more boxes than [`BoxID`](crate::BoxID) can represent
    /// (`BoxID::MAX + 1`).
    TooManyBoxes,

    /// Game board contains more goals than [`GoalID`](crate::GoalID) can represent
    /// (`GoalID::MAX + 1`).
    TooManyGoals,

    /// Game board contains more pushers than [`PusherID`](crate::PusherID) can represent
    /// (`PusherID::MAX + 1`).
    TooManyPushers,
}

#[derive(Debug)]
struct ErrorPayload {
    kind:   BoardSyntaxErrorKind,
    source: Option<Box<dyn Error>>,
}

/// [`std::Error`](std::error::Error) when parsing [`GameBoard`](crate::GameBoard).
#[derive(Debug)]
pub struct BoardSyntaxError(Box<ErrorPayload>);

impl BoardSyntaxError {
    pub fn kind(&self) -> &BoardSyntaxErrorKind { &self.0.kind }

    pub fn new(kind: BoardSyntaxErrorKind) -> Self {
        Self(Box::new(ErrorPayload { kind, source: None }))
    }

    pub fn new_with_source<E>(kind: BoardSyntaxErrorKind, source: Option<E>) -> Self
    where
        E: Into<Box<dyn Error + Send + Sync>>,
    {
        Self(Box::new(ErrorPayload {
            kind,
            source: match source {
                Some(e) => Some(e.into()),
                None => None,
            },
        }))
    }
}

impl Display for BoardSyntaxError {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        match &self.0.kind {
            BoardSyntaxErrorKind::RleDecodingFailed => {
                write!(f, "failed to expand run length encoded game board string")
            }

            BoardSyntaxErrorKind::IllegalCharacter { input: value, x, y } => {
                write!(
                    f,
                    "parsing game board encountered invalid character '{value}' in \
                    row {y} at col {x}"
                )
            }

            BoardSyntaxErrorKind::InvalidHexobanLayout { x, y } => {
                write!(
                    f,
                    "invalid Hexoban textual board layout. Character at ({x}, {y}) is not \
                    consistent with the rest of the board. Hexoban boards represent each tile \
                    with two characters, either space+tile or tile+space. This encoding scheme \
                    must be used for the whole board consistently."
                )
            }

            BoardSyntaxErrorKind::InputSizeNotMatchedByDeclaredSize {
                input,
                declared_width,
                declared_height,
            } => {
                write!(
                    f,
                    "length of input 1D sequence of tiles ({input}) doesn't match declared 2D \
                    board W x H ({declared_width} x {declared_height})"
                )
            }

            BoardSyntaxErrorKind::TooManyBoxes => {
                write!(
                    f,
                    "game board contains more boxes than supported maximum of {}",
                    u32::from(BoxID::MAX) + 1
                )
            }

            BoardSyntaxErrorKind::TooManyGoals => {
                write!(
                    f,
                    "game board contains more goals than supported maximum of {}",
                    u32::from(GoalID::MAX) + 1
                )
            }

            BoardSyntaxErrorKind::TooManyPushers => {
                write!(
                    f,
                    "game board contains more pushers than supported maximum of {}",
                    u32::from(PusherID::MAX) + 1
                )
            }
        }
    }
}

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