titanium_model/
snowflake.rs

1//! Snowflake ID type for Discord
2//!
3//! Discord uses 64-bit unsigned integers for unique identifiers,
4//! but serializes them as strings in JSON to avoid precision loss.
5
6use serde::{Deserialize, Deserializer, Serialize, Serializer};
7use std::fmt;
8
9/// A Discord Snowflake ID.
10///
11/// Snowflakes are unique 64-bit unsigned integers used by Discord.
12/// They are serialized as strings in JSON to prevent precision loss
13/// in languages with limited integer precision (JavaScript).
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
15pub struct Snowflake(pub u64);
16
17impl Snowflake {
18    /// Create a new Snowflake from a u64 value.
19    #[inline]
20    #[must_use]
21    pub const fn new(id: u64) -> Self {
22        Self(id)
23    }
24
25    /// Get the raw u64 value.
26    #[inline]
27    #[must_use]
28    pub const fn get(self) -> u64 {
29        self.0
30    }
31
32    /// Extract the timestamp from this Snowflake.
33    ///
34    /// Returns milliseconds since Discord Epoch (2015-01-01T00:00:00Z).
35    #[inline]
36    #[must_use]
37    pub const fn timestamp(self) -> u64 {
38        (self.0 >> 22) + 1420070400000
39    }
40
41    /// Extract the internal worker ID.
42    #[inline]
43    #[must_use]
44    pub const fn worker_id(self) -> u8 {
45        ((self.0 & 0x3E0000) >> 17) as u8
46    }
47
48    /// Extract the internal process ID.
49    #[inline]
50    #[must_use]
51    pub const fn process_id(self) -> u8 {
52        ((self.0 & 0x1F000) >> 12) as u8
53    }
54
55    /// Extract the increment (sequence number within the same millisecond).
56    #[inline]
57    #[must_use]
58    pub const fn increment(self) -> u16 {
59        (self.0 & 0xFFF) as u16
60    }
61}
62
63impl fmt::Display for Snowflake {
64    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65        write!(f, "{}", self.0)
66    }
67}
68
69impl From<u64> for Snowflake {
70    #[inline]
71    fn from(value: u64) -> Self {
72        Self(value)
73    }
74}
75
76impl From<Snowflake> for u64 {
77    #[inline]
78    fn from(snowflake: Snowflake) -> Self {
79        snowflake.0
80    }
81}
82
83impl Serialize for Snowflake {
84    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
85    where
86        S: Serializer,
87    {
88        // Use itoa to format to stack buffer, avoiding heap allocation for the string
89        let mut buffer = itoa::Buffer::new();
90        let printed = buffer.format(self.0);
91        serializer.serialize_str(printed)
92    }
93}
94
95impl<'de> Deserialize<'de> for Snowflake {
96    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
97    where
98        D: Deserializer<'de>,
99    {
100        // Discord sends snowflakes as strings, but we also handle integers
101        struct SnowflakeVisitor;
102
103        impl serde::de::Visitor<'_> for SnowflakeVisitor {
104            type Value = Snowflake;
105
106            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
107                formatter.write_str("a string or integer snowflake ID")
108            }
109
110            fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
111            where
112                E: serde::de::Error,
113            {
114                Ok(Snowflake(value))
115            }
116
117            fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
118            where
119                E: serde::de::Error,
120            {
121                Ok(Snowflake(value as u64))
122            }
123
124            fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
125            where
126                E: serde::de::Error,
127            {
128                value
129                    .parse::<u64>()
130                    .map(Snowflake)
131                    .map_err(serde::de::Error::custom)
132            }
133        }
134
135        deserializer.deserialize_any(SnowflakeVisitor)
136    }
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142
143    #[test]
144    fn test_snowflake_parsing() {
145        let json_str = r#""175928847299117063""#;
146        let snowflake: Snowflake = crate::json::from_str(json_str).unwrap();
147        assert_eq!(snowflake.get(), 175928847299117063);
148    }
149
150    #[test]
151    fn test_snowflake_serialization() {
152        let snowflake = Snowflake::new(175928847299117063);
153        let json = crate::json::to_string(&snowflake).unwrap();
154        assert_eq!(json, r#""175928847299117063""#);
155    }
156
157    #[test]
158    fn test_snowflake_timestamp() {
159        // Known snowflake with known timestamp
160        let snowflake = Snowflake::new(175928847299117063);
161        assert!(snowflake.timestamp() > 1420070400000);
162    }
163}