oid2 0.1.0

An id schema that is compatible with uuid, lexicographcally sorted (won't explode database indexes), readable, and double-click-copyable.
Documentation
use std::fmt::{Debug, Display};

static ALPHABET: &str = "0abcdefghjkmnpqrstuvwxyz23456789";

const SHORT_LENGTH: usize = 22;

#[derive(Clone)]
pub struct Test<T> {
    _marker: T,
}

fn base32_encode(data: &[u8], alphabet: &str) -> String {
    let mut result = String::new();
    let mut buffer = 0u64;
    let mut bits_in_buffer = 0usize;

    let alphabet_chars: Vec<char> = alphabet.chars().collect();

    for byte in data {
        buffer = (buffer << 8) | (*byte as u64);
        bits_in_buffer += 8;

        while bits_in_buffer >= 5 {
            bits_in_buffer -= 5;
            let index = ((buffer >> bits_in_buffer) & 0x1F) as usize;
            result.push(alphabet_chars[index]);
        }
    }

    if bits_in_buffer > 0 {
        let index = ((buffer << (5 - bits_in_buffer)) & 0x1F) as usize;
        result.push(alphabet_chars[index]);
    }

    result
}

#[cfg(all(feature = "rand", any(feature = "chrono", feature = "time")))]
fn unix_epoch_ms() -> u64 {
    #[cfg(feature = "time")]
    {
        let now = time::OffsetDateTime::now_utc();

        now.unix_timestamp() as u64 * 1_000 + now.millisecond() as u64
    }
    #[cfg(all(feature = "chrono", not(feature = "time")))]
    {
        let now: chrono::DateTime<chrono::Utc> = chrono::Utc::now();

        now.timestamp_millis() as u64
    }
}

pub struct NoLabel;

#[derive(PartialOrd, PartialEq, Eq, Ord, Hash, Copy)]
pub struct Oid<T = NoLabel> {
    data: [u8; 16],
    marker: std::marker::PhantomData<T>,
}

impl<T> Clone for Oid<T> {
    fn clone(&self) -> Self {
        Self {
            data: self.data,
            marker: Default::default(),
        }
    }
}

impl Label for NoLabel {
    fn label() -> &'static str {
        ""
    }
}

pub fn new_oid() -> Oid {
    NoLabel::oid()
}

impl<T: Label> Oid<T> {
    #[cfg(feature = "rand")]
    pub fn from_timestamp_with_rng<R>(timestamp: u64, rng: &mut R) -> Self
        where
            R: rand::Rng,
    {
        if (timestamp & 0xFFFF_0000_0000_0000) != 0 {
            panic!("oid does not support timestamps after +10889-08-02T05:31:50.655Z");
        }
        let high = timestamp << 16 | u64::from(rng.gen::<u16>());
        let low = rng.gen::<u64>();
        let high = high.to_le_bytes();
        let low = low.to_le_bytes();

        let mut data: [u8; 16] = [0; 16];
        data[..8].copy_from_slice(&high);
        data[8..].copy_from_slice(&low);

        Self {
            data,
            marker: Default::default(),
        }
    }

    pub fn short(&self) -> String {
        let encoded = base32_encode(&self.data, ALPHABET);
        format!("{}{}", T::label(), &encoded[SHORT_LENGTH..])
    }

    #[cfg(feature = "uuid")]
    pub fn uuid(&self) -> uuid::Uuid {
        uuid::Uuid::from_bytes(self.data)
    }
}

#[cfg(feature = "uuid")]
impl<T> Into<uuid::Uuid> for Oid<T> {
    fn into(self) -> uuid::Uuid {
        uuid::Uuid::from_bytes(self.data)
    }
}

#[cfg(feature = "uuid")]
impl<T: Label> From<uuid::Uuid> for Oid<T> {
    fn from(value: uuid::Uuid) -> Self {
        let bytes = value.as_ref();
        let mut data: [u8; 16] = [0; 16];
        data.copy_from_slice(bytes);
        T::new(data)
    }
}

impl<T: Label> Debug for Oid<T> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let encoded = base32_encode(&self.data, ALPHABET);
        write!(f, "{}{}_{}",
               T::label(),
               &encoded[..SHORT_LENGTH],
               &encoded[SHORT_LENGTH..],
        )
    }
}

pub trait Label {
    fn label() -> &'static str;

    #[cfg(all(feature = "rand", any(feature = "chrono", feature = "time")))]
    fn oid() -> Oid<Self> where Self: Sized {
        Oid::from_timestamp_with_rng(unix_epoch_ms(), &mut rand::thread_rng())
    }

    fn new(bytes: [u8; 16]) -> Oid<Self> where Self: Sized {
        Oid {
            data: bytes,
            marker: Default::default(),
        }
    }

    fn null() -> Oid<Self> where Self: Sized {
        Oid {
            data: [0; 16],
            marker: Default::default(),
        }
    }
}

macro_rules! label {
    ($name:ident, $label:literal) => {
        pub struct $name;
        impl Label for $name {
            fn label() -> &'static str {
                concat!($label, "_")
            }
        }
    };
}

impl<T: Label> Display for Oid<T> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{:?}", self)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    label!(Team, "team");

    #[test]
    fn it_works() {
        let bytes = [1u8, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16];
        let oid = Team::new(bytes);
        println!("{}", oid.short());
        println!("{}", oid);
        assert_eq!(oid.to_string(), "team_0da0fa0e02cssbhkanf04c_srb0");
        assert_eq!(oid.short(), "team_srb0");
    }

    #[test]
    fn test_null() {
        let oid = Team::null();
        println!("{}", oid.short());
        println!("{}", oid);
        assert_eq!(oid.to_string(), "team_0000000000000000000000_0000");
        assert_eq!(oid.short(), "team_0000");
        let oid = NoLabel::null();
        assert_eq!(oid.to_string(), "0000000000000000000000_0000");
    }

    #[test]
    #[cfg(feature = "uuid")]
    fn test_uuid() {
        let bytes = [1u8, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16];
        let oid = Team::new(bytes);
        let uuid: uuid::Uuid = oid.clone().into();
        assert_eq!(uuid.to_string(), "01020304-0506-0708-090a-0b0c0d0e0f10");
        let uuid2 = oid.uuid();
        assert_eq!(uuid, uuid2);
    }
}