use std::fmt; use uuid::{Uuid, Variant}; use crate::{Julid, COUNTER_BITS}; impl Julid { /// Convert to UUIDv7, possibly losing counter bits and altering the top /// two bits from the lower 64. /// /// UUIDv7s are very similar to Julids, but use 12 bits for a monotonic /// counter instead of 16, and only 62 bits of entropy vs Julids' 64. This /// means that some bits in the original Julid are overwritten with /// UUID-specific values, but only six bits in total are potentially /// altered. pub fn as_uuid(&self) -> Uuid { let counter_mask = (1 << 12) - 1; let entropy_mask = (1 << 62) - 1; let timestamp = self.timestamp(); // https://www.ietf.org/rfc/rfc9562.html#name-uuid-version-7 "ver" is 0b0111 let counter = (self.counter() & counter_mask) | (0b0111 << 12); // https://www.ietf.org/rfc/rfc9562.html#name-uuid-version-7 "var" is 0b10 let entropy = (self.random() & entropy_mask) | (0b10 << 62); let top = (timestamp << 16) | counter as u64; Uuid::from_u64_pair(top, entropy) } /// Create from a UUIDv7; will fail if the UUID is not a valid v7 UUID. /// /// UUIDv7s are very similar to Julids, but use 12 bits for a monotonic /// counter instead of 16, and only 62 bits of entropy vs Julids' 64. /// Therefore, no bits need to be altered when converting to a Julid. pub fn from_uuid(id: Uuid) -> Result { let ver = id.get_version_num(); if ver != 7 { return Err(UuidError::UnsupportedVersion(ver)); } let var = id.get_variant(); if var != Variant::RFC4122 { return Err(UuidError::UnsupportedVariant(var)); } let (hi, lo) = id.as_u64_pair(); // zero out the high bits of the counter, which are "7" (0b0111) from the uuid let mask = (1 << 12) - 1; let counter = hi & mask; let ts = hi >> COUNTER_BITS; let hi = (ts << COUNTER_BITS) | counter; Ok((hi, lo).into()) } } impl From for Uuid { fn from(value: Julid) -> Self { value.as_uuid() } } impl TryFrom for Julid { type Error = UuidError; fn try_from(value: Uuid) -> Result { Julid::from_uuid(value) } } #[derive(Debug, Clone, Copy, PartialEq)] pub enum UuidError { UnsupportedVersion(usize), UnsupportedVariant(uuid::Variant), } impl std::error::Error for UuidError {} impl fmt::Display for UuidError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { let text = match *self { UuidError::UnsupportedVersion(v) => format!("unsupported version {v}"), UuidError::UnsupportedVariant(v) => format!("unsupported variant: {v:?}"), }; write!(f, "{text}") } } #[cfg(test)] mod test { use uuid::Uuid; use crate::{uuid::UuidError, Julid}; #[test] fn into_uuid() { // see example from https://docs.rs/uuid/1.17.0/uuid/struct.Uuid.html#method.new_v7 let ts = 1497624119 * 1000; let j = Julid::at(ts); let u = j.as_uuid().hyphenated().to_string(); assert!(u.starts_with("015cb15a-86d8-7")); } #[test] fn from_uuid() { let j1 = Julid::new(); let u1: Uuid = j1.into(); let ju1: Julid = u1.try_into().unwrap(); assert_eq!(j1.timestamp(), ju1.timestamp()); assert_eq!(j1.counter(), ju1.counter()); assert_eq!(j1.random() << 2, ju1.random() << 2); // once we've converted to uuid and then back to julid, we've reached the fixed // point let u2 = ju1.as_uuid(); let ju2 = u2.try_into().unwrap(); assert_eq!(ju1, ju2); } #[test] fn cant_even_from_uuid_non_v7() { let u = uuid::Uuid::new_v4(); let jr: Result = u.try_into(); assert_eq!(jr, Err(UuidError::UnsupportedVersion(4))); } }