modio/types/
id.rs

1//! Type-safe ID type for each resource.
2
3use std::cmp::Ordering;
4use std::fmt::{self, Write};
5use std::hash::{Hash, Hasher};
6use std::marker::PhantomData;
7use std::num::{NonZeroI64, NonZeroU64, ParseIntError, TryFromIntError};
8use std::str::FromStr;
9
10/// ID with a game marker.
11pub type GameId = Id<marker::GameMarker>;
12/// ID with a mod marker.
13pub type ModId = Id<marker::ModMarker>;
14/// ID with a file marker.
15pub type FileId = Id<marker::FileMarker>;
16/// ID with an event marker.
17pub type EventId = Id<marker::EventMarker>;
18/// ID with a comment marker.
19pub type CommentId = Id<marker::CommentMarker>;
20/// ID with a user marker.
21pub type UserId = Id<marker::UserMarker>;
22/// ID with a team member marker.
23pub type MemberId = Id<marker::MemberMarker>;
24/// ID with a resource marker.
25pub type ResourceId = Id<marker::ResourceMarker>;
26
27/// Markers for various resource types.
28pub mod marker {
29    /// Marker for game IDs.
30    #[non_exhaustive]
31    pub struct GameMarker;
32
33    /// Marker for mod IDs.
34    #[non_exhaustive]
35    pub struct ModMarker;
36
37    /// Marker for file IDs.
38    #[non_exhaustive]
39    pub struct FileMarker;
40
41    /// Marker for event IDs.
42    #[non_exhaustive]
43    pub struct EventMarker;
44
45    /// Marker for comment IDs.
46    #[non_exhaustive]
47    pub struct CommentMarker;
48
49    /// Marker for user IDs.
50    #[non_exhaustive]
51    pub struct UserMarker;
52
53    /// Marker for team member IDs.
54    #[non_exhaustive]
55    pub struct MemberMarker;
56
57    /// Marker for resource IDs.
58    #[non_exhaustive]
59    pub struct ResourceMarker;
60}
61
62/// ID of a resource, such as the ID of a [game] or [mod].
63///
64/// [game]: crate::types::games::Game
65/// [mod]: crate::types::mods::Mod
66#[repr(transparent)]
67pub struct Id<T> {
68    phantom: PhantomData<fn(T) -> T>,
69    value: NonZeroU64,
70}
71
72impl<T> Id<T> {
73    const fn from_nonzero(value: NonZeroU64) -> Self {
74        Self {
75            phantom: PhantomData,
76            value,
77        }
78    }
79
80    /// Create a new ID.
81    ///
82    /// # Examples
83    ///
84    /// ```
85    /// use modio::types::id::marker::GameMarker;
86    /// use modio::types::id::Id;
87    ///
88    /// let id: Id<GameMarker> = Id::new(123);
89    ///
90    /// // Using the provided type aliases.
91    /// use modio::types::id::GameId;
92    ///
93    /// let game_id = GameId::new(123);
94    ///
95    /// assert_eq!(id, game_id);
96    /// ```
97    ///
98    /// # Panics
99    ///
100    /// Panics if the value is 0.
101    #[track_caller]
102    pub const fn new(value: u64) -> Self {
103        if let Some(value) = Self::new_checked(value) {
104            value
105        } else {
106            panic!("value is zero")
107        }
108    }
109
110    /// Create a new ID if the given value is not zero.
111    pub const fn new_checked(value: u64) -> Option<Self> {
112        if let Some(value) = NonZeroU64::new(value) {
113            Some(Self::from_nonzero(value))
114        } else {
115            None
116        }
117    }
118
119    pub const fn get(self) -> u64 {
120        self.value.get()
121    }
122
123    pub const fn transform<New>(self) -> Id<New> {
124        Id::new(self.get())
125    }
126}
127
128impl<T> Clone for Id<T> {
129    fn clone(&self) -> Self {
130        *self
131    }
132}
133
134impl<T> Copy for Id<T> {}
135
136impl<T> fmt::Debug for Id<T> {
137    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
138        f.write_str("Id")?;
139
140        let name = std::any::type_name::<T>();
141        if let Some(pos) = name.rfind("::") {
142            if let Some(marker) = name.get(pos + 2..) {
143                f.write_char('<')?;
144                f.write_str(marker)?;
145                f.write_char('>')?;
146            }
147        }
148
149        f.write_char('(')?;
150        fmt::Debug::fmt(&self.value, f)?;
151        f.write_char(')')
152    }
153}
154
155impl<T> fmt::Display for Id<T> {
156    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
157        fmt::Display::fmt(&self.value, f)
158    }
159}
160
161impl<T> PartialEq for Id<T> {
162    fn eq(&self, other: &Self) -> bool {
163        self.value == other.value
164    }
165}
166
167impl<T> Eq for Id<T> {}
168
169impl<T> PartialEq<u64> for Id<T> {
170    fn eq(&self, other: &u64) -> bool {
171        self.value.get() == *other
172    }
173}
174
175impl<T> PartialEq<Id<T>> for u64 {
176    fn eq(&self, other: &Id<T>) -> bool {
177        other.value.get() == *self
178    }
179}
180
181impl<T> PartialOrd for Id<T> {
182    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
183        Some(self.cmp(other))
184    }
185}
186
187impl<T> Ord for Id<T> {
188    fn cmp(&self, other: &Self) -> Ordering {
189        self.value.cmp(&other.value)
190    }
191}
192
193impl<T> Hash for Id<T> {
194    fn hash<H: Hasher>(&self, state: &mut H) {
195        state.write_u64(self.value.get());
196    }
197}
198
199impl<T> From<Id<T>> for u64 {
200    fn from(value: Id<T>) -> Self {
201        value.get()
202    }
203}
204
205impl<T> From<NonZeroU64> for Id<T> {
206    fn from(value: NonZeroU64) -> Self {
207        Self::from_nonzero(value)
208    }
209}
210
211impl<T> TryFrom<i64> for Id<T> {
212    type Error = TryFromIntError;
213
214    fn try_from(value: i64) -> Result<Self, Self::Error> {
215        let value = NonZeroI64::try_from(value)?;
216        let value = NonZeroU64::try_from(value)?;
217        Ok(Self::from_nonzero(value))
218    }
219}
220
221impl<T> TryFrom<u64> for Id<T> {
222    type Error = TryFromIntError;
223
224    fn try_from(value: u64) -> Result<Self, Self::Error> {
225        let value = NonZeroU64::try_from(value)?;
226        Ok(Id::from_nonzero(value))
227    }
228}
229
230impl<T> FromStr for Id<T> {
231    type Err = ParseIntError;
232
233    fn from_str(s: &str) -> Result<Self, Self::Err> {
234        NonZeroU64::from_str(s).map(Self::from_nonzero)
235    }
236}
237
238use serde::de::{Deserialize, Deserializer};
239use serde::ser::{Serialize, Serializer};
240
241impl<'de, T> Deserialize<'de> for Id<T> {
242    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
243        NonZeroU64::deserialize(deserializer).map(Self::from_nonzero)
244    }
245}
246
247impl<T> Serialize for Id<T> {
248    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
249        self.value.serialize(serializer)
250    }
251}
252
253#[cfg(test)]
254mod tests {
255    use std::str::FromStr;
256
257    use super::marker::*;
258    use super::Id;
259
260    #[test]
261    fn from_str() {
262        assert_eq!(
263            Id::<GameMarker>::new(123),
264            Id::<GameMarker>::from_str("123").unwrap()
265        );
266        assert!(Id::<GameMarker>::from_str("0").is_err());
267        assert!(Id::<GameMarker>::from_str("123a").is_err());
268    }
269
270    #[test]
271    fn try_from() {
272        assert!(Id::<GameMarker>::try_from(-123i64).is_err());
273        assert!(Id::<GameMarker>::try_from(0i64).is_err());
274        assert_eq!(123u64, Id::<GameMarker>::try_from(123i64).unwrap());
275
276        assert!(Id::<GameMarker>::try_from(0u64).is_err());
277        assert_eq!(123u64, Id::<GameMarker>::try_from(123u64).unwrap());
278    }
279}