Skip to main content

ps_uuid/
macros.rs

1//! Compile-time UUID parsing macro.
2
3/// Parse a UUID from a string literal at compile time.
4///
5/// Accepts hyphenated, simple, braced, and URN formats.
6///
7/// # Examples
8///
9/// ```
10/// use ps_uuid::{uuid, UUID};
11///
12/// const DNS_NAMESPACE: UUID = uuid!("6ba7b810-9dad-11d1-80b4-00c04fd430c8");
13/// const SIMPLE: UUID = uuid!("550e8400e29b41d4a716446655440000");
14/// const BRACED: UUID = uuid!("{550e8400-e29b-41d4-a716-446655440000}");
15/// ```
16///
17/// # Compile-time Errors
18///
19/// Invalid UUIDs will cause a compile-time error:
20///
21/// ```compile_fail
22/// use ps_uuid::uuid;
23/// const BAD: ps_uuid::UUID = uuid!("not-a-uuid");
24/// ```
25#[macro_export]
26macro_rules! uuid {
27    ($s:literal) => {{
28        const BYTES: [u8; 16] = $crate::UUID::parse_const($s);
29        $crate::UUID::from_bytes(BYTES)
30    }};
31}
32
33use crate::UUID;
34
35impl UUID {
36    /// Parse a UUID string at compile time.
37    ///
38    /// This is a `const fn` used by the [`uuid!`] macro. Prefer using the macro
39    /// for better error messages.
40    ///
41    /// # Panics
42    ///
43    /// Panics at compile time if the string is not a valid UUID.
44    #[must_use]
45    pub const fn parse_const(s: &str) -> [u8; 16] {
46        let s = s.as_bytes();
47        let (start, end) = find_uuid_bounds(s);
48        let len = end - start;
49
50        let expect_hyphens = match len {
51            32 => false,
52            36 => true,
53            _ => panic!("UUID string must be 32 or 36 characters"),
54        };
55
56        let mut bytes = [0u8; 16];
57        let mut byte_idx = 0;
58        let mut i = start;
59        let mut pos = 0; // position within the UUID portion
60
61        while i < end {
62            let c = s[i];
63
64            if c == b'-' {
65                assert!(expect_hyphens, "unexpected hyphen in UUID");
66                assert!(is_hyphen_position(pos), "hyphen at invalid position");
67                i += 1;
68                pos += 1;
69                continue;
70            }
71
72            let high = hex_digit(c);
73            let low = hex_digit(s[i + 1]);
74            bytes[byte_idx] = (high << 4) | low;
75            byte_idx += 1;
76            i += 2;
77            pos += 2;
78        }
79
80        assert!(byte_idx == 16, "UUID must be exactly 16 bytes");
81
82        bytes
83    }
84}
85
86/// Returns (start, end) indices for the UUID portion of the string.
87const fn find_uuid_bounds(s: &[u8]) -> (usize, usize) {
88    let mut start = 0;
89    let mut end = s.len();
90
91    // Strip "urn:uuid:" prefix (case-insensitive)
92    if s.len() >= 9
93        && (s[0] == b'u' || s[0] == b'U')
94        && (s[1] == b'r' || s[1] == b'R')
95        && (s[2] == b'n' || s[2] == b'N')
96        && s[3] == b':'
97        && (s[4] == b'u' || s[4] == b'U')
98        && (s[5] == b'u' || s[5] == b'U')
99        && (s[6] == b'i' || s[6] == b'I')
100        && (s[7] == b'd' || s[7] == b'D')
101        && s[8] == b':'
102    {
103        start = 9;
104    }
105
106    // Strip braces
107    if end > start + 1 && s[start] == b'{' {
108        assert!(s[end - 1] == b'}', "mismatched braces");
109        start += 1;
110        end -= 1;
111    }
112
113    (start, end)
114}
115
116const fn is_hyphen_position(i: usize) -> bool {
117    i == 8 || i == 13 || i == 18 || i == 23
118}
119
120const fn hex_digit(c: u8) -> u8 {
121    match c {
122        b'0'..=b'9' => c - b'0',
123        b'a'..=b'f' => c - b'a' + 10,
124        b'A'..=b'F' => c - b'A' + 10,
125        _ => panic!("invalid hex digit"),
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    use crate::UUID;
132
133    const EXPECTED: &str = "6ba7b810-9dad-11d1-80b4-00c04fd430c8";
134
135    #[test]
136    fn parse_hyphenated() {
137        const UUID1: UUID = uuid!("6ba7b810-9dad-11d1-80b4-00c04fd430c8");
138        assert_eq!(UUID1.to_string(), "6ba7b810-9dad-11d1-80b4-00c04fd430c8");
139    }
140
141    #[test]
142    fn parse_simple() {
143        const UUID1: UUID = uuid!("6ba7b8109dad11d180b400c04fd430c8");
144        assert_eq!(UUID1.to_string(), "6ba7b810-9dad-11d1-80b4-00c04fd430c8");
145    }
146
147    #[test]
148    fn parse_braced() {
149        const UUID1: UUID = uuid!("{6ba7b810-9dad-11d1-80b4-00c04fd430c8}");
150        assert_eq!(UUID1.to_string(), "6ba7b810-9dad-11d1-80b4-00c04fd430c8");
151    }
152
153    #[test]
154    fn parse_braced_simple() {
155        const UUID1: UUID = uuid!("{6ba7b8109dad11d180b400c04fd430c8}");
156        assert_eq!(UUID1.to_string(), "6ba7b810-9dad-11d1-80b4-00c04fd430c8");
157    }
158
159    #[test]
160    fn parse_urn() {
161        const UUID1: UUID = uuid!("urn:uuid:6ba7b810-9dad-11d1-80b4-00c04fd430c8");
162        assert_eq!(UUID1.to_string(), "6ba7b810-9dad-11d1-80b4-00c04fd430c8");
163    }
164
165    #[test]
166    fn parse_urn_uppercase() {
167        const UUID1: UUID = uuid!("URN:UUID:6BA7B810-9DAD-11D1-80B4-00C04FD430C8");
168        assert_eq!(UUID1.to_string(), "6ba7b810-9dad-11d1-80b4-00c04fd430c8");
169    }
170
171    #[test]
172    fn parse_max() {
173        const MAX: UUID = uuid!("ffffffff-ffff-ffff-ffff-ffffffffffff");
174        assert_eq!(MAX, UUID::max());
175    }
176
177    // Hyphenated format - lowercase
178    #[test]
179    fn hyphenated_lower() {
180        const U: UUID = uuid!("6ba7b810-9dad-11d1-80b4-00c04fd430c8");
181        assert_eq!(U.to_string(), EXPECTED);
182    }
183
184    // Hyphenated format - uppercase
185    #[test]
186    fn hyphenated_upper() {
187        const U: UUID = uuid!("6BA7B810-9DAD-11D1-80B4-00C04FD430C8");
188        assert_eq!(U.to_string(), EXPECTED);
189    }
190
191    // Hyphenated format - mixed case
192    #[test]
193    fn hyphenated_mixed() {
194        const U: UUID = uuid!("6Ba7b810-9DaD-11d1-80B4-00c04FD430c8");
195        assert_eq!(U.to_string(), EXPECTED);
196    }
197
198    // Simple format - lowercase
199    #[test]
200    fn simple_lower() {
201        const U: UUID = uuid!("6ba7b8109dad11d180b400c04fd430c8");
202        assert_eq!(U.to_string(), EXPECTED);
203    }
204
205    // Simple format - uppercase
206    #[test]
207    fn simple_upper() {
208        const U: UUID = uuid!("6BA7B8109DAD11D180B400C04FD430C8");
209        assert_eq!(U.to_string(), EXPECTED);
210    }
211
212    // Simple format - mixed case
213    #[test]
214    fn simple_mixed() {
215        const U: UUID = uuid!("6Ba7b8109DaD11d180B400c04FD430c8");
216        assert_eq!(U.to_string(), EXPECTED);
217    }
218
219    // Braced hyphenated format - lowercase
220    #[test]
221    fn braced_hyphenated_lower() {
222        const U: UUID = uuid!("{6ba7b810-9dad-11d1-80b4-00c04fd430c8}");
223        assert_eq!(U.to_string(), EXPECTED);
224    }
225
226    // Braced hyphenated format - uppercase
227    #[test]
228    fn braced_hyphenated_upper() {
229        const U: UUID = uuid!("{6BA7B810-9DAD-11D1-80B4-00C04FD430C8}");
230        assert_eq!(U.to_string(), EXPECTED);
231    }
232
233    // Braced hyphenated format - mixed case
234    #[test]
235    fn braced_hyphenated_mixed() {
236        const U: UUID = uuid!("{6Ba7b810-9DaD-11d1-80B4-00c04FD430c8}");
237        assert_eq!(U.to_string(), EXPECTED);
238    }
239
240    // Braced simple format - lowercase
241    #[test]
242    fn braced_simple_lower() {
243        const U: UUID = uuid!("{6ba7b8109dad11d180b400c04fd430c8}");
244        assert_eq!(U.to_string(), EXPECTED);
245    }
246
247    // Braced simple format - uppercase
248    #[test]
249    fn braced_simple_upper() {
250        const U: UUID = uuid!("{6BA7B8109DAD11D180B400C04FD430C8}");
251        assert_eq!(U.to_string(), EXPECTED);
252    }
253
254    // Braced simple format - mixed case
255    #[test]
256    fn braced_simple_mixed() {
257        const U: UUID = uuid!("{6Ba7b8109DaD11d180B400c04FD430c8}");
258        assert_eq!(U.to_string(), EXPECTED);
259    }
260
261    // URN hyphenated format - lowercase
262    #[test]
263    fn urn_hyphenated_lower() {
264        const U: UUID = uuid!("urn:uuid:6ba7b810-9dad-11d1-80b4-00c04fd430c8");
265        assert_eq!(U.to_string(), EXPECTED);
266    }
267
268    // URN hyphenated format - uppercase
269    #[test]
270    fn urn_hyphenated_upper() {
271        const U: UUID = uuid!("URN:UUID:6BA7B810-9DAD-11D1-80B4-00C04FD430C8");
272        assert_eq!(U.to_string(), EXPECTED);
273    }
274
275    // URN hyphenated format - mixed case (prefix and UUID)
276    #[test]
277    fn urn_hyphenated_mixed() {
278        const U: UUID = uuid!("Urn:UuId:6Ba7b810-9DaD-11d1-80B4-00c04FD430c8");
279        assert_eq!(U.to_string(), EXPECTED);
280    }
281
282    // URN simple format - lowercase
283    #[test]
284    fn urn_simple_lower() {
285        const U: UUID = uuid!("urn:uuid:6ba7b8109dad11d180b400c04fd430c8");
286        assert_eq!(U.to_string(), EXPECTED);
287    }
288
289    // URN simple format - uppercase
290    #[test]
291    fn urn_simple_upper() {
292        const U: UUID = uuid!("URN:UUID:6BA7B8109DAD11D180B400C04FD430C8");
293        assert_eq!(U.to_string(), EXPECTED);
294    }
295
296    // URN simple format - mixed case
297    #[test]
298    fn urn_simple_mixed() {
299        const U: UUID = uuid!("Urn:UuId:6Ba7b8109DaD11d180B400c04FD430c8");
300        assert_eq!(U.to_string(), EXPECTED);
301    }
302
303    // Special values
304    #[test]
305    fn parse_nil() {
306        const NIL: UUID = uuid!("00000000-0000-0000-0000-000000000000");
307        assert_eq!(NIL, UUID::nil());
308    }
309
310    #[test]
311    fn parse_max_lower() {
312        const MAX: UUID = uuid!("ffffffff-ffff-ffff-ffff-ffffffffffff");
313        assert_eq!(MAX, UUID::max());
314    }
315
316    #[test]
317    fn parse_max_upper() {
318        const MAX: UUID = uuid!("FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF");
319        assert_eq!(MAX, UUID::max());
320    }
321
322    #[test]
323    fn usable_in_const_context() {
324        const DNS_NS: UUID = uuid!("6ba7b810-9dad-11d1-80b4-00c04fd430c8");
325        static STATIC_UUID: UUID = uuid!("6ba7b811-9dad-11d1-80b4-00c04fd430c8");
326
327        assert!(DNS_NS.is_v1());
328        assert!(STATIC_UUID.is_v1());
329    }
330}