Skip to main content

ferroid/base32/
snowflake.rs

1use core::{fmt, marker::PhantomData};
2
3use super::interface::Base32Ext;
4use crate::{
5    base32::Error,
6    generator::Result,
7    id::{BeBytes, Id, SnowflakeId},
8};
9
10/// Extension trait for Crockford Base32 encoding and decoding of ID types.
11///
12/// This trait enables converting IDs backed by integer types into fixed-length,
13/// lexicographically sortable Base32 representation using the [Crockford
14/// Base32](https://www.crockford.com/base32.html) alphabet.
15pub trait Base32SnowExt: SnowflakeId
16where
17    Self::Ty: BeBytes,
18{
19    /// Returns a stack-allocated, zero-initialized buffer of the backing
20    /// primitive in the ID.
21    ///
22    /// This is a convenience method that returns a [`BeBytes::ByteArray`]
23    #[must_use]
24    fn byte_array() -> <<Self as Id>::Ty as BeBytes>::ByteArray {
25        Self::inner_byte_array()
26    }
27
28    /// Returns a stack-allocated, zero-initialized buffer for Base32 encoding.
29    ///
30    /// This is a convenience method that returns a [`BeBytes::Base32Array`]
31    /// suitable for use with [`Base32SnowExt::encode_to_buf`]. The returned
32    /// buffer is stack-allocated, has a fixed size known at compile time, and
33    /// is guaranteed to match the Crockford Base32 output size for the backing
34    /// integer type.
35    ///
36    /// See also: [`Base32SnowExt::encode_to_buf`] for usage.
37    #[must_use]
38    fn base32_array() -> <<Self as Id>::Ty as BeBytes>::Base32Array {
39        Self::inner_base32_array()
40    }
41    /// Returns a formatter containing the Crockford Base32 representation of
42    /// the ID.
43    ///
44    /// The formatter is a lightweight, zero-allocation view over that internal
45    /// buffer that implements [`core::fmt::Display`] and [`AsRef<str>`].
46    ///
47    /// # Example
48    /// ```
49    /// use std::fmt::Write;
50    ///
51    /// use ferroid::{base32::Base32SnowExt, id::SnowflakeTwitterId};
52    ///
53    /// let id = SnowflakeTwitterId::from_raw(2_424_242_424_242_424_242);
54    ///
55    /// // Formatter is a view over the internal encoded buffer
56    /// let formatter = id.encode();
57    ///
58    /// assert_eq!(formatter, "23953MG16DJDJ");
59    /// ```
60    fn encode(&self) -> Base32SnowFormatter<Self> {
61        Base32SnowFormatter::new(self)
62    }
63    /// Encodes this ID into the provided buffer without heap allocation and
64    /// returns a formatter view over the buffer similar to
65    /// [`Base32SnowExt::encode`].
66    ///
67    /// The buffer must be exactly [`BeBytes::BASE32_SIZE`] bytes long, which is
68    /// guaranteed at compile time when using [`Base32SnowExt::base32_array`].
69    /// # Example
70    /// ```
71    /// use ferroid::{
72    ///     base32::Base32SnowExt,
73    ///     id::{BeBytes, Id, SnowflakeTwitterId},
74    /// };
75    ///
76    /// let id = SnowflakeTwitterId::from_raw(2_424_242_424_242_424_242);
77    ///
78    /// // Stack-allocated buffer of the correct size.
79    /// let mut buf = SnowflakeTwitterId::base32_array();
80    ///
81    /// // Formatter is a view over the external buffer
82    /// let formatter = id.encode_to_buf(&mut buf);
83    ///
84    /// assert_eq!(formatter, "23953MG16DJDJ");
85    ///
86    /// // Or access the raw bytes directly:
87    /// let as_str = unsafe { core::str::from_utf8_unchecked(buf.as_ref()) };
88    /// assert_eq!(as_str, "23953MG16DJDJ");
89    /// ```
90    ///
91    /// See also: [`Base32SnowExt::encode`] for a version that manages its own
92    /// buffer.
93    fn encode_to_buf<'buf>(
94        &self,
95        buf: &'buf mut <<Self as Id>::Ty as BeBytes>::Base32Array,
96    ) -> Base32SnowFormatterRef<'buf, Self> {
97        Base32SnowFormatterRef::new(self, buf)
98    }
99    /// Decodes a Base32-encoded string back into an ID.
100    ///
101    /// # ⚠️ Note
102    /// This method structurally decodes a Crockford base32 string into an
103    /// integer representing a Snowflake ID, regardless of whether the input is
104    /// a canonical Snowflake ID.
105    ///
106    /// - If the input string's Crockford encoding is larger than the
107    ///   Snowflake's maximum (i.e. "FZZZZZZZZZZZZ" for 64-bit integers), the
108    ///   excess bit is automatically ignored (i.e., the top 1 bit of the
109    ///   decoded value is discarded), so no overflow or error occurs.
110    /// - As a result, base32 strings that are technically invalid (i.e.,
111    ///   lexicographically greater than the max Snowflake string) will still
112    ///   successfully decode.
113    /// - **However**, if your ID type reserves bits (e.g., reserved or unused
114    ///   bits in your layout), decoding a string with excess bits may set these
115    ///   reserved bits to 1, causing `.is_valid()` to fail, and decode to
116    ///   return an error.
117    ///
118    /// # Errors
119    ///
120    /// Returns an error if the input string:
121    /// - is not the expected fixed length of the backing integer representation
122    ///   (i.e. 13 chars for u64, 26 chars for u128)
123    /// - contains invalid UTF8 or invalid ASCII characters (i.e., not in the
124    ///   Crockford Base32 alphabet)
125    /// - sets reserved bits that make the decoded value invalid for this ID
126    ///   type
127    ///
128    /// # Example
129    /// ```
130    /// use ferroid::{
131    ///     base32::{Base32SnowExt, Error},
132    ///     id::{Id, SnowflakeId, SnowflakeTwitterId},
133    /// };
134    ///
135    /// // Crockford Base32 encodes values in 5-bit chunks, so encoding a u64
136    /// // (64 bits)
137    /// // requires 13 characters (13 × 5 = 65 bits). Since u64 can only hold 64
138    /// // bits, the highest (leftmost) bit is discarded during decoding.
139    /// //
140    /// // This means *any* 13-character Base32 string will decode into a u64, even
141    /// // if it represents a value that exceeds the canonical range of a specific
142    /// // ID type.
143    /// //
144    /// // Many ID formats (such as Twitter Snowflake IDs) reserve one or more high
145    /// // bits for future use. These reserved bits **must remain unset** for the
146    /// // decoded value to be considered valid.
147    /// //
148    /// // For example, in a `SnowflakeTwitterId`, "7ZZZZZZZZZZZZ" represents the
149    /// // largest lexicographically valid encoding that fills all non-reserved bits
150    /// // with ones. Lexicographically larger values like "QZZZZZZZZZZZZ" decode to
151    /// // the *same* ID because their first character differs only in the highest
152    /// // (65th) bit, which is discarded:
153    /// // - '7' = 0b00111 → top bit 0, reserved bit 0, rest = 111...
154    /// // - 'Q' = 0b10111 → top bit 1, reserved bit 0, rest = 111...
155    /// //            ↑↑↑↑ identical after discarding MSB
156    /// let id1 = SnowflakeTwitterId::decode("7ZZZZZZZZZZZZ").unwrap();
157    /// let id2 = SnowflakeTwitterId::decode("QZZZZZZZZZZZZ").unwrap();
158    /// assert_eq!(id1, id2);
159    ///
160    /// // In contrast, "PZZZZZZZZZZZZ" differs in more significant bits and decodes
161    /// // to a distinct value:
162    /// // - 'P' = 0b10110 → top bit 1, reserved bit 0, rest = 110...
163    /// //               ↑ alters bits within the ID layout beyond the reserved region
164    /// let id3 = SnowflakeTwitterId::decode("PZZZZZZZZZZZZ").unwrap();
165    /// assert_ne!(id1, id3);
166    ///
167    /// // If the reserved bits are set (e.g., 'F' = 0b01111 or 'Z' = 0b11111),
168    /// // decoding fails and the invalid ID is returned:
169    /// // - 'F' = 0b01111 → top bit 0, reserved bit 1, rest = 111...
170    /// //            ↑ reserved bit is set - ID is invalid
171    /// let id = SnowflakeTwitterId::decode("FZZZZZZZZZZZZ")
172    ///     .or_else(|err| {
173    ///         match err {
174    ///             Error::DecodeOverflow { id } => {
175    ///                 debug_assert!(!id.is_valid());
176    ///                 // clears reserved bits
177    ///                 let valid = id.into_valid();
178    ///                 debug_assert!(valid.is_valid());
179    ///                 Ok(valid)
180    ///             }
181    ///             other => Err(other),
182    ///         }
183    ///     })
184    ///     .expect("should produce a valid ID");
185    /// ```
186    fn decode(input: impl AsRef<[u8]>) -> Result<Self, Error<Self>> {
187        let decoded = Self::inner_decode(input)?;
188        if !decoded.is_valid() {
189            return Err(Error::DecodeOverflow { id: decoded });
190        }
191        Ok(decoded)
192    }
193}
194
195impl<ID> Base32SnowExt for ID
196where
197    ID: SnowflakeId,
198    ID::Ty: BeBytes,
199{
200}
201
202/// A reusable builder that owns the Base32 buffer and formats an ID.
203#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)]
204pub struct Base32SnowFormatter<T>
205where
206    T: Base32SnowExt,
207    T::Ty: BeBytes,
208{
209    _id: PhantomData<T>,
210    buf: <T::Ty as BeBytes>::Base32Array,
211}
212
213impl<T: Base32SnowExt> Base32SnowFormatter<T>
214where
215    T::Ty: BeBytes,
216{
217    pub fn new(id: &T) -> Self {
218        let mut buf = T::base32_array();
219        id.inner_encode_to_buf(&mut buf);
220        Self {
221            _id: PhantomData,
222            buf,
223        }
224    }
225
226    /// Returns the underlying bytes of the base32 encoding.
227    #[must_use]
228    pub fn as_bytes(&self) -> &[u8] {
229        self.buf.as_ref()
230    }
231
232    /// Returns a `&str` view of the base32 encoding.
233    #[must_use]
234    pub fn as_str(&self) -> &str {
235        // SAFETY: `self.buf` holds only valid Crockford Base32 ASCII characters
236        unsafe { core::str::from_utf8_unchecked(self.as_bytes()) }
237    }
238
239    /// Returns an allocated `String` of the base32 encoding.
240    #[cfg(feature = "alloc")]
241    #[must_use]
242    pub fn as_string(&self) -> alloc::string::String {
243        // SAFETY: `self.buf` holds only valid Crockford Base32 ASCII characters
244        unsafe { alloc::string::String::from_utf8_unchecked(self.as_bytes().to_vec()) }
245    }
246
247    /// Consumes the formatter and returns the raw buffer.
248    pub const fn into_inner(self) -> <T::Ty as BeBytes>::Base32Array {
249        self.buf
250    }
251}
252
253impl<T: Base32SnowExt> core::hash::Hash for Base32SnowFormatter<T>
254where
255    T::Ty: BeBytes,
256{
257    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
258        self.as_str().hash(state);
259    }
260}
261
262impl<T: Base32SnowExt> fmt::Display for Base32SnowFormatter<T>
263where
264    T::Ty: BeBytes,
265{
266    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
267        f.write_str(self.as_str())
268    }
269}
270
271impl<T: Base32SnowExt> fmt::Debug for Base32SnowFormatter<T>
272where
273    T::Ty: BeBytes,
274{
275    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
276        f.debug_tuple("Base32SnowFormatter")
277            .field(&self.as_str())
278            .finish()
279    }
280}
281
282#[cfg(feature = "alloc")]
283impl<T: Base32SnowExt> From<&Base32SnowFormatter<T>> for alloc::string::String
284where
285    T::Ty: BeBytes,
286{
287    fn from(formatter: &Base32SnowFormatter<T>) -> Self {
288        formatter.as_string()
289    }
290}
291#[cfg(feature = "alloc")]
292impl<T: Base32SnowExt> From<Base32SnowFormatter<T>> for alloc::string::String
293where
294    T::Ty: BeBytes,
295{
296    fn from(formatter: Base32SnowFormatter<T>) -> Self {
297        formatter.as_string()
298    }
299}
300
301impl<T: Base32SnowExt> AsRef<str> for Base32SnowFormatter<T>
302where
303    T::Ty: BeBytes,
304{
305    fn as_ref(&self) -> &str {
306        self.as_str()
307    }
308}
309
310impl<T: Base32SnowExt> AsRef<[u8]> for Base32SnowFormatter<T>
311where
312    T::Ty: BeBytes,
313{
314    fn as_ref(&self) -> &[u8] {
315        self.as_bytes()
316    }
317}
318
319impl<T: Base32SnowExt> core::ops::Deref for Base32SnowFormatter<T>
320where
321    T::Ty: BeBytes,
322{
323    type Target = str;
324
325    fn deref(&self) -> &str {
326        self.as_str()
327    }
328}
329
330impl<T: Base32SnowExt> core::borrow::Borrow<str> for Base32SnowFormatter<T>
331where
332    T::Ty: BeBytes,
333{
334    fn borrow(&self) -> &str {
335        self.as_str()
336    }
337}
338
339impl<T: Base32SnowExt> PartialEq<str> for Base32SnowFormatter<T>
340where
341    T::Ty: BeBytes,
342{
343    fn eq(&self, other: &str) -> bool {
344        self.as_str() == other
345    }
346}
347
348impl<T: Base32SnowExt> PartialEq<&str> for Base32SnowFormatter<T>
349where
350    T::Ty: BeBytes,
351{
352    fn eq(&self, other: &&str) -> bool {
353        self == *other
354    }
355}
356
357impl<T: Base32SnowExt> PartialEq<Base32SnowFormatter<T>> for &str
358where
359    T::Ty: BeBytes,
360{
361    fn eq(&self, other: &Base32SnowFormatter<T>) -> bool {
362        other == *self
363    }
364}
365
366#[cfg(feature = "alloc")]
367impl<T: Base32SnowExt> PartialEq<alloc::string::String> for Base32SnowFormatter<T>
368where
369    T::Ty: BeBytes,
370{
371    fn eq(&self, other: &alloc::string::String) -> bool {
372        self.as_str() == other.as_str()
373    }
374}
375#[cfg(feature = "alloc")]
376impl<T: Base32SnowExt> PartialEq<Base32SnowFormatter<T>> for alloc::string::String
377where
378    T::Ty: BeBytes,
379{
380    fn eq(&self, other: &Base32SnowFormatter<T>) -> bool {
381        self.as_str() == other.as_str()
382    }
383}
384
385/// A builder that borrows a user-supplied buffer for Base32 formatting.
386#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)]
387pub struct Base32SnowFormatterRef<'a, T>
388where
389    T: Base32SnowExt,
390    T::Ty: BeBytes,
391{
392    _id: PhantomData<T>,
393    buf: &'a <T::Ty as BeBytes>::Base32Array,
394}
395
396impl<'a, T: Base32SnowExt> Base32SnowFormatterRef<'a, T>
397where
398    T::Ty: BeBytes,
399{
400    pub fn new(id: &T, buf: &'a mut <T::Ty as BeBytes>::Base32Array) -> Self {
401        id.inner_encode_to_buf(buf);
402        Self {
403            _id: PhantomData,
404            buf,
405        }
406    }
407
408    /// Returns the underlying bytes of the base32 encoding.
409    #[must_use]
410    pub fn as_bytes(&self) -> &[u8] {
411        self.buf.as_ref()
412    }
413
414    /// Returns a `&str` view of the base32 encoding.
415    #[must_use]
416    pub fn as_str(&self) -> &str {
417        // SAFETY: `self.buf` holds only valid Crockford Base32 ASCII characters
418        unsafe { core::str::from_utf8_unchecked(self.as_bytes()) }
419    }
420
421    /// Returns an allocated `String` of the base32 encoding.
422    #[cfg(feature = "alloc")]
423    #[must_use]
424    pub fn as_string(&self) -> alloc::string::String {
425        // SAFETY: `self.buf` holds only valid Crockford Base32 ASCII characters
426        unsafe { alloc::string::String::from_utf8_unchecked(self.as_bytes().to_vec()) }
427    }
428}
429
430impl<T: Base32SnowExt> core::hash::Hash for Base32SnowFormatterRef<'_, T>
431where
432    T::Ty: BeBytes,
433{
434    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
435        self.as_str().hash(state);
436    }
437}
438
439impl<T: Base32SnowExt> fmt::Display for Base32SnowFormatterRef<'_, T>
440where
441    T::Ty: BeBytes,
442{
443    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
444        f.write_str(self.as_str())
445    }
446}
447
448impl<T: Base32SnowExt> fmt::Debug for Base32SnowFormatterRef<'_, T>
449where
450    T::Ty: BeBytes,
451{
452    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
453        f.debug_tuple("Base32SnowFormatterRef")
454            .field(&self.as_str())
455            .finish()
456    }
457}
458
459#[cfg(feature = "alloc")]
460impl<T: Base32SnowExt> From<&Base32SnowFormatterRef<'_, T>> for alloc::string::String
461where
462    T::Ty: BeBytes,
463{
464    fn from(formatter: &Base32SnowFormatterRef<'_, T>) -> Self {
465        formatter.as_string()
466    }
467}
468#[cfg(feature = "alloc")]
469impl<T: Base32SnowExt> From<Base32SnowFormatterRef<'_, T>> for alloc::string::String
470where
471    T::Ty: BeBytes,
472{
473    fn from(formatter: Base32SnowFormatterRef<'_, T>) -> Self {
474        formatter.as_string()
475    }
476}
477
478impl<T: Base32SnowExt> AsRef<str> for Base32SnowFormatterRef<'_, T>
479where
480    T::Ty: BeBytes,
481{
482    fn as_ref(&self) -> &str {
483        self.as_str()
484    }
485}
486
487impl<T: Base32SnowExt> AsRef<[u8]> for Base32SnowFormatterRef<'_, T>
488where
489    T::Ty: BeBytes,
490{
491    fn as_ref(&self) -> &[u8] {
492        self.as_bytes()
493    }
494}
495
496impl<T: Base32SnowExt> core::ops::Deref for Base32SnowFormatterRef<'_, T>
497where
498    T::Ty: BeBytes,
499{
500    type Target = str;
501
502    fn deref(&self) -> &str {
503        self.as_str()
504    }
505}
506
507impl<T: Base32SnowExt> core::borrow::Borrow<str> for Base32SnowFormatterRef<'_, T>
508where
509    T::Ty: BeBytes,
510{
511    fn borrow(&self) -> &str {
512        self.as_str()
513    }
514}
515
516impl<T: Base32SnowExt> PartialEq<str> for Base32SnowFormatterRef<'_, T>
517where
518    T::Ty: BeBytes,
519{
520    fn eq(&self, other: &str) -> bool {
521        self.as_str() == other
522    }
523}
524impl<T: Base32SnowExt> PartialEq<&str> for Base32SnowFormatterRef<'_, T>
525where
526    T::Ty: BeBytes,
527{
528    fn eq(&self, other: &&str) -> bool {
529        self == *other
530    }
531}
532impl<T: Base32SnowExt> PartialEq<Base32SnowFormatterRef<'_, T>> for &str
533where
534    T::Ty: BeBytes,
535{
536    fn eq(&self, other: &Base32SnowFormatterRef<'_, T>) -> bool {
537        other == *self
538    }
539}
540
541#[cfg(feature = "alloc")]
542impl<T: Base32SnowExt> PartialEq<alloc::string::String> for Base32SnowFormatterRef<'_, T>
543where
544    T::Ty: BeBytes,
545{
546    fn eq(&self, other: &alloc::string::String) -> bool {
547        self.as_str() == other.as_str()
548    }
549}
550#[cfg(feature = "alloc")]
551impl<T: Base32SnowExt> PartialEq<Base32SnowFormatterRef<'_, T>> for alloc::string::String
552where
553    T::Ty: BeBytes,
554{
555    fn eq(&self, other: &Base32SnowFormatterRef<'_, T>) -> bool {
556        self.as_str() == other.as_str()
557    }
558}
559
560#[cfg(all(test, feature = "alloc", feature = "snowflake"))]
561mod alloc_test {
562    use alloc::string::ToString;
563
564    use crate::{
565        base32::Base32SnowExt,
566        id::{SnowflakeDiscordId, SnowflakeInstagramId, SnowflakeMastodonId, SnowflakeTwitterId},
567    };
568
569    #[test]
570    fn twitter_display() {
571        let id = SnowflakeTwitterId::decode("01ARZ3NDEKTSV").unwrap();
572        assert_eq!(alloc::format!("{id}"), "01ARZ3NDEKTSV");
573        assert_eq!(id.to_string(), "01ARZ3NDEKTSV");
574    }
575    #[test]
576    fn instagram_display() {
577        let id = SnowflakeInstagramId::decode("01ARZ3NDEKTSV").unwrap();
578        assert_eq!(alloc::format!("{id}"), "01ARZ3NDEKTSV");
579        assert_eq!(id.to_string(), "01ARZ3NDEKTSV");
580    }
581    #[test]
582    fn mastodon_display() {
583        let id = SnowflakeMastodonId::decode("01ARZ3NDEKTSV").unwrap();
584        assert_eq!(alloc::format!("{id}"), "01ARZ3NDEKTSV");
585        assert_eq!(id.to_string(), "01ARZ3NDEKTSV");
586    }
587    #[test]
588    fn discord_display() {
589        let id = SnowflakeDiscordId::decode("01ARZ3NDEKTSV").unwrap();
590        assert_eq!(alloc::format!("{id}"), "01ARZ3NDEKTSV");
591        assert_eq!(id.to_string(), "01ARZ3NDEKTSV");
592    }
593}
594
595#[cfg(all(test, feature = "snowflake"))]
596mod test {
597    use crate::{
598        base32::{Base32SnowExt, Error},
599        id::{SnowflakeDiscordId, SnowflakeInstagramId, SnowflakeMastodonId, SnowflakeTwitterId},
600    };
601
602    #[test]
603    fn snow_try_from() {
604        // Don't need to test all IDs
605        let id = SnowflakeTwitterId::try_from("01ARZ3NDEKTSV").unwrap();
606        let encoded = id.encode();
607        assert_eq!(encoded, "01ARZ3NDEKTSV");
608    }
609
610    #[test]
611    fn snow_from_str() {
612        // Don't need to test all IDs
613        use core::str::FromStr;
614        let id = SnowflakeTwitterId::from_str("01ARZ3NDEKTSV").unwrap();
615        let encoded = id.encode();
616        assert_eq!(encoded, "01ARZ3NDEKTSV");
617    }
618
619    #[test]
620    fn twitter_max() {
621        let id = SnowflakeTwitterId::from_components(
622            SnowflakeTwitterId::max_timestamp(),
623            SnowflakeTwitterId::max_machine_id(),
624            SnowflakeTwitterId::max_sequence(),
625        );
626        assert_eq!(id.timestamp(), SnowflakeTwitterId::max_timestamp());
627        assert_eq!(id.machine_id(), SnowflakeTwitterId::max_machine_id());
628        assert_eq!(id.sequence(), SnowflakeTwitterId::max_sequence());
629
630        let encoded = id.encode();
631        assert_eq!(encoded, "7ZZZZZZZZZZZZ");
632        let decoded = SnowflakeTwitterId::decode(encoded).unwrap();
633
634        assert_eq!(decoded.timestamp(), SnowflakeTwitterId::max_timestamp());
635        assert_eq!(decoded.machine_id(), SnowflakeTwitterId::max_machine_id());
636        assert_eq!(decoded.sequence(), SnowflakeTwitterId::max_sequence());
637        assert_eq!(id, decoded);
638    }
639
640    #[test]
641    fn twitter_zero() {
642        let id = SnowflakeTwitterId::from_components(0, 0, 0);
643        assert_eq!(id.timestamp(), 0);
644        assert_eq!(id.machine_id(), 0);
645        assert_eq!(id.sequence(), 0);
646
647        let encoded = id.encode();
648        assert_eq!(encoded, "0000000000000");
649        let decoded = SnowflakeTwitterId::decode(encoded).unwrap();
650
651        assert_eq!(decoded.timestamp(), 0);
652        assert_eq!(decoded.machine_id(), 0);
653        assert_eq!(decoded.sequence(), 0);
654        assert_eq!(id, decoded);
655    }
656
657    #[test]
658    fn discord_max() {
659        let id = SnowflakeDiscordId::from_components(
660            SnowflakeDiscordId::max_timestamp(),
661            SnowflakeDiscordId::max_machine_id(),
662            SnowflakeDiscordId::max_sequence(),
663        );
664        assert_eq!(id.timestamp(), SnowflakeDiscordId::max_timestamp());
665        assert_eq!(id.machine_id(), SnowflakeDiscordId::max_machine_id());
666        assert_eq!(id.sequence(), SnowflakeDiscordId::max_sequence());
667
668        let encoded = id.encode();
669        assert_eq!(encoded, "FZZZZZZZZZZZZ");
670        let decoded = SnowflakeDiscordId::decode(encoded).unwrap();
671
672        assert_eq!(decoded.timestamp(), SnowflakeDiscordId::max_timestamp());
673        assert_eq!(decoded.machine_id(), SnowflakeDiscordId::max_machine_id());
674        assert_eq!(decoded.sequence(), SnowflakeDiscordId::max_sequence());
675        assert_eq!(id, decoded);
676    }
677
678    #[test]
679    fn discord_zero() {
680        let id = SnowflakeDiscordId::from_components(0, 0, 0);
681        assert_eq!(id.timestamp(), 0);
682        assert_eq!(id.machine_id(), 0);
683        assert_eq!(id.sequence(), 0);
684
685        let encoded = id.encode();
686        assert_eq!(encoded, "0000000000000");
687        let decoded = SnowflakeDiscordId::decode(encoded).unwrap();
688
689        assert_eq!(decoded.timestamp(), 0);
690        assert_eq!(decoded.machine_id(), 0);
691        assert_eq!(decoded.sequence(), 0);
692        assert_eq!(id, decoded);
693    }
694
695    #[test]
696    fn instagram_max() {
697        let id = SnowflakeInstagramId::from_components(
698            SnowflakeInstagramId::max_timestamp(),
699            SnowflakeInstagramId::max_machine_id(),
700            SnowflakeInstagramId::max_sequence(),
701        );
702        assert_eq!(id.timestamp(), SnowflakeInstagramId::max_timestamp());
703        assert_eq!(id.machine_id(), SnowflakeInstagramId::max_machine_id());
704        assert_eq!(id.sequence(), SnowflakeInstagramId::max_sequence());
705
706        let encoded = id.encode();
707        assert_eq!(encoded, "FZZZZZZZZZZZZ");
708        let decoded = SnowflakeInstagramId::decode(encoded).unwrap();
709
710        assert_eq!(decoded.timestamp(), SnowflakeInstagramId::max_timestamp());
711        assert_eq!(decoded.machine_id(), SnowflakeInstagramId::max_machine_id());
712        assert_eq!(decoded.sequence(), SnowflakeInstagramId::max_sequence());
713        assert_eq!(id, decoded);
714    }
715
716    #[test]
717    fn instagram_zero() {
718        let id = SnowflakeInstagramId::from_components(0, 0, 0);
719        assert_eq!(id.timestamp(), 0);
720        assert_eq!(id.machine_id(), 0);
721        assert_eq!(id.sequence(), 0);
722
723        let encoded = id.encode();
724        assert_eq!(encoded, "0000000000000");
725        let decoded = SnowflakeInstagramId::decode(encoded).unwrap();
726
727        assert_eq!(decoded.timestamp(), 0);
728        assert_eq!(decoded.machine_id(), 0);
729        assert_eq!(decoded.sequence(), 0);
730        assert_eq!(id, decoded);
731    }
732
733    #[test]
734    fn mastodon_max() {
735        let id = SnowflakeMastodonId::from_components(
736            SnowflakeMastodonId::max_timestamp(),
737            SnowflakeMastodonId::max_machine_id(),
738            SnowflakeMastodonId::max_sequence(),
739        );
740        assert_eq!(id.timestamp(), SnowflakeMastodonId::max_timestamp());
741        assert_eq!(id.machine_id(), SnowflakeMastodonId::max_machine_id());
742        assert_eq!(id.sequence(), SnowflakeMastodonId::max_sequence());
743
744        let encoded = id.encode();
745        assert_eq!(encoded, "FZZZZZZZZZZZZ");
746        let decoded = SnowflakeMastodonId::decode(encoded).unwrap();
747
748        assert_eq!(decoded.timestamp(), SnowflakeMastodonId::max_timestamp());
749        assert_eq!(decoded.machine_id(), SnowflakeMastodonId::max_machine_id());
750        assert_eq!(decoded.sequence(), SnowflakeMastodonId::max_sequence());
751        assert_eq!(id, decoded);
752    }
753
754    #[test]
755    fn mastodon_zero() {
756        let id = SnowflakeMastodonId::from_components(0, 0, 0);
757        assert_eq!(id.timestamp(), 0);
758        assert_eq!(id.machine_id(), 0);
759        assert_eq!(id.sequence(), 0);
760
761        let encoded = id.encode();
762        assert_eq!(encoded, "0000000000000");
763        let decoded = SnowflakeMastodonId::decode(encoded).unwrap();
764
765        assert_eq!(decoded.timestamp(), 0);
766        assert_eq!(decoded.machine_id(), 0);
767        assert_eq!(decoded.sequence(), 0);
768        assert_eq!(id, decoded);
769    }
770
771    #[test]
772    fn decode_invalid_character_fails() {
773        // Base32 Crockford disallows symbols like `@`
774        let invalid = "000000@000000";
775        let res = SnowflakeTwitterId::decode(invalid);
776        assert_eq!(
777            res.unwrap_err(),
778            Error::DecodeInvalidAscii {
779                byte: b'@',
780                index: 6,
781            }
782        );
783    }
784
785    #[test]
786    fn decode_invalid_length_fails() {
787        // Shorter than 13-byte base32 for u64
788        let too_short = "012345678901";
789        let res = SnowflakeTwitterId::decode(too_short);
790        assert_eq!(res.unwrap_err(), Error::DecodeInvalidLen { len: 12 });
791
792        // Longer than 13-byte base32 for u64
793        let too_long = "01234567890123";
794        let res = SnowflakeTwitterId::decode(too_long);
795
796        assert_eq!(res.unwrap_err(), Error::DecodeInvalidLen { len: 14 });
797    }
798}