Skip to main content

nexus_id/
types.rs

1//! Newtype wrappers for ID values.
2//!
3//! These types provide type safety and encapsulation for generated IDs.
4//! Each type wraps an internal representation and provides methods for
5//! conversion, parsing, and access to the underlying data.
6//!
7//! All types support configurable capacity via const generic `CAP` for
8//! fixed-size wire formats. The default capacity is the minimum required.
9//!
10//! # Example
11//!
12//! ```rust
13//! use nexus_id::Base62Id;
14//!
15//! // Default capacity (16 bytes)
16//! let id: Base62Id = Base62Id::encode(12345);
17//!
18//! // Custom capacity for 32-byte wire format
19//! let id: Base62Id<32> = Base62Id::encode(12345);
20//! ```
21
22use core::cmp::Ordering;
23use core::fmt;
24use core::hash::{Hash, Hasher};
25use core::ops::Deref;
26use core::str::FromStr;
27
28use nexus_ascii::AsciiString;
29
30use crate::parse::{self, DecodeError, ParseError, UuidParseError};
31
32// ============================================================================
33// UUID Types
34// ============================================================================
35
36/// UUID in standard dashed format.
37///
38/// Format: `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` (36 characters)
39///
40/// This type wraps the string representation of a UUID. It implements
41/// `Copy`, `Hash`, `Eq`, and `Deref<Target = str>` for ergonomic usage.
42///
43/// # Capacity
44///
45/// The default capacity is 40 bytes (minimum required). Use a larger capacity
46/// for fixed-size wire formats: `Uuid<64>`.
47///
48/// # Example
49///
50/// ```rust
51/// use nexus_id::uuid::UuidV4;
52///
53/// let mut generator = UuidV4::new(12345);
54/// let id = generator.next();
55///
56/// // Use as &str via Deref
57/// println!("{}", &*id);
58///
59/// // Or explicitly
60/// println!("{}", id.as_str());
61/// ```
62#[derive(Clone, Copy, PartialEq, Eq)]
63#[repr(transparent)]
64pub struct Uuid<const CAP: usize = 40>(pub(crate) AsciiString<CAP>);
65
66impl<const CAP: usize> Uuid<CAP> {
67    /// Create a Uuid from raw (hi, lo) 64-bit components.
68    ///
69    /// The `hi` value contains the upper 64 bits and `lo` the lower 64 bits
70    /// of the 128-bit UUID. This is the inverse of [`decode()`](Self::decode).
71    ///
72    /// Any (hi, lo) pair produces a valid UUID string. No validation is needed.
73    #[inline]
74    pub fn from_raw(hi: u64, lo: u64) -> Self {
75        Self(crate::encode::uuid_dashed(hi, lo))
76    }
77
78    /// Construct from a 16-byte big-endian binary representation.
79    ///
80    /// This is the inverse of [`to_bytes()`](Self::to_bytes).
81    ///
82    /// # Errors
83    ///
84    /// Returns [`ParseError::InvalidLength`] if `bytes.len() != 16`.
85    pub fn from_bytes(bytes: &[u8]) -> Result<Self, ParseError> {
86        if bytes.len() != 16 {
87            return Err(ParseError::InvalidLength {
88                expected: 16,
89                got: bytes.len(),
90            });
91        }
92        let hi = u64::from_be_bytes(bytes[0..8].try_into().expect("8-byte slice"));
93        let lo = u64::from_be_bytes(bytes[8..16].try_into().expect("8-byte slice"));
94        Ok(Self::from_raw(hi, lo))
95    }
96
97    /// Construct from a byte slice without length validation.
98    ///
99    /// # Safety
100    ///
101    /// The caller must guarantee that `bytes.len() >= 16`. Reads the first
102    /// 16 bytes as a big-endian UUID.
103    #[inline]
104    pub unsafe fn from_bytes_unchecked(bytes: &[u8]) -> Self {
105        debug_assert!(bytes.len() >= 16);
106        // SAFETY: caller guarantees bytes.len() >= 16
107        unsafe {
108            let hi = u64::from_be_bytes(bytes.get_unchecked(0..8).try_into().unwrap_unchecked());
109            let lo = u64::from_be_bytes(bytes.get_unchecked(8..16).try_into().unwrap_unchecked());
110            Self::from_raw(hi, lo)
111        }
112    }
113
114    /// Returns the UUID as a string slice.
115    #[inline]
116    pub fn as_str(&self) -> &str {
117        self.0.as_str()
118    }
119
120    /// Returns the UUID as a byte slice.
121    #[inline]
122    pub fn as_bytes(&self) -> &[u8] {
123        self.0.as_bytes()
124    }
125
126    /// Decode the UUID back to raw (hi, lo) bytes.
127    ///
128    /// Uses SIMD (SSSE3) hex decoding when available, falling back to
129    /// scalar on other architectures.
130    #[inline]
131    pub fn decode(&self) -> (u64, u64) {
132        // SAFETY: self.0 was validated at construction — always valid hex+dashes.
133        // Uuid is always 36 bytes. try_into is infallible here.
134        let bytes: &[u8; 36] = self.0.as_bytes().try_into().unwrap();
135        unsafe { crate::simd::uuid_parse_dashed(bytes).unwrap_unchecked() }
136    }
137
138    /// Extract the UUID version (4 bits).
139    #[inline]
140    pub fn version(&self) -> u8 {
141        // Version is char at position 14
142        hex_digit(self.0.as_bytes()[14])
143    }
144
145    /// Parse a UUID from a dashed string.
146    ///
147    /// Accepts format: `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` (36 chars).
148    /// Case-insensitive for hex digits.
149    ///
150    /// # Errors
151    ///
152    /// Returns [`UuidParseError`] if the input has wrong length, invalid hex
153    /// characters, or missing/misplaced dashes.
154    pub fn parse(s: &str) -> Result<Self, UuidParseError> {
155        let bytes = s.as_bytes();
156        if bytes.len() != 36 {
157            return Err(UuidParseError::InvalidLength {
158                expected: 36,
159                got: bytes.len(),
160            });
161        }
162
163        // Validate dashes at positions 8, 13, 18, 23
164        if bytes[8] != b'-' || bytes[13] != b'-' || bytes[18] != b'-' || bytes[23] != b'-' {
165            return Err(UuidParseError::InvalidFormat);
166        }
167
168        // SAFETY: We verified bytes.len() == 36.
169        let input: &[u8; 36] = unsafe { &*(bytes.as_ptr().cast::<[u8; 36]>()) };
170
171        // Decode via SIMD path: compacts dashes out, decodes 32 hex chars in parallel.
172        let (hi, lo) = crate::simd::uuid_parse_dashed(input).map_err(|pos| {
173            // Map compacted 32-byte position back to 36-byte input position.
174            let input_pos = match pos {
175                0..=7 => pos,       // segment 1: no offset
176                8..=11 => pos + 1,  // segment 2: skip dash at 8
177                12..=15 => pos + 2, // segment 3: skip dashes at 8, 13
178                16..=19 => pos + 3, // segment 4: skip dashes at 8, 13, 18
179                _ => pos + 4,       // segment 5: skip all 4 dashes
180            };
181            UuidParseError::InvalidChar {
182                position: input_pos,
183                byte: bytes[input_pos],
184            }
185        })?;
186
187        Ok(Self::from_raw(hi, lo))
188    }
189
190    /// Convert to compact format (no dashes).
191    ///
192    /// Returns a `UuidCompact` with default capacity.
193    #[inline]
194    pub fn to_compact(&self) -> UuidCompact {
195        let (hi, lo) = self.decode();
196        UuidCompact::from_raw(hi, lo)
197    }
198
199    /// Check if this is the nil UUID (all zeros).
200    ///
201    /// Compares raw bytes directly — no hex decoding needed.
202    #[inline]
203    pub fn is_nil(&self) -> bool {
204        self.0.as_bytes() == b"00000000-0000-0000-0000-000000000000"
205    }
206
207    /// Extract the timestamp for UUID v7 (milliseconds since Unix epoch).
208    ///
209    /// Returns `None` if this is not a v7 UUID.
210    #[inline]
211    pub fn timestamp_ms(&self) -> Option<u64> {
212        if self.version() != 7 {
213            return None;
214        }
215        let (hi, _) = self.decode();
216        Some(hi >> 16)
217    }
218
219    /// Get the raw 128-bit value as big-endian bytes.
220    pub fn to_bytes(&self) -> [u8; 16] {
221        let (hi, lo) = self.decode();
222        let mut out = [0u8; 16];
223        out[..8].copy_from_slice(&hi.to_be_bytes());
224        out[8..].copy_from_slice(&lo.to_be_bytes());
225        out
226    }
227}
228
229impl<const CAP: usize> Deref for Uuid<CAP> {
230    type Target = str;
231
232    #[inline]
233    fn deref(&self) -> &str {
234        self.0.as_str()
235    }
236}
237
238impl<const CAP: usize> AsRef<str> for Uuid<CAP> {
239    #[inline]
240    fn as_ref(&self) -> &str {
241        self.0.as_str()
242    }
243}
244
245impl<const CAP: usize> fmt::Display for Uuid<CAP> {
246    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
247        f.write_str(self.0.as_str())
248    }
249}
250
251impl<const CAP: usize> fmt::Debug for Uuid<CAP> {
252    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
253        write!(f, "Uuid({})", self.0.as_str())
254    }
255}
256
257impl<const CAP: usize> Hash for Uuid<CAP> {
258    #[inline]
259    fn hash<H: Hasher>(&self, state: &mut H) {
260        self.0.hash(state);
261    }
262}
263
264impl<const CAP: usize> Ord for Uuid<CAP> {
265    #[inline]
266    fn cmp(&self, other: &Self) -> Ordering {
267        // Lexicographic order = time order for v7 UUIDs
268        self.0.cmp(&other.0)
269    }
270}
271
272impl<const CAP: usize> PartialOrd for Uuid<CAP> {
273    #[inline]
274    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
275        Some(self.cmp(other))
276    }
277}
278
279impl<const CAP: usize> FromStr for Uuid<CAP> {
280    type Err = UuidParseError;
281
282    #[inline]
283    fn from_str(s: &str) -> Result<Self, Self::Err> {
284        Self::parse(s)
285    }
286}
287
288// ============================================================================
289// UUID Compact (no dashes)
290// ============================================================================
291
292/// UUID in compact format (no dashes).
293///
294/// Format: `xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` (32 characters)
295///
296/// # Capacity
297///
298/// The default capacity is 32 bytes (minimum required). Use a larger capacity
299/// for fixed-size wire formats: `UuidCompact<64>`.
300#[derive(Clone, Copy, PartialEq, Eq)]
301#[repr(transparent)]
302pub struct UuidCompact<const CAP: usize = 32>(pub(crate) AsciiString<CAP>);
303
304impl<const CAP: usize> UuidCompact<CAP> {
305    /// Create a UuidCompact from raw (hi, lo) 64-bit components.
306    ///
307    /// The `hi` value contains the upper 64 bits and `lo` the lower 64 bits
308    /// of the 128-bit UUID. This is the inverse of [`decode()`](Self::decode).
309    #[inline]
310    pub fn from_raw(hi: u64, lo: u64) -> Self {
311        Self(crate::encode::hex_u128(hi, lo))
312    }
313
314    /// Construct from a 16-byte big-endian binary representation.
315    ///
316    /// This is the inverse of [`to_bytes()`](Self::to_bytes).
317    ///
318    /// # Errors
319    ///
320    /// Returns [`ParseError::InvalidLength`] if `bytes.len() != 16`.
321    pub fn from_bytes(bytes: &[u8]) -> Result<Self, ParseError> {
322        if bytes.len() != 16 {
323            return Err(ParseError::InvalidLength {
324                expected: 16,
325                got: bytes.len(),
326            });
327        }
328        let hi = u64::from_be_bytes(bytes[0..8].try_into().expect("8-byte slice"));
329        let lo = u64::from_be_bytes(bytes[8..16].try_into().expect("8-byte slice"));
330        Ok(Self::from_raw(hi, lo))
331    }
332
333    /// Construct from a byte slice without length validation.
334    ///
335    /// # Safety
336    ///
337    /// The caller must guarantee that `bytes.len() >= 16`.
338    #[inline]
339    pub unsafe fn from_bytes_unchecked(bytes: &[u8]) -> Self {
340        debug_assert!(bytes.len() >= 16);
341        // SAFETY: caller guarantees bytes.len() >= 16
342        unsafe {
343            let hi = u64::from_be_bytes(bytes.get_unchecked(0..8).try_into().unwrap_unchecked());
344            let lo = u64::from_be_bytes(bytes.get_unchecked(8..16).try_into().unwrap_unchecked());
345            Self::from_raw(hi, lo)
346        }
347    }
348
349    #[inline]
350    pub fn as_str(&self) -> &str {
351        self.0.as_str()
352    }
353
354    #[inline]
355    pub fn as_bytes(&self) -> &[u8] {
356        self.0.as_bytes()
357    }
358
359    /// Decode back to raw (hi, lo) bytes.
360    pub fn decode(&self) -> (u64, u64) {
361        let bytes: &[u8; 32] = self.0.as_bytes().try_into().expect("32-byte hex string");
362        // SAFETY: Data was validated at construction; decode cannot fail.
363        unsafe { crate::simd::hex_decode_32(bytes).unwrap_unchecked() }
364    }
365
366    /// Parse a compact UUID from a hex string (no dashes).
367    ///
368    /// Accepts format: 32 hex characters. Case-insensitive.
369    pub fn parse(s: &str) -> Result<Self, ParseError> {
370        let bytes = s.as_bytes();
371        if bytes.len() != 32 {
372            return Err(ParseError::InvalidLength {
373                expected: 32,
374                got: bytes.len(),
375            });
376        }
377
378        // SIMD path: validates and decodes in a single pass
379        let hex_bytes: &[u8; 32] = bytes.try_into().expect("32-byte hex string");
380        let (hi, lo) =
381            crate::simd::hex_decode_32(hex_bytes).map_err(|pos| ParseError::InvalidChar {
382                position: pos,
383                byte: bytes[pos],
384            })?;
385
386        Ok(Self::from_raw(hi, lo))
387    }
388
389    /// Convert to dashed format.
390    ///
391    /// Returns a `Uuid` with default capacity.
392    #[inline]
393    pub fn to_dashed(&self) -> Uuid {
394        let (hi, lo) = self.decode();
395        Uuid::from_raw(hi, lo)
396    }
397
398    /// Check if this is the nil UUID.
399    ///
400    /// Compares raw bytes directly — no hex decoding needed.
401    #[inline]
402    pub fn is_nil(&self) -> bool {
403        self.0.as_bytes() == b"00000000000000000000000000000000"
404    }
405
406    /// Get the raw 128-bit value as big-endian bytes.
407    pub fn to_bytes(&self) -> [u8; 16] {
408        let (hi, lo) = self.decode();
409        let mut out = [0u8; 16];
410        out[..8].copy_from_slice(&hi.to_be_bytes());
411        out[8..].copy_from_slice(&lo.to_be_bytes());
412        out
413    }
414}
415
416impl<const CAP: usize> Deref for UuidCompact<CAP> {
417    type Target = str;
418
419    #[inline]
420    fn deref(&self) -> &str {
421        self.0.as_str()
422    }
423}
424
425impl<const CAP: usize> AsRef<str> for UuidCompact<CAP> {
426    #[inline]
427    fn as_ref(&self) -> &str {
428        self.0.as_str()
429    }
430}
431
432impl<const CAP: usize> fmt::Display for UuidCompact<CAP> {
433    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
434        f.write_str(self.0.as_str())
435    }
436}
437
438impl<const CAP: usize> fmt::Debug for UuidCompact<CAP> {
439    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
440        write!(f, "UuidCompact({})", self.0.as_str())
441    }
442}
443
444impl<const CAP: usize> Hash for UuidCompact<CAP> {
445    #[inline]
446    fn hash<H: Hasher>(&self, state: &mut H) {
447        self.0.hash(state);
448    }
449}
450
451impl<const CAP: usize> Ord for UuidCompact<CAP> {
452    #[inline]
453    fn cmp(&self, other: &Self) -> Ordering {
454        self.0.cmp(&other.0)
455    }
456}
457
458impl<const CAP: usize> PartialOrd for UuidCompact<CAP> {
459    #[inline]
460    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
461        Some(self.cmp(other))
462    }
463}
464
465impl<const CAP: usize> FromStr for UuidCompact<CAP> {
466    type Err = ParseError;
467
468    #[inline]
469    fn from_str(s: &str) -> Result<Self, Self::Err> {
470        Self::parse(s)
471    }
472}
473
474// ============================================================================
475// HexId64 - Hex-encoded u64
476// ============================================================================
477
478/// Hex-encoded 64-bit ID.
479///
480/// Format: 16 lowercase hex characters.
481///
482/// # Capacity
483///
484/// The default capacity is 16 bytes (minimum required). Use a larger capacity
485/// for fixed-size wire formats: `HexId64<32>`.
486#[derive(Clone, Copy, PartialEq, Eq)]
487#[repr(transparent)]
488pub struct HexId64<const CAP: usize = 16>(pub(crate) AsciiString<CAP>);
489
490impl<const CAP: usize> HexId64<CAP> {
491    /// Encode a u64 as hex.
492    #[inline]
493    pub fn encode(value: u64) -> Self {
494        Self(crate::encode::hex_u64(value))
495    }
496
497    #[inline]
498    pub fn as_str(&self) -> &str {
499        self.0.as_str()
500    }
501
502    #[inline]
503    pub fn as_bytes(&self) -> &[u8] {
504        self.0.as_bytes()
505    }
506
507    /// Decode back to u64.
508    pub fn decode(&self) -> u64 {
509        let bytes: &[u8; 16] = self.0.as_bytes().try_into().expect("16-byte hex string");
510        // SAFETY: Data was validated at construction; decode cannot fail.
511        unsafe { crate::simd::hex_decode_16(bytes).unwrap_unchecked() }
512    }
513
514    /// Parse a hex ID from a 16-character hex string. Case-insensitive.
515    pub fn parse(s: &str) -> Result<Self, ParseError> {
516        let bytes = s.as_bytes();
517        if bytes.len() != 16 {
518            return Err(ParseError::InvalidLength {
519                expected: 16,
520                got: bytes.len(),
521            });
522        }
523
524        // SIMD path: validates and decodes in a single pass
525        let hex_bytes: &[u8; 16] = bytes.try_into().expect("16-byte hex string");
526        let value =
527            crate::simd::hex_decode_16(hex_bytes).map_err(|pos| ParseError::InvalidChar {
528                position: pos,
529                byte: bytes[pos],
530            })?;
531
532        Ok(Self::encode(value))
533    }
534}
535
536impl<const CAP: usize> Deref for HexId64<CAP> {
537    type Target = str;
538
539    #[inline]
540    fn deref(&self) -> &str {
541        self.0.as_str()
542    }
543}
544
545impl<const CAP: usize> AsRef<str> for HexId64<CAP> {
546    #[inline]
547    fn as_ref(&self) -> &str {
548        self.0.as_str()
549    }
550}
551
552impl<const CAP: usize> fmt::Display for HexId64<CAP> {
553    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
554        f.write_str(self.0.as_str())
555    }
556}
557
558impl<const CAP: usize> fmt::Debug for HexId64<CAP> {
559    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
560        write!(f, "HexId64({})", self.0.as_str())
561    }
562}
563
564impl<const CAP: usize> Hash for HexId64<CAP> {
565    #[inline]
566    fn hash<H: Hasher>(&self, state: &mut H) {
567        self.0.hash(state);
568    }
569}
570
571impl<const CAP: usize> FromStr for HexId64<CAP> {
572    type Err = ParseError;
573
574    #[inline]
575    fn from_str(s: &str) -> Result<Self, Self::Err> {
576        Self::parse(s)
577    }
578}
579
580// ============================================================================
581// Base62Id - Base62-encoded u64
582// ============================================================================
583
584/// Base62-encoded 64-bit ID.
585///
586/// Format: 11 alphanumeric characters (0-9, A-Z, a-z).
587///
588/// # Capacity
589///
590/// The default capacity is 16 bytes (minimum required). Use a larger capacity
591/// for fixed-size wire formats: `Base62Id<32>`.
592#[derive(Clone, Copy, PartialEq, Eq)]
593#[repr(transparent)]
594pub struct Base62Id<const CAP: usize = 16>(pub(crate) AsciiString<CAP>);
595
596impl<const CAP: usize> Base62Id<CAP> {
597    /// Encode a u64 as base62.
598    #[inline]
599    pub fn encode(value: u64) -> Self {
600        Self(crate::encode::base62_u64(value))
601    }
602
603    #[inline]
604    pub fn as_str(&self) -> &str {
605        self.0.as_str()
606    }
607
608    #[inline]
609    pub fn as_bytes(&self) -> &[u8] {
610        self.0.as_bytes()
611    }
612
613    /// Decode back to u64.
614    pub fn decode(&self) -> u64 {
615        let bytes = self.0.as_bytes();
616        let mut value: u64 = 0;
617        for &b in bytes {
618            value = value * 62 + base62_digit(b) as u64;
619        }
620        value
621    }
622
623    /// Parse a base62 ID from an 11-character string.
624    pub fn parse(s: &str) -> Result<Self, DecodeError> {
625        let bytes = s.as_bytes();
626        if bytes.len() != 11 {
627            return Err(DecodeError::InvalidLength {
628                expected: 11,
629                got: bytes.len(),
630            });
631        }
632
633        let mut value: u64 = 0;
634        let mut i = 0;
635        while i < 11 {
636            let d = parse::validate_base62(bytes[i], i)?;
637            value = value
638                .checked_mul(62)
639                .and_then(|v| v.checked_add(d as u64))
640                .ok_or(DecodeError::Overflow)?;
641            i += 1;
642        }
643
644        Ok(Self::encode(value))
645    }
646}
647
648impl<const CAP: usize> Deref for Base62Id<CAP> {
649    type Target = str;
650
651    #[inline]
652    fn deref(&self) -> &str {
653        self.0.as_str()
654    }
655}
656
657impl<const CAP: usize> AsRef<str> for Base62Id<CAP> {
658    #[inline]
659    fn as_ref(&self) -> &str {
660        self.0.as_str()
661    }
662}
663
664impl<const CAP: usize> fmt::Display for Base62Id<CAP> {
665    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
666        f.write_str(self.0.as_str())
667    }
668}
669
670impl<const CAP: usize> fmt::Debug for Base62Id<CAP> {
671    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
672        write!(f, "Base62Id({})", self.0.as_str())
673    }
674}
675
676impl<const CAP: usize> Hash for Base62Id<CAP> {
677    #[inline]
678    fn hash<H: Hasher>(&self, state: &mut H) {
679        self.0.hash(state);
680    }
681}
682
683impl<const CAP: usize> FromStr for Base62Id<CAP> {
684    type Err = DecodeError;
685
686    #[inline]
687    fn from_str(s: &str) -> Result<Self, Self::Err> {
688        Self::parse(s)
689    }
690}
691
692// ============================================================================
693// Base36Id - Base36-encoded u64
694// ============================================================================
695
696/// Base36-encoded 64-bit ID.
697///
698/// Format: 13 alphanumeric characters (0-9, a-z), case-insensitive.
699///
700/// # Capacity
701///
702/// The default capacity is 16 bytes (minimum required). Use a larger capacity
703/// for fixed-size wire formats: `Base36Id<32>`.
704#[derive(Clone, Copy, PartialEq, Eq)]
705#[repr(transparent)]
706pub struct Base36Id<const CAP: usize = 16>(pub(crate) AsciiString<CAP>);
707
708impl<const CAP: usize> Base36Id<CAP> {
709    /// Encode a u64 as base36.
710    #[inline]
711    pub fn encode(value: u64) -> Self {
712        Self(crate::encode::base36_u64(value))
713    }
714
715    #[inline]
716    pub fn as_str(&self) -> &str {
717        self.0.as_str()
718    }
719
720    #[inline]
721    pub fn as_bytes(&self) -> &[u8] {
722        self.0.as_bytes()
723    }
724
725    /// Decode back to u64.
726    pub fn decode(&self) -> u64 {
727        let bytes = self.0.as_bytes();
728        let mut value: u64 = 0;
729        for &b in bytes {
730            value = value * 36 + base36_digit(b) as u64;
731        }
732        value
733    }
734
735    /// Parse a base36 ID from a 13-character string. Case-insensitive.
736    pub fn parse(s: &str) -> Result<Self, DecodeError> {
737        let bytes = s.as_bytes();
738        if bytes.len() != 13 {
739            return Err(DecodeError::InvalidLength {
740                expected: 13,
741                got: bytes.len(),
742            });
743        }
744
745        let mut value: u64 = 0;
746        let mut i = 0;
747        while i < 13 {
748            let d = parse::validate_base36(bytes[i], i)?;
749            value = value
750                .checked_mul(36)
751                .and_then(|v| v.checked_add(d as u64))
752                .ok_or(DecodeError::Overflow)?;
753            i += 1;
754        }
755
756        Ok(Self::encode(value))
757    }
758}
759
760impl<const CAP: usize> Deref for Base36Id<CAP> {
761    type Target = str;
762
763    #[inline]
764    fn deref(&self) -> &str {
765        self.0.as_str()
766    }
767}
768
769impl<const CAP: usize> AsRef<str> for Base36Id<CAP> {
770    #[inline]
771    fn as_ref(&self) -> &str {
772        self.0.as_str()
773    }
774}
775
776impl<const CAP: usize> fmt::Display for Base36Id<CAP> {
777    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
778        f.write_str(self.0.as_str())
779    }
780}
781
782impl<const CAP: usize> fmt::Debug for Base36Id<CAP> {
783    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
784        write!(f, "Base36Id({})", self.0.as_str())
785    }
786}
787
788impl<const CAP: usize> Hash for Base36Id<CAP> {
789    #[inline]
790    fn hash<H: Hasher>(&self, state: &mut H) {
791        self.0.hash(state);
792    }
793}
794
795impl<const CAP: usize> FromStr for Base36Id<CAP> {
796    type Err = DecodeError;
797
798    #[inline]
799    fn from_str(s: &str) -> Result<Self, Self::Err> {
800        Self::parse(s)
801    }
802}
803
804// ============================================================================
805// ULID
806// ============================================================================
807
808/// ULID (Universally Unique Lexicographically Sortable Identifier).
809///
810/// Format: 26 Crockford Base32 characters (128 bits total)
811/// - First 10 chars: 48-bit timestamp (milliseconds since Unix epoch)
812/// - Last 16 chars: 80 bits of randomness
813///
814/// ULIDs are lexicographically sortable and monotonically increasing.
815///
816/// # Capacity
817///
818/// The default capacity is 32 bytes (minimum required). Use a larger capacity
819/// for fixed-size wire formats: `Ulid<64>`.
820///
821/// # Example
822///
823/// ```rust
824/// use std::time::{Instant, SystemTime, UNIX_EPOCH};
825/// use nexus_id::ulid::UlidGenerator;
826///
827/// let epoch = Instant::now();
828/// let unix_base = SystemTime::now()
829///     .duration_since(UNIX_EPOCH)
830///     .unwrap()
831///     .as_millis() as u64;
832///
833/// let mut generator = UlidGenerator::new(epoch, unix_base, 12345);
834/// let id = generator.next(Instant::now());
835/// assert_eq!(id.len(), 26);
836/// ```
837#[derive(Clone, Copy, PartialEq, Eq)]
838#[repr(transparent)]
839pub struct Ulid<const CAP: usize = 32>(pub(crate) AsciiString<CAP>);
840
841impl<const CAP: usize> Ulid<CAP> {
842    /// Create a ULID from raw components.
843    ///
844    /// - `timestamp_ms`: 48-bit millisecond timestamp (upper bits ignored)
845    /// - `rand_hi`: upper 16 bits of the 80-bit random field
846    /// - `rand_lo`: lower 64 bits of the 80-bit random field
847    #[inline]
848    pub fn from_raw(timestamp_ms: u64, rand_hi: u16, rand_lo: u64) -> Self {
849        Self(crate::encode::ulid_encode(timestamp_ms, rand_hi, rand_lo))
850    }
851
852    /// Construct from a 16-byte big-endian binary representation.
853    ///
854    /// Layout: `[timestamp: 6 bytes][rand_hi: 2 bytes][rand_lo: 8 bytes]`
855    ///
856    /// This is the inverse of [`to_bytes()`](Self::to_bytes).
857    ///
858    /// # Errors
859    ///
860    /// Returns [`ParseError::InvalidLength`] if `bytes.len() != 16`.
861    pub fn from_bytes(bytes: &[u8]) -> Result<Self, ParseError> {
862        if bytes.len() != 16 {
863            return Err(ParseError::InvalidLength {
864                expected: 16,
865                got: bytes.len(),
866            });
867        }
868        // Timestamp: bytes 0-5 (48 bits, big-endian)
869        let mut ts_buf = [0u8; 8];
870        ts_buf[2..8].copy_from_slice(&bytes[0..6]);
871        let timestamp_ms = u64::from_be_bytes(ts_buf);
872
873        let rand_hi = u16::from_be_bytes(bytes[6..8].try_into().expect("2-byte slice"));
874        let rand_lo = u64::from_be_bytes(bytes[8..16].try_into().expect("8-byte slice"));
875
876        Ok(Self::from_raw(timestamp_ms, rand_hi, rand_lo))
877    }
878
879    /// Construct from a byte slice without length validation.
880    ///
881    /// # Safety
882    ///
883    /// The caller must guarantee that `bytes.len() >= 16`.
884    #[inline]
885    pub unsafe fn from_bytes_unchecked(bytes: &[u8]) -> Self {
886        debug_assert!(bytes.len() >= 16);
887        // SAFETY: caller guarantees bytes.len() >= 16
888        unsafe {
889            let mut ts_buf = [0u8; 8];
890            core::ptr::copy_nonoverlapping(bytes.as_ptr(), ts_buf.as_mut_ptr().add(2), 6);
891            let timestamp_ms = u64::from_be_bytes(ts_buf);
892
893            let rand_hi =
894                u16::from_be_bytes(bytes.get_unchecked(6..8).try_into().unwrap_unchecked());
895            let rand_lo =
896                u64::from_be_bytes(bytes.get_unchecked(8..16).try_into().unwrap_unchecked());
897
898            Self::from_raw(timestamp_ms, rand_hi, rand_lo)
899        }
900    }
901
902    #[inline]
903    pub fn as_str(&self) -> &str {
904        self.0.as_str()
905    }
906
907    #[inline]
908    pub fn as_bytes(&self) -> &[u8] {
909        self.0.as_bytes()
910    }
911
912    /// Extract the timestamp (milliseconds since Unix epoch).
913    pub fn timestamp_ms(&self) -> u64 {
914        let bytes = self.0.as_bytes();
915        let mut ts: u64 = 0;
916
917        // Decode first 10 characters (48 bits of timestamp)
918        // Char 0: 3 bits, Chars 1-9: 5 bits each = 3 + 45 = 48 bits
919        ts = (ts << 3) | crockford32_digit(bytes[0]) as u64;
920        for &b in &bytes[1..10] {
921            ts = (ts << 5) | crockford32_digit(b) as u64;
922        }
923
924        ts
925    }
926
927    /// Parse a ULID from a 26-character Crockford Base32 string.
928    ///
929    /// Case-insensitive. Accepts Crockford aliases (I/L → 1, O → 0).
930    pub fn parse(s: &str) -> Result<Self, ParseError> {
931        let bytes = s.as_bytes();
932        if bytes.len() != 26 {
933            return Err(ParseError::InvalidLength {
934                expected: 26,
935                got: bytes.len(),
936            });
937        }
938
939        // Single-pass: validate and decode simultaneously via lookup table.
940        // Decode timestamp (chars 0-9): 3 + 9×5 = 48 bits
941        // First char encodes only 3 bits — values > 7 overflow the 48-bit timestamp.
942        let first = parse::validate_crockford32(bytes[0], 0)?;
943        if first > 7 {
944            return Err(ParseError::InvalidChar {
945                position: 0,
946                byte: bytes[0],
947            });
948        }
949        let mut ts: u64 = first as u64;
950        let mut i = 1;
951        while i < 10 {
952            let d = parse::validate_crockford32(bytes[i], i)? as u64;
953            ts = (ts << 5) | d;
954            i += 1;
955        }
956
957        // Decode random (chars 10-25): 80 bits
958        let c10 = parse::validate_crockford32(bytes[10], 10)? as u16;
959        let c11 = parse::validate_crockford32(bytes[11], 11)? as u16;
960        let c12 = parse::validate_crockford32(bytes[12], 12)? as u16;
961        let c13 = parse::validate_crockford32(bytes[13], 13)? as u64;
962
963        let rand_hi = (c10 << 11) | (c11 << 6) | (c12 << 1) | ((c13 >> 4) as u16);
964
965        let mut rand_lo: u64 = c13 & 0x0F;
966        i = 14;
967        while i < 26 {
968            let d = parse::validate_crockford32(bytes[i], i)? as u64;
969            rand_lo = (rand_lo << 5) | d;
970            i += 1;
971        }
972
973        Ok(Self::from_raw(ts, rand_hi, rand_lo))
974    }
975
976    /// Check if this is a nil ULID (all zeros).
977    #[inline]
978    pub fn is_nil(&self) -> bool {
979        self.timestamp_ms() == 0 && {
980            let (hi, lo) = self.random();
981            hi == 0 && lo == 0
982        }
983    }
984
985    /// Convert to a UUID v7-compatible format.
986    ///
987    /// Maps the ULID's 128-bit value into UUID v7 layout, setting version (7) and
988    /// variant (RFC) bits.
989    ///
990    /// Returns a `Uuid` with default capacity.
991    ///
992    /// # Data Loss
993    ///
994    /// This conversion is **lossy**. ULID has 80 random bits, but UUID v7 reserves
995    /// 6 bits for version+variant, leaving only 74 bits for randomness. The bottom
996    /// 6 bits of `rand_lo` are discarded. The conversion is not reversible.
997    pub fn to_uuid(&self) -> Uuid {
998        let ts = self.timestamp_ms();
999        let (rand_hi, rand_lo) = self.random();
1000
1001        // Pack into UUID v7 layout
1002        // hi: [timestamp: 48][version=7: 4][rand_a: 12]
1003        let rand_a = (rand_hi >> 4) as u64; // top 12 bits of rand_hi
1004        let hi = (ts << 16) | (0x7 << 12) | (rand_a & 0xFFF);
1005
1006        // lo: [variant=10: 2][rand_b: 62]
1007        // Use remaining bits from rand_hi (4 bits) + rand_lo (64 bits) → take 62 bits
1008        let remaining = ((rand_hi as u64 & 0x0F) << 58) | (rand_lo >> 6);
1009        let lo = (0b10u64 << 62) | (remaining & 0x3FFF_FFFF_FFFF_FFFF);
1010
1011        Uuid::from_raw(hi, lo)
1012    }
1013
1014    /// Get the raw 128-bit value as big-endian bytes.
1015    pub fn to_bytes(&self) -> [u8; 16] {
1016        let ts = self.timestamp_ms();
1017        let (rand_hi, rand_lo) = self.random();
1018
1019        let mut out = [0u8; 16];
1020        // Timestamp in bytes 0-5 (48 bits, big-endian)
1021        let ts_bytes = ts.to_be_bytes();
1022        out[0..6].copy_from_slice(&ts_bytes[2..8]);
1023        // Random hi in bytes 6-7 (16 bits)
1024        out[6..8].copy_from_slice(&rand_hi.to_be_bytes());
1025        // Random lo in bytes 8-15 (64 bits)
1026        out[8..16].copy_from_slice(&rand_lo.to_be_bytes());
1027        out
1028    }
1029
1030    /// Decode the random portion as (hi: u16, lo: u64).
1031    pub fn random(&self) -> (u16, u64) {
1032        let bytes = self.0.as_bytes();
1033
1034        // Chars 10-13 contain rand_hi (16 bits) spread across boundaries
1035        // Char 10: bits 11-15 of rand_hi (5 bits)
1036        // Char 11: bits 6-10 of rand_hi (5 bits)
1037        // Char 12: bits 1-5 of rand_hi (5 bits)
1038        // Char 13: bit 0 of rand_hi (1 bit) + bits 60-63 of rand_lo (4 bits)
1039
1040        let c10 = crockford32_digit(bytes[10]) as u16;
1041        let c11 = crockford32_digit(bytes[11]) as u16;
1042        let c12 = crockford32_digit(bytes[12]) as u16;
1043        let c13 = crockford32_digit(bytes[13]) as u64;
1044
1045        let rand_hi = (c10 << 11) | (c11 << 6) | (c12 << 1) | ((c13 >> 4) as u16);
1046
1047        // Chars 13-25 contain rand_lo (64 bits)
1048        // Char 13 contributes 4 bits (already extracted above for rand_hi)
1049        let mut rand_lo: u64 = c13 & 0x0F;
1050        for &b in &bytes[14..26] {
1051            rand_lo = (rand_lo << 5) | crockford32_digit(b) as u64;
1052        }
1053
1054        (rand_hi, rand_lo)
1055    }
1056}
1057
1058impl<const CAP: usize> Deref for Ulid<CAP> {
1059    type Target = str;
1060
1061    #[inline]
1062    fn deref(&self) -> &str {
1063        self.0.as_str()
1064    }
1065}
1066
1067impl<const CAP: usize> AsRef<str> for Ulid<CAP> {
1068    #[inline]
1069    fn as_ref(&self) -> &str {
1070        self.0.as_str()
1071    }
1072}
1073
1074impl<const CAP: usize> fmt::Display for Ulid<CAP> {
1075    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1076        f.write_str(self.0.as_str())
1077    }
1078}
1079
1080impl<const CAP: usize> fmt::Debug for Ulid<CAP> {
1081    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1082        write!(f, "Ulid({})", self.0.as_str())
1083    }
1084}
1085
1086impl<const CAP: usize> Hash for Ulid<CAP> {
1087    #[inline]
1088    fn hash<H: Hasher>(&self, state: &mut H) {
1089        self.0.hash(state);
1090    }
1091}
1092
1093impl<const CAP: usize> Ord for Ulid<CAP> {
1094    #[inline]
1095    fn cmp(&self, other: &Self) -> Ordering {
1096        // Lexicographic order = time order (timestamp in MSB chars)
1097        self.0.cmp(&other.0)
1098    }
1099}
1100
1101impl<const CAP: usize> PartialOrd for Ulid<CAP> {
1102    #[inline]
1103    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
1104        Some(self.cmp(other))
1105    }
1106}
1107
1108impl<const CAP: usize> FromStr for Ulid<CAP> {
1109    type Err = ParseError;
1110
1111    #[inline]
1112    fn from_str(s: &str) -> Result<Self, Self::Err> {
1113        Self::parse(s)
1114    }
1115}
1116
1117// ============================================================================
1118// Cross-type From impls
1119// ============================================================================
1120
1121impl<const CAP: usize> From<Uuid<CAP>> for UuidCompact {
1122    /// Lossless conversion: strip dashes.
1123    #[inline]
1124    fn from(u: Uuid<CAP>) -> Self {
1125        u.to_compact()
1126    }
1127}
1128
1129impl<const CAP: usize> From<UuidCompact<CAP>> for Uuid {
1130    /// Lossless conversion: add dashes.
1131    #[inline]
1132    fn from(u: UuidCompact<CAP>) -> Self {
1133        u.to_dashed()
1134    }
1135}
1136
1137impl<const CAP: usize> From<Ulid<CAP>> for Uuid {
1138    /// Convert ULID to UUID v7 format (sets version and variant bits).
1139    ///
1140    /// **Lossy**: 6 bits of randomness are discarded. See [`Ulid::to_uuid()`].
1141    #[inline]
1142    fn from(u: Ulid<CAP>) -> Self {
1143        u.to_uuid()
1144    }
1145}
1146
1147// ============================================================================
1148// Helper functions
1149// ============================================================================
1150
1151/// Convert Crockford Base32 character to value (0-31) via lookup table.
1152/// For already-validated data (from our own encode output).
1153#[inline]
1154fn crockford32_digit(b: u8) -> u8 {
1155    parse::CROCKFORD32_DECODE[b as usize]
1156}
1157
1158/// Convert hex character to value (0-15).
1159#[inline]
1160const fn hex_digit(b: u8) -> u8 {
1161    match b {
1162        b'0'..=b'9' => b - b'0',
1163        b'a'..=b'f' => b - b'a' + 10,
1164        b'A'..=b'F' => b - b'A' + 10,
1165        _ => 0, // Should never happen for valid IDs
1166    }
1167}
1168
1169/// Convert base62 character to value (0-61).
1170#[inline]
1171const fn base62_digit(b: u8) -> u8 {
1172    match b {
1173        b'0'..=b'9' => b - b'0',
1174        b'A'..=b'Z' => b - b'A' + 10,
1175        b'a'..=b'z' => b - b'a' + 36,
1176        _ => 0,
1177    }
1178}
1179
1180/// Convert base36 character to value (0-35).
1181#[inline]
1182const fn base36_digit(b: u8) -> u8 {
1183    match b {
1184        b'0'..=b'9' => b - b'0',
1185        b'a'..=b'z' => b - b'a' + 10,
1186        b'A'..=b'Z' => b - b'A' + 10, // Case insensitive
1187        _ => 0,
1188    }
1189}
1190
1191#[cfg(all(test, feature = "std"))]
1192mod tests {
1193    use super::*;
1194
1195    #[test]
1196    fn uuid_decode_roundtrip() {
1197        let hi = 0x0123_4567_89AB_CDEF_u64;
1198        let lo = 0xFEDC_BA98_7654_3210_u64;
1199
1200        let uuid: Uuid = Uuid::from_raw(hi, lo);
1201        let (decoded_hi, decoded_lo) = uuid.decode();
1202
1203        assert_eq!(hi, decoded_hi);
1204        assert_eq!(lo, decoded_lo);
1205    }
1206
1207    #[test]
1208    fn uuid_larger_cap() {
1209        let hi = 0x0123_4567_89AB_CDEF_u64;
1210        let lo = 0xFEDC_BA98_7654_3210_u64;
1211
1212        let small: Uuid<40> = Uuid::from_raw(hi, lo);
1213        let large: Uuid<64> = Uuid::from_raw(hi, lo);
1214
1215        assert_eq!(small.as_str(), large.as_str());
1216        assert_eq!(small.decode(), large.decode());
1217    }
1218
1219    #[test]
1220    fn uuid_compact_decode_roundtrip() {
1221        let hi = 0x0123_4567_89AB_CDEF_u64;
1222        let lo = 0xFEDC_BA98_7654_3210_u64;
1223
1224        let uuid: UuidCompact = UuidCompact::from_raw(hi, lo);
1225        let (decoded_hi, decoded_lo) = uuid.decode();
1226
1227        assert_eq!(hi, decoded_hi);
1228        assert_eq!(lo, decoded_lo);
1229    }
1230
1231    #[test]
1232    fn hex_id64_decode_roundtrip() {
1233        for value in [0, 1, 12345, u64::MAX, 0xDEAD_BEEF_CAFE_BABE] {
1234            let id: HexId64 = HexId64::encode(value);
1235            assert_eq!(id.decode(), value);
1236        }
1237    }
1238
1239    #[test]
1240    fn hex_id64_larger_cap() {
1241        let id_small: HexId64<16> = HexId64::encode(12345);
1242        let id_large: HexId64<32> = HexId64::encode(12345);
1243        assert_eq!(id_small.as_str(), id_large.as_str());
1244    }
1245
1246    #[test]
1247    fn base62_id_decode_roundtrip() {
1248        for value in [0, 1, 12345, u64::MAX] {
1249            let id: Base62Id = Base62Id::encode(value);
1250            assert_eq!(id.decode(), value);
1251        }
1252    }
1253
1254    #[test]
1255    fn base62_id_larger_cap() {
1256        let id_small: Base62Id<16> = Base62Id::encode(12345);
1257        let id_large: Base62Id<32> = Base62Id::encode(12345);
1258        assert_eq!(id_small.as_str(), id_large.as_str());
1259    }
1260
1261    #[test]
1262    fn base36_id_decode_roundtrip() {
1263        for value in [0, 1, 12345, u64::MAX] {
1264            let id: Base36Id = Base36Id::encode(value);
1265            assert_eq!(id.decode(), value);
1266        }
1267    }
1268
1269    #[test]
1270    fn base36_id_larger_cap() {
1271        let id_small: Base36Id<16> = Base36Id::encode(12345);
1272        let id_large: Base36Id<32> = Base36Id::encode(12345);
1273        assert_eq!(id_small.as_str(), id_large.as_str());
1274    }
1275
1276    #[test]
1277    fn ulid_larger_cap() {
1278        let small: Ulid<32> = Ulid::from_raw(1_700_000_000_000, 0x1234, 0xDEAD_BEEF);
1279        let large: Ulid<64> = Ulid::from_raw(1_700_000_000_000, 0x1234, 0xDEAD_BEEF);
1280        assert_eq!(small.as_str(), large.as_str());
1281        assert_eq!(small.timestamp_ms(), large.timestamp_ms());
1282    }
1283
1284    #[test]
1285    fn uuid_version() {
1286        // V4 UUID
1287        let hi = 0x0123_4567_89AB_4DEF_u64; // version 4 at position
1288        let lo = 0x8EDC_BA98_7654_3210_u64;
1289        let uuid: Uuid = Uuid::from_raw(hi, lo);
1290        assert_eq!(uuid.version(), 4);
1291
1292        // V7 UUID
1293        let hi = 0x0123_4567_89AB_7DEF_u64; // version 7 at position
1294        let uuid: Uuid = Uuid::from_raw(hi, lo);
1295        assert_eq!(uuid.version(), 7);
1296    }
1297
1298    #[test]
1299    fn display_works() {
1300        let uuid: Uuid = Uuid::from_raw(0x0123_4567_89AB_CDEF, 0xFEDC_BA98_7654_3210);
1301        let s = format!("{}", uuid);
1302        assert_eq!(s, "01234567-89ab-cdef-fedc-ba9876543210");
1303    }
1304
1305    #[test]
1306    fn deref_works() {
1307        let uuid: Uuid = Uuid::from_raw(0x0123_4567_89AB_CDEF, 0xFEDC_BA98_7654_3210);
1308        let s: &str = &uuid;
1309        assert_eq!(s, "01234567-89ab-cdef-fedc-ba9876543210");
1310    }
1311
1312    #[test]
1313    fn uuid_from_bytes_roundtrip() {
1314        let original: Uuid = Uuid::from_raw(0x0123_4567_89AB_CDEF, 0xFEDC_BA98_7654_3210);
1315        let bytes = original.to_bytes();
1316        let recovered: Uuid = Uuid::from_bytes(&bytes).unwrap();
1317        assert_eq!(original, recovered);
1318    }
1319
1320    #[test]
1321    fn uuid_from_bytes_wrong_length() {
1322        assert!(Uuid::<40>::from_bytes(&[0u8; 15]).is_err());
1323        assert!(Uuid::<40>::from_bytes(&[0u8; 17]).is_err());
1324        assert!(Uuid::<40>::from_bytes(&[]).is_err());
1325    }
1326
1327    #[test]
1328    fn uuid_from_bytes_unchecked_roundtrip() {
1329        let original: Uuid = Uuid::from_raw(0xDEAD_BEEF_CAFE_BABE, 0x0123_4567_89AB_CDEF);
1330        let bytes = original.to_bytes();
1331        let recovered: Uuid = unsafe { Uuid::from_bytes_unchecked(&bytes) };
1332        assert_eq!(original, recovered);
1333    }
1334
1335    #[test]
1336    fn uuid_compact_from_bytes_roundtrip() {
1337        let original: UuidCompact =
1338            UuidCompact::from_raw(0x0123_4567_89AB_CDEF, 0xFEDC_BA98_7654_3210);
1339        let bytes = original.to_bytes();
1340        let recovered: UuidCompact = UuidCompact::from_bytes(&bytes).unwrap();
1341        assert_eq!(original, recovered);
1342    }
1343
1344    #[test]
1345    fn uuid_compact_from_bytes_wrong_length() {
1346        assert!(UuidCompact::<32>::from_bytes(&[0u8; 15]).is_err());
1347        assert!(UuidCompact::<32>::from_bytes(&[0u8; 17]).is_err());
1348    }
1349
1350    #[test]
1351    fn ulid_from_bytes_roundtrip() {
1352        let original: Ulid = Ulid::from_raw(1_700_000_000_000, 0xABCD, 0xDEAD_BEEF_CAFE_BABE);
1353        let bytes = original.to_bytes();
1354        let recovered: Ulid = Ulid::from_bytes(&bytes).unwrap();
1355        assert_eq!(original.timestamp_ms(), recovered.timestamp_ms());
1356        assert_eq!(original.random(), recovered.random());
1357        assert_eq!(original, recovered);
1358    }
1359
1360    #[test]
1361    fn ulid_from_bytes_wrong_length() {
1362        assert!(Ulid::<32>::from_bytes(&[0u8; 15]).is_err());
1363        assert!(Ulid::<32>::from_bytes(&[0u8; 17]).is_err());
1364    }
1365
1366    #[test]
1367    fn ulid_from_bytes_unchecked_roundtrip() {
1368        let original: Ulid = Ulid::from_raw(1_700_000_000_000, 0x1234, 0x0123_4567_89AB_CDEF);
1369        let bytes = original.to_bytes();
1370        let recovered: Ulid = unsafe { Ulid::from_bytes_unchecked(&bytes) };
1371        assert_eq!(original, recovered);
1372    }
1373
1374    #[test]
1375    fn ulid_parse_rejects_overflow_first_char() {
1376        // First char value > 7 overflows the 48-bit timestamp.
1377        // '8' decodes to value 8 in Crockford Base32.
1378        let overflow = "80000000000000000000000000";
1379        assert!(Ulid::<32>::parse(overflow).is_err());
1380
1381        // 'Z' decodes to value 31 — also invalid.
1382        let z_first = "Z0000000000000000000000000";
1383        assert!(Ulid::<32>::parse(z_first).is_err());
1384
1385        // '7' (value 7) is the max valid first char.
1386        let max_valid = "70000000000000000000000000";
1387        assert!(Ulid::<32>::parse(max_valid).is_ok());
1388    }
1389
1390    // ====================================================================
1391    // Overflow detection
1392    // ====================================================================
1393
1394    #[test]
1395    fn base62_parse_overflow() {
1396        use crate::parse::DecodeError;
1397
1398        // "zzzzzzzzzzz" (11 z's) = 61*(62^10 + 62^9 + ... + 1) > u64::MAX
1399        let result = Base62Id::<16>::parse("zzzzzzzzzzz");
1400        assert_eq!(result, Err(DecodeError::Overflow));
1401
1402        // u64::MAX encodes to "LygHa16AHYF" — should round-trip
1403        let max_id: Base62Id = Base62Id::encode(u64::MAX);
1404        let parsed = Base62Id::<16>::parse(max_id.as_str()).unwrap();
1405        assert_eq!(parsed.decode(), u64::MAX);
1406    }
1407
1408    #[test]
1409    fn base36_parse_overflow() {
1410        use crate::parse::DecodeError;
1411
1412        // "zzzzzzzzzzzzz" (13 z's) = 35*(36^12 + ...) > u64::MAX
1413        let result = Base36Id::<16>::parse("zzzzzzzzzzzzz");
1414        assert_eq!(result, Err(DecodeError::Overflow));
1415
1416        // u64::MAX should round-trip
1417        let max_id: Base36Id = Base36Id::encode(u64::MAX);
1418        let parsed = Base36Id::<16>::parse(max_id.as_str()).unwrap();
1419        assert_eq!(parsed.decode(), u64::MAX);
1420    }
1421
1422    // ====================================================================
1423    // Uuid::parse negative cases
1424    // ====================================================================
1425
1426    #[test]
1427    fn uuid_parse_wrong_length() {
1428        use crate::parse::UuidParseError;
1429
1430        let result = Uuid::<40>::parse("01234567-89ab-cdef-fedc-ba987654321"); // 35 chars
1431        assert!(matches!(result, Err(UuidParseError::InvalidLength { .. })));
1432
1433        let result = Uuid::<40>::parse("01234567-89ab-cdef-fedc-ba98765432100"); // 37 chars
1434        assert!(matches!(result, Err(UuidParseError::InvalidLength { .. })));
1435
1436        let result = Uuid::<40>::parse("");
1437        assert!(matches!(result, Err(UuidParseError::InvalidLength { .. })));
1438    }
1439
1440    #[test]
1441    fn uuid_parse_bad_dashes() {
1442        use crate::parse::UuidParseError;
1443
1444        // Missing dash at position 8
1445        let result = Uuid::<40>::parse("01234567089ab-cdef-fedc-ba9876543210");
1446        assert!(matches!(result, Err(UuidParseError::InvalidFormat)));
1447
1448        // Missing dash at position 13
1449        let result = Uuid::<40>::parse("01234567-89ab0cdef-fedc-ba9876543210");
1450        assert!(matches!(result, Err(UuidParseError::InvalidFormat)));
1451
1452        // Missing dash at position 18
1453        let result = Uuid::<40>::parse("01234567-89ab-cdef0fedc-ba9876543210");
1454        assert!(matches!(result, Err(UuidParseError::InvalidFormat)));
1455
1456        // Missing dash at position 23
1457        let result = Uuid::<40>::parse("01234567-89ab-cdef-fedc0ba9876543210");
1458        assert!(matches!(result, Err(UuidParseError::InvalidFormat)));
1459    }
1460
1461    #[test]
1462    fn uuid_parse_invalid_hex_char() {
1463        use crate::parse::UuidParseError;
1464
1465        // 'g' is not a valid hex character
1466        let result = Uuid::<40>::parse("g1234567-89ab-cdef-fedc-ba9876543210");
1467        assert!(matches!(
1468            result,
1469            Err(UuidParseError::InvalidChar { position: 0, .. })
1470        ));
1471
1472        // Invalid in the middle segment
1473        let result = Uuid::<40>::parse("01234567-89xb-cdef-fedc-ba9876543210");
1474        assert!(matches!(
1475            result,
1476            Err(UuidParseError::InvalidChar { position: 11, .. })
1477        ));
1478    }
1479
1480    // ====================================================================
1481    // is_nil() tests
1482    // ====================================================================
1483
1484    #[test]
1485    fn uuid_is_nil() {
1486        let nil: Uuid = Uuid::from_raw(0, 0);
1487        assert!(nil.is_nil());
1488
1489        let not_nil: Uuid = Uuid::from_raw(0, 1);
1490        assert!(!not_nil.is_nil());
1491
1492        let not_nil: Uuid = Uuid::from_raw(1, 0);
1493        assert!(!not_nil.is_nil());
1494    }
1495
1496    #[test]
1497    fn uuid_compact_is_nil() {
1498        let nil: UuidCompact = UuidCompact::from_raw(0, 0);
1499        assert!(nil.is_nil());
1500
1501        let not_nil: UuidCompact = UuidCompact::from_raw(0, 1);
1502        assert!(!not_nil.is_nil());
1503    }
1504
1505    #[test]
1506    fn ulid_is_nil() {
1507        let nil: Ulid = Ulid::from_raw(0, 0, 0);
1508        assert!(nil.is_nil());
1509
1510        // Non-zero timestamp
1511        let not_nil: Ulid = Ulid::from_raw(1, 0, 0);
1512        assert!(!not_nil.is_nil());
1513
1514        // Non-zero rand_hi
1515        let not_nil: Ulid = Ulid::from_raw(0, 1, 0);
1516        assert!(!not_nil.is_nil());
1517
1518        // Non-zero rand_lo
1519        let not_nil: Ulid = Ulid::from_raw(0, 0, 1);
1520        assert!(!not_nil.is_nil());
1521    }
1522
1523    // ====================================================================
1524    // Crockford Base32 alias handling
1525    // ====================================================================
1526
1527    #[test]
1528    fn ulid_parse_crockford_aliases() {
1529        // Crockford spec: I/i/L/l → 1, O/o → 0
1530        // "01" prefix with aliases should parse the same as canonical "01"
1531        let canonical: Ulid = Ulid::parse("01000000000000000000000000").unwrap();
1532
1533        // 'O' → 0 (alias for zero)
1534        let with_o: Ulid = Ulid::parse("O1000000000000000000000000").unwrap();
1535        assert_eq!(canonical, with_o);
1536
1537        // 'I' → 1
1538        let with_i: Ulid = Ulid::parse("0I000000000000000000000000").unwrap();
1539        assert_eq!(canonical, with_i);
1540
1541        // 'L' → 1
1542        let with_l: Ulid = Ulid::parse("0L000000000000000000000000").unwrap();
1543        assert_eq!(canonical, with_l);
1544
1545        // 'i' → 1 (lowercase)
1546        let with_i_lower: Ulid = Ulid::parse("0i000000000000000000000000").unwrap();
1547        assert_eq!(canonical, with_i_lower);
1548
1549        // 'o' → 0 (lowercase)
1550        let with_o_lower: Ulid = Ulid::parse("o1000000000000000000000000").unwrap();
1551        assert_eq!(canonical, with_o_lower);
1552
1553        // 'l' → 1 (lowercase)
1554        let with_l_lower: Ulid = Ulid::parse("0l000000000000000000000000").unwrap();
1555        assert_eq!(canonical, with_l_lower);
1556    }
1557
1558    // ====================================================================
1559    // Ulid::to_uuid() lossy conversion
1560    // ====================================================================
1561
1562    #[test]
1563    fn ulid_to_uuid_preserves_timestamp() {
1564        let ts = 1_700_000_000_000u64;
1565        let ulid: Ulid = Ulid::from_raw(ts, 0x1234, 0xDEAD_BEEF_CAFE_BABE);
1566        let uuid = ulid.to_uuid();
1567
1568        // Version should be 7
1569        assert_eq!(uuid.version(), 7);
1570
1571        // Timestamp should survive the conversion (top 48 bits of hi)
1572        let (hi, _) = uuid.decode();
1573        let extracted_ts = hi >> 16;
1574        assert_eq!(extracted_ts, ts);
1575    }
1576
1577    #[test]
1578    fn ulid_to_uuid_is_lossy() {
1579        // Two ULIDs that differ only in the bottom 6 bits of rand_lo
1580        // should map to the same UUID (those bits are discarded).
1581        let ulid_a: Ulid = Ulid::from_raw(1_700_000_000_000, 0x1234, 0xDEAD_BEEF_CAFE_BA00);
1582        let ulid_b: Ulid = Ulid::from_raw(1_700_000_000_000, 0x1234, 0xDEAD_BEEF_CAFE_BA3F);
1583
1584        // Different ULIDs
1585        assert_ne!(ulid_a, ulid_b);
1586
1587        // Same UUID (bottom 6 bits lost)
1588        assert_eq!(ulid_a.to_uuid(), ulid_b.to_uuid());
1589    }
1590
1591    #[test]
1592    fn ulid_to_uuid_sets_variant_bits() {
1593        let ulid: Ulid = Ulid::from_raw(1_700_000_000_000, 0xFFFF, u64::MAX);
1594        let uuid = ulid.to_uuid();
1595        let (_, lo) = uuid.decode();
1596
1597        // Top 2 bits of lo must be 0b10 (RFC variant)
1598        assert_eq!(lo >> 62, 0b10);
1599    }
1600}