add support for converting to and from v7 uuids
Update version, update license, add tests for UUIDv7.
This commit is contained in:
parent
1e93d0b1e4
commit
b47826d4b3
7 changed files with 141 additions and 29 deletions
14
Cargo.toml
14
Cargo.toml
|
@ -1,9 +1,11 @@
|
|||
[package]
|
||||
name = "julid-rs"
|
||||
version = "1.6.18033988"
|
||||
# 1.61803398874989484
|
||||
# ^
|
||||
version = "1.6.180339887"
|
||||
authors = ["Joe Ardent <code@ardent.nebcorp.com>"]
|
||||
edition = "2021"
|
||||
keywords = ["ulid", "library", "sqlite", "extension", "julid"]
|
||||
edition = "2024"
|
||||
keywords = ["ulid", "library", "sqlite", "extension", "julid", "uuid", "guid"]
|
||||
|
||||
description = "A crate and loadable extension for SQLite that provides Joe's ULIDs."
|
||||
readme = "README.md"
|
||||
|
@ -14,6 +16,7 @@ repository = "https://git.kittencollective.com/nebkor/julid-rs"
|
|||
default = ["serde", "sqlx"] # just the regular crate
|
||||
serde = ["dep:serde"]
|
||||
sqlx = ["dep:sqlx"]
|
||||
uuid = ["dep:uuid"]
|
||||
|
||||
# WARNING! don't enable this feature in your project's Cargo.toml if using julid-rs as a dependency;
|
||||
# see https://gitlab.com/nebkor/julid/-/issues/1
|
||||
|
@ -27,16 +30,19 @@ crate-type = ["cdylib", "rlib"]
|
|||
rand = "0.8"
|
||||
|
||||
# for the CLI
|
||||
clap = { version = "4.3", default-features = false, features = ["help", "usage", "std", "derive"] }
|
||||
clap = { version = "4", default-features = false, features = ["help", "usage", "std", "derive"] }
|
||||
chrono = { version = "0.4", default-features = false, features = ["std", "time"] }
|
||||
|
||||
# all other deps are optional
|
||||
serde = { version = "1.0", features = ["derive"], optional = true }
|
||||
sqlx = { version = "0.7", features = ["sqlite"], default-features = false, optional = true }
|
||||
sqlite-loadable = { version = "0.0.5", optional = true }
|
||||
uuid = { version = "1.17", default-features = false, optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
divan = "0.1"
|
||||
uuid = { version = "1.17", default-features = false, features = ["v7"] }
|
||||
julid-rs = { path = ".", features = ["uuid"] }
|
||||
|
||||
[[bench]]
|
||||
name = "simple"
|
||||
|
|
10
LICENSE.md
10
LICENSE.md
|
@ -1,5 +1,13 @@
|
|||
# The Chaos License (GLP)
|
||||
# Dual Licensed (combined terms are binding)
|
||||
|
||||
This software is governed under the combined terms of the following two licenses:
|
||||
|
||||
## The Chaos License (GLP)
|
||||
|
||||
This software is released under the terms of the Chaos License. In cases where the terms of the
|
||||
license are unclear, refer to the [Fuck Around and Find Out
|
||||
License](https://git.sr.ht/~boringcactus/fafol/tree/master/LICENSE-v0.2.md).
|
||||
|
||||
## The Butlerian Jihad License (DUN)
|
||||
|
||||
If you feed this code into an LLM, I will fuck you up.
|
||||
|
|
2
VERSION
2
VERSION
|
@ -1 +1 @@
|
|||
1.618033988
|
||||
1.6180339887
|
||||
|
|
|
@ -27,13 +27,13 @@ struct Cli {
|
|||
|
||||
fn main() {
|
||||
let cli = Cli::parse();
|
||||
if let Some(ts) = cli.input {
|
||||
if let Ok(ts) = Julid::from_str(&ts) {
|
||||
if let Some(julid_string_input) = cli.input {
|
||||
if let Ok(ts) = Julid::from_str(&julid_string_input) {
|
||||
println!("Created at:\t\t{}", ts.created_at());
|
||||
println!("Monotonic counter:\t{}", ts.counter());
|
||||
println!("Random:\t\t\t{}", ts.random());
|
||||
} else {
|
||||
eprintln!("Could not parse input '{}' as a Julid", ts);
|
||||
eprintln!("Could not parse input '{julid_string_input}' as a Julid");
|
||||
std::process::exit(1);
|
||||
}
|
||||
} else {
|
||||
|
|
31
src/julid.rs
31
src/julid.rs
|
@ -6,20 +6,11 @@ use std::{
|
|||
|
||||
use rand::{random, thread_rng, Rng};
|
||||
|
||||
use crate::{base32, DecodeError};
|
||||
use crate::{base32, DecodeError, COUNTER_BITS, RANDOM_BITS, TIME_BITS, UNIQUE_BITS};
|
||||
|
||||
/// This is used to ensure monotonicity for new IDs.
|
||||
static LAST_MSB: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
/// The number of bits in a Julid's time portion
|
||||
const TIME_BITS: u8 = 48;
|
||||
/// The number of bits in the monotonic counter for intra-millisecond IDs
|
||||
const COUNTER_BITS: u8 = 16;
|
||||
/// The number of random bits + bits in the monotonic counter
|
||||
const UNIQUE_BITS: u8 = 80;
|
||||
/// The number of fully random bits
|
||||
const RANDOM_BITS: u8 = 64;
|
||||
|
||||
macro_rules! bitmask {
|
||||
($len:expr) => {
|
||||
((1 << $len) - 1)
|
||||
|
@ -74,6 +65,14 @@ impl Julid {
|
|||
}
|
||||
}
|
||||
|
||||
/// Return a new Julid with the given timestamp (in milliseconds), no
|
||||
/// counter bits set, and 64 random lower bits.
|
||||
pub fn at(ts_ms: u64) -> Self {
|
||||
let hi = ts_ms << COUNTER_BITS;
|
||||
let lo = random();
|
||||
(hi, lo).into()
|
||||
}
|
||||
|
||||
/// The 'Alpha Julid'.
|
||||
///
|
||||
/// The Alpha Julid is special form of Julid that is specified to have
|
||||
|
@ -161,15 +160,7 @@ impl Julid {
|
|||
/// assert!(result.is_ok());
|
||||
/// assert_eq!(&result.unwrap().to_string(), text);
|
||||
/// ```
|
||||
pub const fn from_str(encoded: &str) -> Result<Julid, DecodeError> {
|
||||
match base32::decode(encoded) {
|
||||
Ok(int_val) => Ok(Julid(int_val)),
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
|
||||
#[deprecated(since = "1.6.1803398", note = "use `from_str` instead")]
|
||||
pub const fn from_string(encoded: &str) -> Result<Julid, DecodeError> {
|
||||
pub const fn from_str(encoded: &str) -> Result<Self, DecodeError> {
|
||||
match base32::decode(encoded) {
|
||||
Ok(int_val) => Ok(Julid(int_val)),
|
||||
Err(err) => Err(err),
|
||||
|
@ -182,7 +173,7 @@ impl Julid {
|
|||
}
|
||||
|
||||
/// Creates a Julid using the provided bytes array, assumed big-endian.
|
||||
pub const fn from_bytes(bytes: [u8; 16]) -> Julid {
|
||||
pub const fn from_bytes(bytes: [u8; 16]) -> Self {
|
||||
Self(u128::from_be_bytes(bytes))
|
||||
}
|
||||
}
|
||||
|
|
13
src/lib.rs
13
src/lib.rs
|
@ -11,12 +11,25 @@ pub mod serde;
|
|||
/// Traits from the SQLx crate for getting Julids into and out of SQLite
|
||||
/// databases from normal Rust applications. (feature `sqlx` (default))
|
||||
pub mod sqlx;
|
||||
#[cfg(feature = "uuid")]
|
||||
/// UUIDv7s are almost as good as Julids, and can be interconverted almost
|
||||
/// perfectly.
|
||||
pub mod uuid;
|
||||
|
||||
#[doc(inline)]
|
||||
pub use base32::DecodeError;
|
||||
#[doc(inline)]
|
||||
pub use julid::Julid;
|
||||
|
||||
/// 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;
|
||||
/// The number of fully random bits
|
||||
pub const RANDOM_BITS: u8 = 64;
|
||||
|
||||
/// This `unsafe extern "C"` function is the main entry point into the loadable
|
||||
/// SQLite extension. By default, it and the `plugin` module it depends on will
|
||||
/// not be built. Build with `cargo build --features plugin`
|
||||
|
|
94
src/uuid.rs
Normal file
94
src/uuid.rs
Normal file
|
@ -0,0 +1,94 @@
|
|||
use std::fmt;
|
||||
|
||||
use uuid::{Uuid, Variant};
|
||||
|
||||
use crate::Julid;
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
Ok(id.as_u64_pair().into())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
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 crate::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 = j1.as_uuid();
|
||||
let ju1 = Julid::from_uuid(u1).unwrap();
|
||||
// casting a julid to a uuid alters the counter and entropy bits slightly, so
|
||||
// the original julid and one derived from a uuid made from it won't be the
|
||||
// same.
|
||||
assert_ne!(j1, ju1);
|
||||
|
||||
let u2 = ju1.as_uuid();
|
||||
let ju2 = Julid::from_uuid(u2).unwrap();
|
||||
// but once we've made that alteration, we've reached the fixed point
|
||||
assert_eq!(ju1, ju2);
|
||||
assert_eq!(u1, u2);
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue