sokoban-elements 0.2.1

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

use crate::{PieceIdError, PieceIdErrorKind};

/// `Sokoban+` identifier for box and goal.
///
/// See [`BoxID`](crate::BoxID), [`GoalID`](crate::GoalID) and
/// [`SokobanPlus`](crate::SokobanPlus) for more details.
///
/// # Serde
///
/// With the `serde` feature, this serializes as a bare integer and deserializes through
/// [`TryFrom`](std::convert::TryFrom), so `0` is rejected. See [`BoxID`](crate::BoxID).
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(
    feature = "serde",
    derive(serde::Serialize, serde::Deserialize),
    serde(try_from = "u16", into = "u16")
)]
pub struct PlusID(NonZeroU16);

impl PlusID {
    pub fn new(id: u16) -> Result<Self, PieceIdError> { id.try_into() }
}

impl Display for PlusID {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.0) }
}

// Convert into integer types

impl From<PlusID> for u16 {
    fn from(value: PlusID) -> Self { value.0.get() }
}

impl From<PlusID> for usize {
    fn from(value: PlusID) -> Self { value.0.get().into() }
}

macro_rules! implement_plus_id_construct_from_int {
    ( u8 ) => {
        impl TryFrom<u8> for PlusID {
            type Error = PieceIdError;
            fn try_from(value: u8) -> Result<Self, Self::Error> {
                Ok(Self(NonZeroU16::try_from(u16::from(value)).map_err(|_| {
                    PieceIdError {
                        kind:  PieceIdErrorKind::InvalidPlusID,
                        value: value.to_string(),
                    }
                })?))
            }
        }
    };

    ( u16) => {
        impl TryFrom<u16> for PlusID {
            type Error = PieceIdError;
            fn try_from(value: u16) -> Result<Self, Self::Error> {
                Ok(Self(NonZeroU16::try_from(value).map_err(|_| {
                    PieceIdError {
                        kind:  PieceIdErrorKind::InvalidPlusID,
                        value: value.to_string(),
                    }
                })?))
            }
        }
    };

    ( $IntTy:ty ) => {
        impl TryFrom<$IntTy> for PlusID {
            type Error = PieceIdError;
            fn try_from(value: $IntTy) -> Result<Self, Self::Error> {
                Ok(Self(
                    NonZeroU16::try_from(u16::try_from(value).map_err(|_| {
                        PieceIdError {
                            kind:  PieceIdErrorKind::InvalidPlusID,
                            value: value.to_string(),
                        }
                    })?)
                    .map_err(|_| {
                        PieceIdError {
                            kind:  PieceIdErrorKind::InvalidPlusID,
                            value: value.to_string(),
                        }
                    })?,
                ))
            }
        }
    };
}

implement_plus_id_construct_from_int!(u8);
implement_plus_id_construct_from_int!(u16);
implement_plus_id_construct_from_int!(u32);
implement_plus_id_construct_from_int!(u64);
implement_plus_id_construct_from_int!(usize);
implement_plus_id_construct_from_int!(i8);
implement_plus_id_construct_from_int!(i16);
implement_plus_id_construct_from_int!(i32);
implement_plus_id_construct_from_int!(i64);
implement_plus_id_construct_from_int!(isize);

// Compare to integer types, ie. `BoxID == u32`
//
// This should not be implemented: `u32 == BoxID`.
//
// https://doc.rust-lang.org/std/cmp/trait.PartialEq.html#cross-crate-considerations

impl PartialEq<u8> for PlusID {
    fn eq(&self, other: &u8) -> bool { self.0.get() == u16::from(*other) }
}

impl PartialEq<u16> for PlusID {
    fn eq(&self, other: &u16) -> bool { self.0.get() == *other }
}

impl PartialEq<u32> for PlusID {
    fn eq(&self, other: &u32) -> bool { u32::from(self.0.get()) == *other }
}

impl PartialEq<u64> for PlusID {
    fn eq(&self, other: &u64) -> bool { u64::from(self.0.get()) == *other }
}

impl PartialEq<usize> for PlusID {
    fn eq(&self, other: &usize) -> bool { usize::from(self.0.get()) == *other }
}

impl PartialEq<i8> for PlusID {
    fn eq(&self, other: &i8) -> bool { i32::from(self.0.get()) == i32::from(*other) }
}

