julid-rs 1.6.1803398874989

A crate and loadable extension for SQLite that provides Joe's ULIDs.
Documentation
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 const 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 technically need to be altered when converting to a
    /// Julid, but we zero out the high bits of the counter where the UUID
    /// version was stored.
    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);
        assert_eq!(ju1.random() >> 62, 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)));
    }
}