Skip to main content

sokoban_elements/sokoban_plus/
plus_id.rs

1use std::fmt::{Display, Formatter};
2use std::num::NonZeroU16;
3
4use crate::{PieceIdError, PieceIdErrorKind};
5
6/// `Sokoban+` identifier for box and goal.
7///
8/// See [`BoxID`](crate::BoxID), [`GoalID`](crate::GoalID) and
9/// [`SokobanPlus`](crate::SokobanPlus) for more details.
10///
11/// # Serde
12///
13/// With the `serde` feature, this serializes as a bare integer and deserializes through
14/// [`TryFrom`](std::convert::TryFrom), so `0` is rejected. See [`BoxID`](crate::BoxID).
15#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
16#[cfg_attr(
17    feature = "serde",
18    derive(serde::Serialize, serde::Deserialize),
19    serde(try_from = "u16", into = "u16")
20)]
21pub struct PlusID(NonZeroU16);
22
23impl PlusID {
24    pub fn new(id: u16) -> Result<Self, PieceIdError> { id.try_into() }
25}
26
27impl Display for PlusID {
28    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.0) }
29}
30
31// Convert into integer types
32
33impl From<PlusID> for u16 {
34    fn from(value: PlusID) -> Self { value.0.get() }
35}
36
37impl From<PlusID> for usize {
38    fn from(value: PlusID) -> Self { value.0.get().into() }
39}
40
41macro_rules! implement_plus_id_construct_from_int {
42    ( u8 ) => {
43        impl TryFrom<u8> for PlusID {
44            type Error = PieceIdError;
45            fn try_from(value: u8) -> Result<Self, Self::Error> {
46                Ok(Self(NonZeroU16::try_from(u16::from(value)).map_err(|_| {
47                    PieceIdError {
48                        kind:  PieceIdErrorKind::InvalidPlusID,
49                        value: value.to_string(),
50                    }
51                })?))
52            }
53        }
54    };
55
56    ( u16) => {
57        impl TryFrom<u16> for PlusID {
58            type Error = PieceIdError;
59            fn try_from(value: u16) -> Result<Self, Self::Error> {
60                Ok(Self(NonZeroU16::try_from(value).map_err(|_| {
61                    PieceIdError {
62                        kind:  PieceIdErrorKind::InvalidPlusID,
63                        value: value.to_string(),
64                    }
65                })?))
66            }
67        }
68    };
69
70    ( $IntTy:ty ) => {
71        impl TryFrom<$IntTy> for PlusID {
72            type Error = PieceIdError;
73            fn try_from(value: $IntTy) -> Result<Self, Self::Error> {
74                Ok(Self(
75                    NonZeroU16::try_from(u16::try_from(value).map_err(|_| {
76                        PieceIdError {
77                            kind:  PieceIdErrorKind::InvalidPlusID,
78                            value: value.to_string(),
79                        }
80                    })?)
81                    .map_err(|_| {
82                        PieceIdError {
83                            kind:  PieceIdErrorKind::InvalidPlusID,
84                            value: value.to_string(),
85                        }
86                    })?,
87                ))
88            }
89        }
90    };
91}
92
93implement_plus_id_construct_from_int!(u8);
94implement_plus_id_construct_from_int!(u16);
95implement_plus_id_construct_from_int!(u32);
96implement_plus_id_construct_from_int!(u64);
97implement_plus_id_construct_from_int!(usize);
98implement_plus_id_construct_from_int!(i8);
99implement_plus_id_construct_from_int!(i16);
100implement_plus_id_construct_from_int!(i32);
101implement_plus_id_construct_from_int!(i64);
102implement_plus_id_construct_from_int!(isize);
103
104// Compare to integer types, ie. `BoxID == u32`
105//
106// This should not be implemented: `u32 == BoxID`.
107//
108// https://doc.rust-lang.org/std/cmp/trait.PartialEq.html#cross-crate-considerations
109
110impl PartialEq<u8> for PlusID {
111    fn eq(&self, other: &u8) -> bool { self.0.get() == u16::from(*other) }
112}
113
114impl PartialEq<u16> for PlusID {
115    fn eq(&self, other: &u16) -> bool { self.0.get() == *other }
116}
117
118impl PartialEq<u32> for PlusID {
119    fn eq(&self, other: &u32) -> bool { u32::from(self.0.get()) == *other }
120}
121
122impl PartialEq<u64> for PlusID {
123    fn eq(&self, other: &u64) -> bool { u64::from(self.0.get()) == *other }
124}
125
126impl PartialEq<usize> for PlusID {
127    fn eq(&self, other: &usize) -> bool { usize::from(self.0.get()) == *other }
128}
129
130impl PartialEq<i8> for PlusID {
131    fn eq(&self, other: &i8) -> bool { i32::from(self.0.get()) == i32::from(*other) }
132}
133
134impl PartialEq<i16> for PlusID {
135    fn eq(&self, other: &i16) -> bool { i32::from(self.0.get()) == i32::from(*other) }
136}
137
138impl PartialEq<i32> for PlusID {
139    fn eq(&self, other: &i32) -> bool { i32::from(self.0.get()) == *other }
140}
141
142impl PartialEq<i64> for PlusID {
143    fn eq(&self, other: &i64) -> bool { i64::from(self.0.get()) == *other }
144}
145
146impl PartialEq<isize> for PlusID {
147    fn eq(&self, other: &isize) -> bool {
148        match isize::try_from(self.0.get()) {
149            Ok(self_isize) => self_isize == *other,
150            Err(_) => false,
151        }
152    }
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158
159    #[test]
160    fn test_plus_id_can_be_constructed_from_integers() -> Result<(), PieceIdError> {
161        assert!(PlusID::new(42)? == 42_u8);
162
163        assert!(PlusID::try_from(42_u8)? == 42_u8);
164        assert!(PlusID::try_from(42_u16)? == 42_u8);
165        assert!(PlusID::try_from(42_u32)? == 42_u8);
166        assert!(PlusID::try_from(42_u64)? == 42_u8);
167        assert!(PlusID::try_from(42_usize)? == 42_u8);
168
169        assert!(PlusID::try_from(42_i8)? == 42_u8);
170        assert!(PlusID::try_from(42_i16)? == 42_u8);
171        assert!(PlusID::try_from(42_i32)? == 42_u8);
172        assert!(PlusID::try_from(42_i64)? == 42_u8);
173        assert!(PlusID::try_from(42_isize)? == 42_u8);
174
175        assert!(PlusID::try_from(-1_i32).is_err());
176
177        Ok(())
178    }
179
180    #[test]
181    fn test_plus_id_is_value_constrained() {
182        assert!(PlusID::new(0).is_err());
183        assert!(PlusID::try_from(0_u8).is_err());
184        assert!(PlusID::try_from(usize::from(u16::MAX) + 1).is_err());
185    }
186
187    #[test]
188    fn test_plus_id_is_comparable_to_integers() -> Result<(), PieceIdError> {
189        assert!(PlusID::try_from(1_u8)? == 1_u8);
190        assert!(PlusID::try_from(1_u8)? == 1_u16);
191        assert!(PlusID::try_from(1_u8)? == 1_u32);
192        assert!(PlusID::try_from(1_u8)? == 1_u64);
193        assert!(PlusID::try_from(1_u8)? == 1_usize);
194        assert!(PlusID::try_from(1_u8)? == 1_i8);
195        assert!(PlusID::try_from(1_u8)? == 1_i16);
196        assert!(PlusID::try_from(1_u8)? == 1_i32);
197        assert!(PlusID::try_from(1_u8)? == 1_i64);
198        assert!(PlusID::try_from(1_u8)? == 1_isize);
199
200        assert!(PlusID::try_from(1_u8)? != 2_u8);
201        assert!(PlusID::try_from(1_u8)? != 2_u16);
202        assert!(PlusID::try_from(1_u8)? != 2_u32);
203        assert!(PlusID::try_from(1_u8)? != 2_u64);
204        assert!(PlusID::try_from(1_u8)? != 2_usize);
205        assert!(PlusID::try_from(1_u8)? != 2_i8);
206        assert!(PlusID::try_from(1_u8)? != 2_i16);
207        assert!(PlusID::try_from(1_u8)? != 2_i32);
208        assert!(PlusID::try_from(1_u8)? != 2_i64);
209        assert!(PlusID::try_from(1_u8)? != 2_isize);
210
211        Ok(())
212    }
213
214    #[cfg(feature = "serde")]
215    mod test_plus_id_serde {
216        use std::error::Error;
217
218        use super::*;
219
220        #[test]
221        fn it_serializes_as_a_bare_integer() -> Result<(), Box<dyn Error>> {
222            assert_eq!(serde_json::to_string(&PlusID::new(42)?)?, "42");
223            assert_eq!(serde_json::from_str::<PlusID>("42")?, PlusID::new(42)?);
224
225            Ok(())
226        }
227
228        #[test]
229        fn it_rejects_ids_out_of_range() {
230            assert!(serde_json::from_str::<PlusID>("0").is_err());
231            assert!(serde_json::from_str::<PlusID>("65536").is_err());
232            assert!(serde_json::from_str::<PlusID>("-1").is_err());
233        }
234    }
235}