1use std::{
19 ffi::CStr,
20 fmt::{Debug, Display},
21 hash::Hash,
22 io::{Cursor, Write},
23 str::FromStr,
24};
25
26use rand::RngCore;
27use serde::{Deserialize, Deserializer, Serialize, Serializer};
28use uuid::Uuid;
29
30pub(crate) const UUID4_LEN: usize = 37;
32
33#[repr(C)]
36#[derive(Copy, Clone, Hash, PartialEq, Eq)]
37#[cfg_attr(
38 feature = "python",
39 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.core")
40)]
41pub struct UUID4 {
42 pub(crate) value: [u8; 37], }
45
46impl UUID4 {
47 #[must_use]
51 pub fn new() -> Self {
52 let mut rng = rand::rng();
53 let mut bytes = [0u8; 16];
54 rng.fill_bytes(&mut bytes);
55
56 bytes[6] = (bytes[6] & 0x0F) | 0x40; bytes[8] = (bytes[8] & 0x3F) | 0x80; let mut value = [0u8; UUID4_LEN];
60 let mut cursor = Cursor::new(&mut value[..36]);
61
62 write!(
63 cursor,
64 "{:08x}-{:04x}-{:04x}-{:04x}-{:012x}",
65 u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]),
66 u16::from_be_bytes([bytes[4], bytes[5]]),
67 u16::from_be_bytes([bytes[6], bytes[7]]),
68 u16::from_be_bytes([bytes[8], bytes[9]]),
69 u64::from_be_bytes([
70 bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15], 0, 0
71 ]) >> 16
72 )
73 .expect("Error writing UUID string to buffer");
74
75 value[36] = 0; Self { value }
78 }
79
80 #[must_use]
86 pub fn to_cstr(&self) -> &CStr {
87 CStr::from_bytes_with_nul(&self.value)
89 .expect("UUID byte representation should be a valid C string")
90 }
91
92 #[must_use]
94 pub fn as_str(&self) -> &str {
95 self.to_cstr().to_str().expect("UUID should be valid UTF-8")
97 }
98
99 #[must_use]
104 pub fn as_bytes(&self) -> [u8; 16] {
105 let uuid_str = self.to_cstr().to_str().expect("Valid UTF-8");
108 let uuid = Uuid::parse_str(uuid_str).expect("Valid UUID4");
109 *uuid.as_bytes()
110 }
111
112 fn validate_v4(uuid: &Uuid) {
113 assert_eq!(
115 uuid.get_version(),
116 Some(uuid::Version::Random),
117 "UUID is not version 4"
118 );
119
120 assert_eq!(
122 uuid.get_variant(),
123 uuid::Variant::RFC4122,
124 "UUID is not RFC 4122 variant"
125 );
126 }
127
128 fn try_validate_v4(uuid: &Uuid) -> Result<(), String> {
129 if uuid.get_version() != Some(uuid::Version::Random) {
130 return Err("UUID is not version 4".to_string());
131 }
132 if uuid.get_variant() != uuid::Variant::RFC4122 {
133 return Err("UUID is not RFC 4122 variant".to_string());
134 }
135 Ok(())
136 }
137
138 fn from_validated_uuid(uuid: &Uuid) -> Self {
139 let mut value = [0; UUID4_LEN];
140 let uuid_str = uuid.to_string();
141 value[..uuid_str.len()].copy_from_slice(uuid_str.as_bytes());
142 value[uuid_str.len()] = 0; Self { value }
144 }
145}
146
147impl FromStr for UUID4 {
148 type Err = String;
149
150 fn from_str(value: &str) -> Result<Self, Self::Err> {
158 let uuid = Uuid::try_parse(value).map_err(|e| e.to_string())?;
159 Self::try_validate_v4(&uuid)?;
160 Ok(Self::from_validated_uuid(&uuid))
161 }
162}
163
164impl From<&str> for UUID4 {
165 fn from(value: &str) -> Self {
171 value
172 .parse()
173 .expect("`value` should be a valid UUID version 4 (RFC 4122)")
174 }
175}
176
177impl From<String> for UUID4 {
178 fn from(value: String) -> Self {
184 Self::from(value.as_str())
185 }
186}
187
188impl From<uuid::Uuid> for UUID4 {
189 fn from(value: uuid::Uuid) -> Self {
195 Self::validate_v4(&value);
196 Self::from_validated_uuid(&value)
197 }
198}
199
200impl From<UUID4> for uuid::Uuid {
201 fn from(value: UUID4) -> Self {
203 Self::from_bytes(value.as_bytes())
204 }
205}
206
207impl Default for UUID4 {
208 fn default() -> Self {
212 Self::new()
213 }
214}
215
216impl Debug for UUID4 {
217 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
218 write!(f, "{}({})", stringify!(UUID4), self)
219 }
220}
221
222impl Display for UUID4 {
223 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
224 write!(f, "{}", self.to_cstr().to_string_lossy())
225 }
226}
227
228impl Serialize for UUID4 {
229 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
230 where
231 S: Serializer,
232 {
233 self.to_string().serialize(serializer)
234 }
235}
236
237impl<'de> Deserialize<'de> for UUID4 {
238 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
239 where
240 D: Deserializer<'de>,
241 {
242 let uuid4_str: &str = Deserialize::deserialize(deserializer)?;
243 uuid4_str.parse().map_err(serde::de::Error::custom)
244 }
245}
246
247#[cfg(test)]
248mod tests {
249 use std::{
250 collections::hash_map::DefaultHasher,
251 hash::{Hash, Hasher},
252 };
253
254 use rstest::*;
255 use uuid;
256
257 use super::*;
258
259 #[rstest]
260 fn test_new() {
261 let uuid = UUID4::new();
262 let uuid_string = uuid.to_string();
263 let uuid_parsed = Uuid::parse_str(&uuid_string).unwrap();
264 assert_eq!(uuid_parsed.get_version().unwrap(), uuid::Version::Random);
265 assert_eq!(uuid_parsed.to_string().len(), 36);
266
267 assert_eq!(&uuid_string[14..15], "4");
269 let variant_char = &uuid_string[19..20];
271 assert!(matches!(variant_char, "8" | "9" | "a" | "b" | "A" | "B"));
272 }
273
274 #[rstest]
275 fn test_uuid_format() {
276 let uuid = UUID4::new();
277 let bytes = uuid.value;
278
279 assert_eq!(bytes[36], 0);
281
282 assert_eq!(bytes[8] as char, '-');
284 assert_eq!(bytes[13] as char, '-');
285 assert_eq!(bytes[18] as char, '-');
286 assert_eq!(bytes[23] as char, '-');
287
288 let s = uuid.to_string();
289 assert_eq!(s.chars().nth(14).unwrap(), '4');
290 }
291
292 #[rstest]
293 #[should_panic(expected = "UUID is not version 4")]
294 fn test_from_str_with_non_version_4_uuid_panics() {
295 let uuid_string = "6ba7b810-9dad-11d1-80b4-00c04fd430c8"; let _ = UUID4::from(uuid_string);
297 }
298
299 #[rstest]
300 fn test_case_insensitive_parsing() {
301 let upper = "2D89666B-1A1E-4A75-B193-4EB3B454C757";
302 let lower = "2d89666b-1a1e-4a75-b193-4eb3b454c757";
303 let uuid_upper = UUID4::from(upper);
304 let uuid_lower = UUID4::from(lower);
305
306 assert_eq!(uuid_upper, uuid_lower);
307 assert_eq!(uuid_upper.to_string(), lower);
308 }
309
310 #[rstest]
311 #[case("6ba7b810-9dad-11d1-80b4-00c04fd430c8")] #[case("000001f5-8fa9-21d1-9df3-00e098032b8c")] #[case("3d813cbb-47fb-32ba-91df-831e1593ac29")] #[case("fb4f37c1-4ba3-5173-9812-2b90e76a06f7")] #[should_panic(expected = "UUID is not version 4")]
316 fn test_invalid_version(#[case] uuid_string: &str) {
317 let _ = UUID4::from(uuid_string);
318 }
319
320 #[rstest]
321 #[should_panic(expected = "UUID is not RFC 4122 variant")]
322 fn test_non_rfc4122_variant() {
323 let uuid = "550e8400-e29b-41d4-0000-446655440000";
325 let _ = UUID4::from(uuid);
326 }
327
328 #[rstest]
329 #[case("")] #[case("not-a-uuid-at-all")] #[case("6ba7b810-9dad-11d1-80b4")] #[case("6ba7b810-9dad-11d1-80b4-00c04fd430c8-extra")] #[case("6ba7b810-9dad-11d1-80b4=00c04fd430c8")] #[case("6ba7b81019dad111d180b400c04fd430c8")] #[case("6ba7b810-9dad-11d1-80b4-00c04fd430")] #[case("6ba7b810-9dad-11d1-80b4-00c04fd430cg")] fn test_invalid_uuid_cases(#[case] invalid_uuid: &str) {
338 assert!(UUID4::from_str(invalid_uuid).is_err());
339 }
340
341 #[rstest]
342 fn test_default() {
343 let uuid: UUID4 = UUID4::default();
344 let uuid_string = uuid.to_string();
345 let uuid_parsed = Uuid::parse_str(&uuid_string).unwrap();
346 assert_eq!(uuid_parsed.get_version().unwrap(), uuid::Version::Random);
347 }
348
349 #[rstest]
350 fn test_from_str() {
351 let uuid_string = "2d89666b-1a1e-4a75-b193-4eb3b454c757";
352 let uuid = UUID4::from(uuid_string);
353 let result_string = uuid.to_string();
354 let result_parsed = Uuid::parse_str(&result_string).unwrap();
355 let expected_parsed = Uuid::parse_str(uuid_string).unwrap();
356 assert_eq!(result_parsed, expected_parsed);
357 }
358
359 #[rstest]
360 fn test_from_uuid() {
361 let original = uuid::Uuid::new_v4();
362 let uuid4 = UUID4::from(original);
363 assert_eq!(uuid4.to_string(), original.to_string());
364 }
365
366 #[rstest]
367 fn test_equality() {
368 let uuid1 = UUID4::from("2d89666b-1a1e-4a75-b193-4eb3b454c757");
369 let uuid2 = UUID4::from("46922ecb-4324-4e40-a56c-841e0d774cef");
370 assert_eq!(uuid1, uuid1);
371 assert_ne!(uuid1, uuid2);
372 }
373
374 #[rstest]
375 fn test_debug() {
376 let uuid_string = "2d89666b-1a1e-4a75-b193-4eb3b454c757";
377 let uuid = UUID4::from(uuid_string);
378 assert_eq!(format!("{uuid:?}"), format!("UUID4({uuid_string})"));
379 }
380
381 #[rstest]
382 fn test_display() {
383 let uuid_string = "2d89666b-1a1e-4a75-b193-4eb3b454c757";
384 let uuid = UUID4::from(uuid_string);
385 assert_eq!(format!("{uuid}"), uuid_string);
386 }
387
388 #[rstest]
389 fn test_to_cstr() {
390 let uuid = UUID4::new();
391 let cstr = uuid.to_cstr();
392
393 assert_eq!(cstr.to_str().unwrap(), uuid.to_string());
394 assert_eq!(cstr.to_bytes_with_nul()[36], 0);
395 }
396
397 #[rstest]
398 fn test_as_str() {
399 let uuid = UUID4::new();
400 let s = uuid.as_str();
401
402 assert_eq!(s, uuid.to_string());
403 assert_eq!(s.len(), 36);
404 }
405
406 #[rstest]
407 fn test_hash_consistency() {
408 let uuid = UUID4::new();
409
410 let mut hasher1 = DefaultHasher::new();
411 let mut hasher2 = DefaultHasher::new();
412
413 uuid.hash(&mut hasher1);
414 uuid.hash(&mut hasher2);
415
416 assert_eq!(hasher1.finish(), hasher2.finish());
417 }
418
419 #[rstest]
420 fn test_serialize_json() {
421 let uuid_string = "2d89666b-1a1e-4a75-b193-4eb3b454c757";
422 let uuid = UUID4::from(uuid_string);
423
424 let serialized = serde_json::to_string(&uuid).unwrap();
425 let expected_json = format!("\"{uuid_string}\"");
426 assert_eq!(serialized, expected_json);
427 }
428
429 #[rstest]
430 fn test_deserialize_json() {
431 let uuid_string = "2d89666b-1a1e-4a75-b193-4eb3b454c757";
432 let serialized = format!("\"{uuid_string}\"");
433
434 let deserialized: UUID4 = serde_json::from_str(&serialized).unwrap();
435 assert_eq!(deserialized.to_string(), uuid_string);
436 }
437
438 #[rstest]
439 fn test_serialize_deserialize_round_trip() {
440 let uuid = UUID4::new();
441
442 let serialized = serde_json::to_string(&uuid).unwrap();
443 let deserialized: UUID4 = serde_json::from_str(&serialized).unwrap();
444
445 assert_eq!(uuid, deserialized);
446 }
447
448 #[rstest]
449 fn test_as_bytes() {
450 let uuid_string = "2d89666b-1a1e-4a75-b193-4eb3b454c757";
451 let uuid = UUID4::from(uuid_string);
452
453 let bytes = uuid.as_bytes();
454 assert_eq!(bytes.len(), 16);
455
456 let reconstructed = Uuid::from_bytes(bytes);
458 assert_eq!(reconstructed.to_string(), uuid_string);
459
460 assert_eq!(reconstructed.get_version().unwrap(), uuid::Version::Random);
462 }
463
464 #[rstest]
465 fn test_as_bytes_round_trip() {
466 let uuid1 = UUID4::new();
467 let bytes = uuid1.as_bytes();
468 let uuid2 = UUID4::from(Uuid::from_bytes(bytes));
469
470 assert_eq!(uuid1, uuid2);
471 }
472
473 #[rstest]
474 #[case("\"not-a-uuid\"")] #[case("\"6ba7b810-9dad-11d1-80b4-00c04fd430c8\"")] #[case("\"\"")] fn test_deserialize_invalid_uuid_returns_error(#[case] json: &str) {
478 let result: Result<UUID4, _> = serde_json::from_str(json);
479 assert!(result.is_err());
480 }
481}