281 lines
7.9 KiB
Rust
281 lines
7.9 KiB
Rust
use core::{fmt, str::FromStr};
|
|
use std::{
|
|
sync::atomic::{AtomicU64, Ordering},
|
|
time::Duration,
|
|
};
|
|
|
|
use rand::{random, thread_rng, Rng};
|
|
|
|
use crate::base32::{self, DecodeError};
|
|
|
|
/// This is used to ensure monotonicity for new IDs.
|
|
static LAST_SORTABLE: AtomicU64 = AtomicU64::new(0);
|
|
|
|
/// The number of bits in a Julid's time portion
|
|
pub const TIME_BITS: u8 = 48;
|
|
/// The number of bits in the monotonic counter for intra-millisecond IDs
|
|
pub const COUNTER_BITS: u8 = 16;
|
|
/// The number of random bits + bits in the monotonic counter
|
|
pub const UNIQUE_BITS: u8 = 80;
|
|
pub const RANDOM_BITS: u8 = 64;
|
|
|
|
macro_rules! bitmask {
|
|
($len:expr) => {
|
|
((1 << $len) - 1)
|
|
};
|
|
}
|
|
|
|
/// A Julid is a unique 128-bit lexicographically sortable identifier,
|
|
/// compatible with ULIDs.
|
|
///
|
|
/// Canonically, it is represented as a 26 character Crockford Base32 encoded
|
|
/// string, or as a sequence of 16 bytes in big-endian order.
|
|
///
|
|
/// Of the 128-bits, the 48 most-significant are a unix timestamp in
|
|
/// milliseconds. The next 16 bits are a monotonic counter for IDs created in
|
|
/// the same millisecond. The remaining 64 least-significant bits are fully
|
|
/// random.
|
|
#[derive(Debug, PartialOrd, Ord, PartialEq, Eq, Hash, Clone, Copy)]
|
|
pub struct Julid(pub u128);
|
|
|
|
impl Julid {
|
|
/// Return a new Julid. If a previous ID was generated in the same
|
|
/// millisecond, increment the monotonic counter, up to u16::MAX. The random
|
|
/// bits are always fresh, so once the monotonic counter is saturated,
|
|
/// subsequent IDs from the current millisecond will not have an
|
|
/// inherent ordering. See discussion at https://github.com/ahawker/ulid/issues/306#issuecomment-451850395
|
|
pub fn new() -> Self {
|
|
let lsb: u64 = random();
|
|
loop {
|
|
let ts = std::time::SystemTime::now()
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
.unwrap_or(Duration::ZERO)
|
|
.as_millis() as u64;
|
|
let last = LAST_SORTABLE.load(Ordering::SeqCst);
|
|
let ots = last >> COUNTER_BITS;
|
|
if ots < ts {
|
|
let msb = ts << COUNTER_BITS;
|
|
if LAST_SORTABLE
|
|
.compare_exchange(last, msb, Ordering::SeqCst, Ordering::Relaxed)
|
|
.is_ok()
|
|
{
|
|
break (msb, lsb).into();
|
|
}
|
|
} else {
|
|
let counter = ((last & bitmask!(COUNTER_BITS) as u64) as u16).saturating_add(1);
|
|
let msb = (ots << COUNTER_BITS) + counter as u64;
|
|
if LAST_SORTABLE
|
|
.compare_exchange(last, msb, Ordering::SeqCst, Ordering::Relaxed)
|
|
.is_ok()
|
|
{
|
|
break (msb, lsb).into();
|
|
}
|
|
}
|
|
// we didn't update the global counter, try again
|
|
let micros = thread_rng().gen_range(10..50);
|
|
std::thread::sleep(Duration::from_micros(micros));
|
|
}
|
|
}
|
|
|
|
/// Creates a Julid from a Crockford Base32 encoded string
|
|
///
|
|
/// An DecodeError will be returned when the given string is not formated
|
|
/// properly.
|
|
///
|
|
/// # Example
|
|
/// ```rust
|
|
/// use julid::julid::Julid;
|
|
/// let text = "01D39ZY06FGSCTVN4T2V9PKHFZ";
|
|
/// let result = Julid::from_string(text);
|
|
///
|
|
/// assert!(result.is_ok());
|
|
/// assert_eq!(&result.unwrap().to_string(), text);
|
|
/// ```
|
|
pub const fn from_string(encoded: &str) -> Result<Julid, DecodeError> {
|
|
match base32::decode(encoded) {
|
|
Ok(int_val) => Ok(Julid(int_val)),
|
|
Err(err) => Err(err),
|
|
}
|
|
}
|
|
|
|
/// The 'Alpha Julid'.
|
|
///
|
|
/// The Alpha Julid is special form of Julid that is specified to have
|
|
/// all 128 bits set to zero.
|
|
pub const fn alpha() -> Julid {
|
|
Julid(0)
|
|
}
|
|
|
|
/// The 'Omega Julid'.
|
|
///
|
|
/// The Omega Julid is special form of Julid that is specified to have
|
|
/// all 128 bits set to one.
|
|
pub const fn omega() -> Self {
|
|
Julid(u128::MAX)
|
|
}
|
|
|
|
/// Gets the timestamp section of this ulid
|
|
pub const fn timestamp(&self) -> u64 {
|
|
(self.0 >> UNIQUE_BITS) as u64
|
|
}
|
|
|
|
pub const fn counter(&self) -> u16 {
|
|
let mask = bitmask!(COUNTER_BITS);
|
|
((self.0 >> RANDOM_BITS) & mask) as u16
|
|
}
|
|
|
|
pub const fn sortable(&self) -> u64 {
|
|
let mask = bitmask!(TIME_BITS + COUNTER_BITS);
|
|
((self.0 >> RANDOM_BITS) & mask) as u64
|
|
}
|
|
|
|
pub const fn random(&self) -> u128 {
|
|
self.0 & bitmask!(RANDOM_BITS)
|
|
}
|
|
|
|
/// Gets the non-timestamp section of this Julid (random + counter bits).
|
|
pub const fn unique(&self) -> u128 {
|
|
self.0 & bitmask!(UNIQUE_BITS)
|
|
}
|
|
|
|
/// Creates a Crockford Base32 encoded string that represents this Julid
|
|
///
|
|
/// # Example
|
|
/// ```rust
|
|
/// use julid::julid::Julid;
|
|
/// let text = "01D39ZY06FGSCTVN4T2V9PKHFZ";
|
|
/// let id = Julid::from_string(text).unwrap();
|
|
///
|
|
/// assert_eq!(&id.to_string(), text);
|
|
/// ```
|
|
pub fn as_string(self) -> String {
|
|
base32::encode(self.0)
|
|
}
|
|
|
|
/// Test if the Julid is Alpha
|
|
pub const fn is_alpha(&self) -> bool {
|
|
self.0 == 0u128
|
|
}
|
|
|
|
/// Creates a Julid using the provided bytes array, assumed big-endian.
|
|
pub const fn from_bytes(bytes: [u8; 16]) -> Julid {
|
|
Self(u128::from_be_bytes(bytes))
|
|
}
|
|
|
|
/// Returns the bytes of the Julid in big-endian order.
|
|
pub const fn to_bytes(self) -> [u8; 16] {
|
|
self.0.to_be_bytes()
|
|
}
|
|
}
|
|
|
|
impl Default for Julid {
|
|
fn default() -> Self {
|
|
Julid::alpha()
|
|
}
|
|
}
|
|
|
|
impl From<Julid> for String {
|
|
fn from(ulid: Julid) -> String {
|
|
ulid.as_string()
|
|
}
|
|
}
|
|
|
|
impl From<(u64, u64)> for Julid {
|
|
fn from((msb, lsb): (u64, u64)) -> Self {
|
|
Julid(u128::from(msb) << 64 | u128::from(lsb))
|
|
}
|
|
}
|
|
|
|
impl From<Julid> for (u64, u64) {
|
|
fn from(ulid: Julid) -> (u64, u64) {
|
|
((ulid.0 >> 64) as u64, (ulid.0 & bitmask!(64)) as u64)
|
|
}
|
|
}
|
|
|
|
impl From<u128> for Julid {
|
|
fn from(value: u128) -> Julid {
|
|
Julid(value)
|
|
}
|
|
}
|
|
|
|
impl From<Julid> for u128 {
|
|
fn from(ulid: Julid) -> u128 {
|
|
ulid.0
|
|
}
|
|
}
|
|
|
|
impl From<[u8; 16]> for Julid {
|
|
fn from(bytes: [u8; 16]) -> Self {
|
|
Self(u128::from_be_bytes(bytes))
|
|
}
|
|
}
|
|
|
|
impl From<Julid> for [u8; 16] {
|
|
fn from(ulid: Julid) -> Self {
|
|
ulid.0.to_be_bytes()
|
|
}
|
|
}
|
|
|
|
impl FromStr for Julid {
|
|
type Err = DecodeError;
|
|
|
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
Julid::from_string(s)
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for Julid {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
|
|
write!(f, "{}", self.as_string())
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_static() {
|
|
let s = Julid(0x41414141414141414141414141414141).as_string();
|
|
let u = Julid::from_string(&s).unwrap();
|
|
assert_eq!(&s, "21850M2GA1850M2GA1850M2GA1");
|
|
assert_eq!(u.0, 0x41414141414141414141414141414141);
|
|
}
|
|
|
|
#[test]
|
|
fn can_into_thing() {
|
|
let ulid = Julid::from_str("01FKMG6GAG0PJANMWFN84TNXCD").unwrap();
|
|
let s: String = ulid.into();
|
|
let u: u128 = ulid.into();
|
|
let uu: (u64, u64) = ulid.into();
|
|
let bytes: [u8; 16] = ulid.into();
|
|
|
|
assert_eq!(Julid::from_str(&s).unwrap(), ulid);
|
|
assert_eq!(Julid::from(u), ulid);
|
|
assert_eq!(Julid::from(uu), ulid);
|
|
assert_eq!(Julid::from(bytes), ulid);
|
|
}
|
|
|
|
#[test]
|
|
fn default_is_nil() {
|
|
assert_eq!(Julid::default(), Julid::alpha());
|
|
}
|
|
|
|
#[test]
|
|
fn can_display_things() {
|
|
println!("{}", Julid::alpha());
|
|
println!("{}", DecodeError::InvalidLength(0));
|
|
println!("{}", DecodeError::InvalidChar('^'));
|
|
}
|
|
|
|
#[test]
|
|
fn can_increment() {
|
|
let mut max = 0;
|
|
for i in 0..100 {
|
|
let id = Julid::new();
|
|
max = id.counter().max(max);
|
|
assert!(max <= i);
|
|
}
|
|
assert!(max > 49);
|
|
}
|
|
}
|