u64_id/
lib.rs

1use core::fmt;
2
3/// An ID for simply applications, implemented as a wrapper around [`u64`]s.
4///
5/// ## Valid Range
6///
7/// The valid range of ids is given by [`U64Id::VALID_RANGE`]. Essentially,
8/// this range is `..(u64::MAX - 128)`.
9///
10/// ## Collisions
11///
12/// Ids are made entirely randomly, so a collision is possible, but unbelievably
13/// unlikely, since just a simply u64 has enough bitspace in it to prevent a collision.
14#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Eq, Ord, Hash)]
15#[repr(transparent)]
16pub struct U64Id(pub u64);
17
18impl U64Id {
19    /// Returns the forbidden `null` id. Applications can use this as an Id
20    /// which will never be made.
21    pub const NULL: U64Id = U64Id(u64::MAX);
22
23    /// Returns the valid range for [`U64Id`]s. Nothing prevents an Id from
24    /// being generated outside of this range, so it should not be relied upon
25    /// for safety.
26    ///
27    /// The amount of space at the top was randomly determined to be useful.
28    pub const VALID_RANGE: core::ops::Range<u64> = 0..(u64::MAX - 128);
29
30    /// Creates a new, random AssetId, seeded cheaply from thread_rng.
31    ///
32    /// To avoid calling this internal function repeatedly, consider making
33    /// a `U64Id` directly.
34    #[cfg(feature = "rand")]
35    pub fn new() -> Self {
36        use rand::Rng;
37
38        Self(rand::rng().random_range(Self::VALID_RANGE))
39    }
40
41    /// Checks if the asset is the `null` ID.
42    pub const fn is_null(self) -> bool {
43        self.0 == u64::MAX
44    }
45}
46
47#[cfg(feature = "rand")]
48impl Default for U64Id {
49    fn default() -> Self {
50        Self::new()
51    }
52}
53
54impl fmt::Display for U64Id {
55    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56        write!(f, "{:x}", self.0)
57    }
58}
59impl fmt::LowerHex for U64Id {
60    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61        let val = self.0;
62
63        fmt::LowerHex::fmt(&val, f)
64    }
65}
66impl fmt::UpperHex for U64Id {
67    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68        let val = self.0;
69
70        fmt::UpperHex::fmt(&val, f)
71    }
72}
73
74impl std::str::FromStr for U64Id {
75    type Err = std::num::ParseIntError;
76
77    fn from_str(s: &str) -> Result<Self, Self::Err> {
78        u64::from_str_radix(s, 16).map(Self)
79    }
80}
81
82#[cfg(feature = "serde")]
83impl serde::Serialize for U64Id {
84    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
85    where
86        S: serde::Serializer,
87    {
88        // we serialize the number as a string with lowercase hex formatting by default
89        serializer.serialize_str(&format!("{:x}", self.0))
90    }
91}
92
93#[cfg(feature = "serde")]
94impl<'de> serde::Deserialize<'de> for U64Id {
95    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
96    where
97        D: serde::Deserializer<'de>,
98    {
99        struct AssetIdVisitor;
100        impl<'de> serde::de::Visitor<'de> for AssetIdVisitor {
101            type Value = u64;
102
103            fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
104                formatter.write_str("a hex-encoded integer between 0 and 2^64 - 1")
105            }
106
107            fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
108            where
109                E: serde::de::Error,
110            {
111                u64::from_str_radix(v, 16).map_err(|_| {
112                    serde::de::Error::invalid_value(serde::de::Unexpected::Str(v), &self)
113                })
114            }
115
116            // we can also deserialize a u64! This can be nice. Yes. It is nice.
117            fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
118            where
119                E: serde::de::Error,
120            {
121                u64::from_str_radix(&v.to_string(), 16).map_err(|_| {
122                    serde::de::Error::invalid_value(serde::de::Unexpected::Unsigned(v), &self)
123                })
124            }
125
126            // we can also deserialize an i64! This can be nice. Yes. It is nice.
127            fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
128            where
129                E: serde::de::Error,
130            {
131                u64::from_str_radix(&v.to_string(), 16).map_err(|_| {
132                    serde::de::Error::invalid_value(serde::de::Unexpected::Signed(v), &self)
133                })
134            }
135        }
136
137        if deserializer.is_human_readable() {
138            deserializer.deserialize_any(AssetIdVisitor).map(U64Id)
139        } else {
140            deserializer.deserialize_str(AssetIdVisitor).map(U64Id)
141        }
142    }
143}
144
145#[cfg(test)]
146mod tests {
147    use super::*;
148
149    #[test]
150    fn null_tests() {
151        let asset = U64Id::NULL;
152        assert!(asset.is_null());
153    }
154
155    #[cfg(feature = "serde")]
156    #[test]
157    fn basic_serde() {
158        assert_eq!(
159            serde_json::from_str::<U64Id>("12345").unwrap(),
160            U64Id(74565)
161        );
162
163        assert_eq!(
164            serde_json::from_str::<U64Id>("\"a12b345\"").unwrap(),
165            U64Id(168997701)
166        );
167    }
168
169    #[cfg(feature = "serde")]
170    #[test]
171    fn serde_cycle() {
172        let input = "\"123454321\"";
173        let output = serde_json::from_str::<U64Id>(input).unwrap();
174        let input_again = serde_json::to_string(&output).unwrap();
175
176        assert_eq!(input, input_again);
177    }
178
179    #[cfg(feature = "serde")]
180    #[test]
181    fn serde_cycle_around() {
182        let input = U64Id(12345321);
183        let output = serde_json::to_string::<U64Id>(&input).unwrap();
184        let input_again = serde_json::from_str(&output).unwrap();
185
186        assert_eq!(input, input_again);
187    }
188}