tnid/
uuidlike.rs

1/// A wrapper for 128-bit values that may or may not be valid TNIDs.
2///
3/// This type provides a way to work with 128-bit UUID-like values without the strict
4/// validation that [`Tnid`](crate::Tnid) requires. Unlike [`Tnid`](crate::Tnid), which
5/// only accepts values that conform to the TNID specification (correct UUIDv8 version/variant
6/// bits and valid name encoding), `UUIDLike` accepts any 128-bit value.
7///
8/// This makes `UUIDLike` useful for:
9/// - Inspecting potentially invalid TNIDs to understand why they don't parse
10/// - Converting between different UUID representations (u128, hex strings) without validation
11/// - Working with UUIDs from external systems that may not be TNIDs
12/// - Debugging and troubleshooting TNID-related issues
13///
14/// # Examples
15///
16/// Basic usage:
17/// ```rust
18/// use tnid::UUIDLike;
19///
20/// // Create from any 128-bit value
21/// let uuid_like = UUIDLike::new(0x12345678_1234_1234_1234_123456789abc);
22///
23/// // Convert to different representations
24/// let as_u128 = uuid_like.as_u128();
25/// let as_string = uuid_like.to_uuid_string_cased(false);
26/// ```
27///
28/// Inspecting potentially invalid TNIDs:
29/// ```rust
30/// use tnid::{UUIDLike, Tnid, TnidName, NameStr};
31///
32/// struct User;
33/// impl TnidName for User {
34///     const ID_NAME: NameStr<'static> = NameStr::new_const("user");
35/// }
36///
37/// // Parse a UUID string that might not be a valid TNID
38/// let uuid_str = "cab1952a-f09d-86d9-928e-96ea03dc6af3";
39/// let uuid_like = UUIDLike::parse_uuid_string(uuid_str).unwrap();
40///
41/// // Try to convert to TNID - this performs validation
42/// match Tnid::<User>::from_u128(uuid_like.as_u128()) {
43///     Some(tnid) => println!("Valid TNID: {}", tnid),
44///     None => println!("Not a valid TNID (wrong version/variant/name)"),
45/// }
46/// ```
47#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
48pub struct UUIDLike(u128);
49
50impl std::fmt::Debug for UUIDLike {
51    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52        write!(f, "{}", self.to_uuid_string_cased(false))
53    }
54}
55
56impl UUIDLike {
57    /// Returns the raw 128-bit value.
58    ///
59    /// # Examples
60    ///
61    /// ```rust
62    /// use tnid::UUIDLike;
63    ///
64    /// let uuid_like = UUIDLike::new(0x12345678_1234_1234_1234_123456789abc);
65    /// assert_eq!(uuid_like.as_u128(), 0x12345678_1234_1234_1234_123456789abc);
66    /// ```
67    pub fn as_u128(&self) -> u128 {
68        self.0
69    }
70
71    /// Creates a new `UUIDLike` from a 128-bit value.
72    ///
73    /// Accepts any `u128` value without validation.
74    ///
75    /// # Examples
76    ///
77    /// ```rust
78    /// use tnid::UUIDLike;
79    ///
80    /// let uuid_like = UUIDLike::new(0x12345678_1234_1234_1234_123456789abc);
81    /// assert_eq!(uuid_like.as_u128(), 0x12345678_1234_1234_1234_123456789abc);
82    /// ```
83    pub fn new(id: u128) -> Self {
84        Self(id)
85    }
86
87    /// Converts to UUID hex string format with specified case.
88    ///
89    /// Produces the standard UUID format: `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`
90    ///
91    /// # Parameters
92    ///
93    /// - `uppercase`: If `true`, uses uppercase hex digits (A-F). If `false`, uses lowercase (a-f).
94    ///
95    /// # Examples
96    ///
97    /// ```rust
98    /// use tnid::UUIDLike;
99    ///
100    /// let uuid_like = UUIDLike::new(0xCAB1952A_F09D_86D9_928E_96EA03DC6AF3);
101    ///
102    /// let lowercase = uuid_like.to_uuid_string_cased(false);
103    /// assert_eq!(lowercase, "cab1952a-f09d-86d9-928e-96ea03dc6af3");
104    ///
105    /// let uppercase = uuid_like.to_uuid_string_cased(true);
106    /// assert_eq!(uppercase, "CAB1952A-F09D-86D9-928E-96EA03DC6AF3");
107    /// ```
108    pub fn to_uuid_string_cased(&self, uppercase: bool) -> String {
109        crate::utils::u128_to_uuid_string(self.0, uppercase)
110    }
111
112    /// Parses a UUID hex string into a `UUIDLike`.
113    ///
114    /// Accepts the standard UUID format: `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`
115    ///
116    /// Accepts both uppercase and lowercase hex digits. Validates format but not TNID-specific requirements.
117    ///
118    /// Returns `None` if the string is not a valid UUID hex string.
119    ///
120    /// # Examples
121    ///
122    /// ```rust
123    /// use tnid::UUIDLike;
124    ///
125    /// // Parse lowercase
126    /// let uuid = UUIDLike::parse_uuid_string("cab1952a-f09d-86d9-928e-96ea03dc6af3");
127    /// assert!(uuid.is_some());
128    ///
129    /// // Parse uppercase
130    /// let uuid = UUIDLike::parse_uuid_string("CAB1952A-F09D-86D9-928E-96EA03DC6AF3");
131    /// assert!(uuid.is_some());
132    ///
133    /// // Parse mixed case
134    /// let uuid = UUIDLike::parse_uuid_string("CaB1952a-F09D-86d9-928E-96ea03dc6af3");
135    /// assert!(uuid.is_some());
136    ///
137    /// // Invalid format
138    /// assert!(UUIDLike::parse_uuid_string("not-a-uuid").is_none());
139    /// ```
140    pub fn parse_uuid_string(uuid_string: &str) -> Option<Self> {
141        if uuid_string.len() != 36 {
142            return None;
143        }
144
145        let bytes = uuid_string.as_bytes();
146        if bytes.get(8) != Some(&b'-')
147            || bytes.get(13) != Some(&b'-')
148            || bytes.get(18) != Some(&b'-')
149            || bytes.get(23) != Some(&b'-')
150        {
151            return None;
152        }
153
154        // the from_str_radix below should also check that chars are hex digits, so this is redundant, but included for easier debugging
155        #[cfg(debug_assertions)]
156        for (i, c) in uuid_string.chars().enumerate() {
157            if i == 8 || i == 13 || i == 18 || i == 23 {
158                if c != '-' {
159                    return None;
160                }
161            } else if !c.is_ascii_hexdigit() {
162                return None;
163            }
164        }
165
166        // parse 5 hyphen-separated sections as hex
167        let s1 = u32::from_str_radix(&uuid_string[0..8], 16).ok()?;
168        let s2 = u16::from_str_radix(&uuid_string[9..13], 16).ok()?;
169        let s3 = u16::from_str_radix(&uuid_string[14..18], 16).ok()?;
170        let s4 = u16::from_str_radix(&uuid_string[19..23], 16).ok()?;
171        let s5 = u64::from_str_radix(&uuid_string[24..36], 16).ok()?;
172
173        // Combine sections into u128 (reverse of to_uuid_string_cased)
174        let id = ((s1 as u128) << 96)
175            | ((s2 as u128) << 80)
176            | ((s3 as u128) << 64)
177            | ((s4 as u128) << 48)
178            | (s5 as u128);
179
180        Some(Self(id))
181    }
182}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187
188    #[test]
189    fn parse_lowercase() {
190        let result = UUIDLike::parse_uuid_string("ffffffff-ffff-ffff-ffff-ffffffffffff");
191        assert_eq!(result.unwrap().as_u128(), u128::MAX);
192    }
193
194    #[test]
195    fn parse_uppercase() {
196        let result = UUIDLike::parse_uuid_string("FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF");
197        assert_eq!(result.unwrap().as_u128(), u128::MAX);
198    }
199
200    #[test]
201    fn parse_mixed_case() {
202        let result = UUIDLike::parse_uuid_string("AaBbCcDd-1234-5678-90aB-cDeF01234567");
203        assert!(result.is_some());
204    }
205
206    #[test]
207    fn parse_all_zeros() {
208        let result = UUIDLike::parse_uuid_string("00000000-0000-0000-0000-000000000000");
209        assert_eq!(result.unwrap().as_u128(), 0);
210    }
211}