julid-rs/src/uuid.rs
2025-06-25 23:20:28 -07:00

123 lines
3.9 KiB
Rust

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<Self, UuidError> {
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<Julid> for Uuid {
fn from(value: Julid) -> Self {
value.as_uuid()
}
}
impl TryFrom<Uuid> for Julid {
type Error = UuidError;
fn try_from(value: Uuid) -> Result<Self, Self::Error> {
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<Julid, UuidError> = u.try_into();
assert_eq!(jr, Err(UuidError::UnsupportedVersion(4)));
}
}