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}