impl PartialEq<i16> for PlusID {
    fn eq(&self, other: &i16) -> bool { i32::from(self.0.get()) == i32::from(*other) }
}

impl PartialEq<i32> for PlusID {
    fn eq(&self, other: &i32) -> bool { i32::from(self.0.get()) == *other }
}

impl PartialEq<i64> for PlusID {
    fn eq(&self, other: &i64) -> bool { i64::from(self.0.get()) == *other }
}

impl PartialEq<isize> for PlusID {
    fn eq(&self, other: &isize) -> bool {
        match isize::try_from(self.0.get()) {
            Ok(self_isize) => self_isize == *other,
            Err(_) => false,
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_plus_id_can_be_constructed_from_integers() -> Result<(), PieceIdError> {
        assert!(PlusID::new(42)? == 42_u8);

        assert!(PlusID::try_from(42_u8)? == 42_u8);
        assert!(PlusID::try_from(42_u16)? == 42_u8);
        assert!(PlusID::try_from(42_u32)? == 42_u8);
        assert!(PlusID::try_from(42_u64)? == 42_u8);
        assert!(PlusID::try_from(42_usize)? == 42_u8);

        assert!(PlusID::try_from(42_i8)? == 42_u8);
        assert!(PlusID::try_from(42_i16)? == 42_u8);
        assert!(PlusID::try_from(42_i32)? == 42_u8);
        assert!(PlusID::try_from(42_i64)? == 42_u8);
        assert!(PlusID::try_from(42_isize)? == 42_u8);

        assert!(PlusID::try_from(-1_i32).is_err());

        Ok(())
    }

    #[test]
    fn test_plus_id_is_value_constrained() {
        assert!(PlusID::new(0).is_err());
        assert!(PlusID::try_from(0_u8).is_err());
        assert!(PlusID::try_from(usize::from(u16::MAX) + 1).is_err());
    }

    #[test]
    fn test_plus_id_is_comparable_to_integers() -> Result<(), PieceIdError> {
        assert!(PlusID::try_from(1_u8)? == 1_u8);
        assert!(PlusID::try_from(1_u8)? == 1_u16);
        assert!(PlusID::try_from(1_u8)? == 1_u32);
        assert!(PlusID::try_from(1_u8)? == 1_u64);
        assert!(PlusID::try_from(1_u8)? == 1_usize);
        assert!(PlusID::try_from(1_u8)? == 1_i8);
        assert!(PlusID::try_from(1_u8)? == 1_i16);
        assert!(PlusID::try_from(1_u8)? == 1_i32);
        assert!(PlusID::try_from(1_u8)? == 1_i64);
        assert!(PlusID::try_from(1_u8)? == 1_isize);

        assert!(PlusID::try_from(1_u8)? != 2_u8);
        assert!(PlusID::try_from(1_u8)? != 2_u16);
        assert!(PlusID::try_from(1_u8)? != 2_u32);
        assert!(PlusID::try_from(1_u8)? != 2_u64);
        assert!(PlusID::try_from(1_u8)? != 2_usize);
        assert!(PlusID::try_from(1_u8)? != 2_i8);
        assert!(PlusID::try_from(1_u8)? != 2_i16);
        assert!(PlusID::try_from(1_u8)? != 2_i32);
        assert!(PlusID::try_from(1_u8)? != 2_i64);
        assert!(PlusID::try_from(1_u8)? != 2_isize);

        Ok(())
    }

    #[cfg(feature = "serde")]
    mod test_plus_id_serde {
        use std::error::Error;

        use super::*;

        #[test]
        fn it_serializes_as_a_bare_integer() -> Result<(), Box<dyn Error>> {
            assert_eq!(serde_json::to_string(&PlusID::new(42)?)?, "42");
            assert_eq!(serde_json::from_str::<PlusID>("42")?, PlusID::new(42)?);

            Ok(())
        }

        #[test]
        fn it_rejects_ids_out_of_range() {
            assert!(serde_json::from_str::<PlusID>("0").is_err());
            assert!(serde_json::from_str::<PlusID>("65536").is_err());
            assert!(serde_json::from_str::<PlusID>("-1").is_err());
        }
    }
}