Compare commits
16 commits
Author | SHA1 | Date | |
---|---|---|---|
|
e333ea5263 | ||
|
8bbad764c3 | ||
|
02b99ba010 | ||
|
66621bb8c7 | ||
|
dd6b28a039 | ||
|
acb32e56a6 | ||
|
eabcc26276 | ||
|
1e93d0b1e4 | ||
|
7dfe1ffb94 | ||
|
e0150a2161 | ||
|
a54d704300 | ||
|
705afc19e9 | ||
|
6a9e43feee | ||
|
f2ade6d85e | ||
|
34f85a95a0 | ||
|
d9e68f057c |
13 changed files with 359 additions and 171 deletions
45
Cargo.toml
45
Cargo.toml
|
@ -1,22 +1,30 @@
|
||||||
[package]
|
[package]
|
||||||
name = "julid-rs"
|
name = "julid-rs"
|
||||||
version = "1.6.1803"
|
# 1.61803398874989484
|
||||||
|
#----------------^
|
||||||
|
version = "1.6.1803398874989"
|
||||||
authors = ["Joe Ardent <code@ardent.nebcorp.com>"]
|
authors = ["Joe Ardent <code@ardent.nebcorp.com>"]
|
||||||
edition = "2021"
|
edition = "2024"
|
||||||
keywords = ["ulid", "library", "sqlite", "extension", "julid"]
|
keywords = ["ulid", "sqlite", "julid", "uuid", "guid"]
|
||||||
|
|
||||||
description = "A crate and loadable extension for SQLite that provides Joe's ULIDs."
|
description = "A crate and loadable extension for SQLite that provides Joe's ULIDs."
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license-file = "LICENSE.md"
|
license-file = "LICENSE.md"
|
||||||
repository = "https://gitlab.com/nebkor/julid"
|
repository = "https://git.kittencollective.com/nebkor/julid-rs"
|
||||||
|
|
||||||
|
[package.metadata.docs.rs]
|
||||||
|
all-features = true
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["chrono", "serde", "sqlx"] # just the regular crate
|
default = ["serde", "sqlx", "cli", "std", "chrono"] # no uuid or sqlite plugin
|
||||||
chrono = ["dep:chrono"]
|
chrono = ["dep:chrono"]
|
||||||
|
cli = ["dep:clap", "chrono"]
|
||||||
serde = ["dep:serde"]
|
serde = ["dep:serde"]
|
||||||
sqlx = ["dep:sqlx"]
|
sqlx = ["dep:sqlx"]
|
||||||
|
std = ["chrono/std", "serde?/alloc"]
|
||||||
|
uuid = ["dep:uuid"]
|
||||||
|
|
||||||
# WARNING! don't enable this feature in your project's Cargo.toml if using julid-rs as a dependency;
|
# WARNING! don't enable this feature in your project's Cargo.toml if using julid-rs as a Rust dependency;
|
||||||
# see https://gitlab.com/nebkor/julid/-/issues/1
|
# see https://gitlab.com/nebkor/julid/-/issues/1
|
||||||
plugin = ["dep:sqlite-loadable"] # builds libjulid.* for loading into sqlite
|
plugin = ["dep:sqlite-loadable"] # builds libjulid.* for loading into sqlite
|
||||||
|
|
||||||
|
@ -27,18 +35,25 @@ crate-type = ["cdylib", "rlib"]
|
||||||
[dependencies]
|
[dependencies]
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
|
|
||||||
# for the CLI
|
|
||||||
clap = { version = "4.3", default-features = false, features = ["help", "usage", "std", "derive"] }
|
|
||||||
|
|
||||||
# all other deps are optional
|
# all other deps are optional
|
||||||
chrono = { version = "0.4", optional = true, default-features = false, features = ["std", "time"] }
|
chrono = { version = "0.4", default-features = false, features = ["std"], optional = true }
|
||||||
serde = { version = "1.0", features = ["derive"], optional = true }
|
# for the CLI
|
||||||
sqlx = { version = "0.7", features = ["sqlite"], default-features = false, optional = true }
|
clap = { version = "4", default-features = false, features = ["help", "usage", "std", "derive"], optional = true }
|
||||||
|
serde = { version = "1.0", default-features = false, features = ["derive"], optional = true }
|
||||||
|
sqlx = { version = "0.8", features = ["sqlite"], default-features = false, optional = true }
|
||||||
sqlite-loadable = { version = "0.0.5", 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", default-features = false, features = ["v4"] }
|
||||||
|
julid-rs = { path = ".", features = ["uuid"] }
|
||||||
|
|
||||||
|
[[bench]]
|
||||||
|
name = "simple"
|
||||||
|
harness = false
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "julid-gen"
|
name = "julid-gen"
|
||||||
path = "src/bin/gen.rs"
|
path = "src/bin/gen.rs"
|
||||||
|
required-features = ["chrono", "cli"]
|
||||||
[package.metadata.docs.rs]
|
|
||||||
all-features = true
|
|
||||||
|
|
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
|
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 are unclear, refer to the [Fuck Around and Find Out
|
||||||
License](https://git.sr.ht/~boringcactus/fafol/tree/master/LICENSE-v0.2.md).
|
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.
|
||||||
|
|
117
README.md
117
README.md
|
@ -20,6 +20,8 @@ sqlite> select datetime(julid_timestamp(julid_new()), 'auto');
|
||||||
2023-07-27 17:47:50
|
2023-07-27 17:47:50
|
||||||
sqlite> select julid_counter(julid_new());
|
sqlite> select julid_counter(julid_new());
|
||||||
0
|
0
|
||||||
|
sqlite> select julid_string();
|
||||||
|
01HM4WJ7T90001P8SN9898FBTN
|
||||||
```
|
```
|
||||||
|
|
||||||
Crates.io: <https://crates.io/crates/julid-rs>
|
Crates.io: <https://crates.io/crates/julid-rs>
|
||||||
|
@ -28,6 +30,9 @@ Docs.rs: <https://docs.rs/julid-rs/latest/julid/>
|
||||||
|
|
||||||
Blog post: <https://proclamations.nebcorp-hias.com/sundries/presenting-julids/>
|
Blog post: <https://proclamations.nebcorp-hias.com/sundries/presenting-julids/>
|
||||||
|
|
||||||
|
As of June of 2025, they can also be converted to and from [version 7
|
||||||
|
UUIDs](https://www.ietf.org/rfc/rfc9562.html#name-uuid-version-7), though some precision in the
|
||||||
|
intra-millisecond counter is lost when going to a UUID, via the `uuid` optional feature.
|
||||||
|
|
||||||
## A slightly deeper look
|
## A slightly deeper look
|
||||||
|
|
||||||
|
@ -71,9 +76,8 @@ are always fresh, it's not possible to easily guess a valid Julid if you already
|
||||||
# How to use
|
# How to use
|
||||||
|
|
||||||
The Julid crate can be used in two different ways: as a regular Rust library, declared in your Rust
|
The Julid crate can be used in two different ways: as a regular Rust library, declared in your Rust
|
||||||
project's `Cargo.toml` file (say, by running `cargo add julid-rs`), and used as shown above. There's
|
project's `Cargo.toml` file (say, by running `cargo add julid-rs`), and used as shown in the sample
|
||||||
a rudimentary [benchmark](https://gitlab.com/nebkor/julid/-/blob/main/examples/benchmark.rs) example
|
commandline program (see below). But the primary use case for me was as a loadable
|
||||||
in the repo that shows off most of the Rust API. But the primary use case for me was as a loadable
|
|
||||||
SQLite extension. Both are covered in the [documentation](https://docs.rs/julid-rs/latest/julid/),
|
SQLite extension. Both are covered in the [documentation](https://docs.rs/julid-rs/latest/julid/),
|
||||||
but let's go over them here, starting with the extension.
|
but let's go over them here, starting with the extension.
|
||||||
|
|
||||||
|
@ -83,6 +87,8 @@ The extension, when loaded into SQLite, provides the following functions:
|
||||||
|
|
||||||
* `julid_new()`: create a new Julid and return it as a 16-byte
|
* `julid_new()`: create a new Julid and return it as a 16-byte
|
||||||
[blob](https://www.sqlite.org/datatype3.html#storage_classes_and_datatypes)
|
[blob](https://www.sqlite.org/datatype3.html#storage_classes_and_datatypes)
|
||||||
|
* `julid_string()`: create a new Julid and return it as a 26-character [base-32
|
||||||
|
Crockford](https://en.wikipedia.org/wiki/Base32)-encoded string
|
||||||
* `julid_seconds(julid)`: get the number seconds (as a 64-bit float) since the UNIX epoch that this
|
* `julid_seconds(julid)`: get the number seconds (as a 64-bit float) since the UNIX epoch that this
|
||||||
julid was created (convenient for passing to the builtin `datetime()` function)
|
julid was created (convenient for passing to the builtin `datetime()` function)
|
||||||
* `julid_counter(julid)`: show the value of this julid's monotonic counter
|
* `julid_counter(julid)`: show the value of this julid's monotonic counter
|
||||||
|
@ -119,11 +125,11 @@ For a table created like:
|
||||||
create table if not exists watches (
|
create table if not exists watches (
|
||||||
id blob not null primary key default (julid_new()),
|
id blob not null primary key default (julid_new()),
|
||||||
kind int not null, -- enum for movie or tv show or whatev
|
kind int not null, -- enum for movie or tv show or whatev
|
||||||
title text not null, -- this has a secondary index
|
title text not null,
|
||||||
length int,
|
length int,
|
||||||
release_date int,
|
release_date date,
|
||||||
added_by blob not null,
|
added_by blob not null,
|
||||||
last_updated int not null default (unixepoch()),
|
last_updated date not null default CURRENT_TIMESTAMP,
|
||||||
foreign key (added_by) references users (id)
|
foreign key (added_by) references users (id)
|
||||||
);
|
);
|
||||||
```
|
```
|
||||||
|
@ -149,86 +155,53 @@ the C interface, which is inherently unsafe. If you are not building the plugin,
|
||||||
## Inside a Rust program
|
## Inside a Rust program
|
||||||
|
|
||||||
Of course, you can also use it outside of a database; the `Julid` type is publicly exported. There's
|
Of course, you can also use it outside of a database; the `Julid` type is publicly exported. There's
|
||||||
a simple benchmark in the examples folder of the repo, the important parts of which look like:
|
a simple commandline program in `src/bin/gen.rs`, and can be run like `cargo run --bin julid-gen`
|
||||||
|
(or you can `cargo install julid-rs` to get the `julid-gen` program installed on your computer),
|
||||||
|
which will generate and print one Julid. If you want to see its component pieces, grab the Julid
|
||||||
|
printed from it, and then run it with the `-d` flag:
|
||||||
|
|
||||||
``` rust
|
```
|
||||||
use julid::Julid;
|
$ julid-gen 4
|
||||||
|
01HV2G2ATR000CJ2WESB7CVC19
|
||||||
fn main() {
|
01HV2G2ATR000K1AGQPKMX5H0M
|
||||||
/* snip some stuff */
|
01HV2G2ATR001CM27S59BHZ25G
|
||||||
|
01HV2G2ATR001WPJ8BS7PZHE6A
|
||||||
let start = Instant::now();
|
$ julid-gen -d 01HV2G2ATR001WPJ8BS7PZHE6A
|
||||||
for _ in 0..num {
|
Created at: 2024-04-09 22:36:11.992 UTC
|
||||||
v.push(Julid::new());
|
Monotonic counter: 3
|
||||||
}
|
Random: 14648252224908081354
|
||||||
let end = Instant::now();
|
|
||||||
let dur = (end - start).as_micros();
|
|
||||||
|
|
||||||
for id in v.iter() {
|
|
||||||
eprintln!(
|
|
||||||
"{id}: created_at {}; counter: {}; sortable: {}",
|
|
||||||
id.created_at(),
|
|
||||||
id.counter(),
|
|
||||||
id.sortable()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
println!("{num} Julids generated in {dur}us");
|
|
||||||
```
|
```
|
||||||
|
|
||||||
If you were to run it on a computer like mine (AMD Ryzen 9 3900X, 12-core, 2.2-4.6 GHz), you might
|
The help is useful:
|
||||||
see something like this:
|
|
||||||
|
|
||||||
``` text
|
|
||||||
$ cargo run --example=benchmark --release -- -n 30000 2> /dev/null
|
|
||||||
30000 Julids generated in 1240us
|
|
||||||
```
|
```
|
||||||
|
|
||||||
That's about 24,000 IDs/millisecond; 24 *MILLION* per second!
|
|
||||||
|
|
||||||
The default optional Cargo features include implementations of traits for getting Julids into and
|
|
||||||
out of SQLite with [SQLx](https://github.com/launchbadge/sqlx), and for generally
|
|
||||||
serializing/deserializing with [Serde](https://serde.rs/), via the `sqlx` and `serde` features,
|
|
||||||
respectively. One final default optional feature, `chrono`, uses the Chrono crate to return the
|
|
||||||
timestamp as a [`DateTime`](https://docs.rs/chrono/latest/chrono/struct.DateTime.html) by adding a
|
|
||||||
`created_at(&self)` method to `Julid`.
|
|
||||||
|
|
||||||
Something to note: don't enable the `plugin` feature in your Cargo.toml if you're using this crate
|
|
||||||
inside your Rust application, especially if you're *also* loading it as an extension in SQLite in
|
|
||||||
your application. You'll get a long and confusing runtime panic due to there being multiple
|
|
||||||
entrypoints defined with the same name.
|
|
||||||
|
|
||||||
## On the command line
|
|
||||||
|
|
||||||
An even simpler program than the benchmark called `julid-gen` is available to install via cargo:
|
|
||||||
|
|
||||||
`cargo install julid-rs --no-default-features`
|
|
||||||
|
|
||||||
And then using it is as simple as,
|
|
||||||
|
|
||||||
``` text
|
|
||||||
$ julid-gen -h
|
$ julid-gen -h
|
||||||
Generate and print Julids
|
Generate, print, and parse Julids
|
||||||
|
|
||||||
Usage: julid-gen [NUM]
|
Usage: julid-gen [OPTIONS] [NUM]
|
||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
[NUM] Number of Julids to generate [default: 1]
|
[NUM] Number of Julids to generate [default: 1]
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
-h, --help Print help
|
-d, --decode <INPUT> Print the components of the given Julid
|
||||||
-V, --version Print version
|
-a, --answer The answer to the meaning of Julid
|
||||||
|
-h, --help Print help
|
||||||
$ julid-gen
|
-V, --version Print version
|
||||||
01H9DYRVDX0001X0RE5Y7XFGBC
|
|
||||||
|
|
||||||
$ julid-gen 5
|
|
||||||
01H9DYT48E000EK2EH7P67N8GG
|
|
||||||
01H9DYT48E000ZBKXVZ91HEZX4
|
|
||||||
01H9DYT48E0012VX89PYX4HDKP
|
|
||||||
01H9DYT48E001GE29AWCH1RDCM
|
|
||||||
01H9DYT48E0028CDHNVC59KKHQ
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The whole program is just 34 lines, so check it out.
|
||||||
|
|
||||||
|
The default optional Cargo features include implementations of traits for getting Julids into and
|
||||||
|
out of SQLite with [SQLx](https://github.com/launchbadge/sqlx), and for generally
|
||||||
|
serializing/deserializing with [Serde](https://serde.rs/), via the `sqlx` and `serde` features,
|
||||||
|
respectively.
|
||||||
|
|
||||||
|
Something to note: don't enable the `plugin` feature in your Cargo.toml if you're using this crate
|
||||||
|
inside your Rust application, especially if you're *also* loading it as an extension in SQLite in
|
||||||
|
your application. You'll get a long and confusing runtime panic due to there being multiple
|
||||||
|
entrypoints defined with the same name.
|
||||||
|
|
||||||
# Thanks
|
# Thanks
|
||||||
|
|
||||||
This project wouldn't have happened without a lot of inspiration (and a little shameless stealing)
|
This project wouldn't have happened without a lot of inspiration (and a little shameless stealing)
|
||||||
|
|
2
VERSION
2
VERSION
|
@ -1 +1 @@
|
||||||
1.61803
|
1.61803398874989
|
||||||
|
|
16
benches/simple.rs
Normal file
16
benches/simple.rs
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
use divan::black_box;
|
||||||
|
use julid::Julid;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
divan::main();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[divan::bench]
|
||||||
|
fn jbench() {
|
||||||
|
for _ in 0..1_000_000 {
|
||||||
|
let x = black_box(Julid::new());
|
||||||
|
if x < 1u128.into() {
|
||||||
|
println!("that's weird");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,37 +0,0 @@
|
||||||
use std::time::Instant;
|
|
||||||
|
|
||||||
use clap::Parser;
|
|
||||||
use julid::Julid;
|
|
||||||
|
|
||||||
#[derive(Debug, Parser)]
|
|
||||||
struct Cli {
|
|
||||||
#[clap(
|
|
||||||
long,
|
|
||||||
short,
|
|
||||||
help = "Number of Julids to generate",
|
|
||||||
default_value_t = 2_000
|
|
||||||
)]
|
|
||||||
pub num: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
let cli = Cli::parse();
|
|
||||||
let num = cli.num;
|
|
||||||
let mut v = Vec::with_capacity(num);
|
|
||||||
let start = Instant::now();
|
|
||||||
for _ in 0..num {
|
|
||||||
v.push(Julid::new());
|
|
||||||
}
|
|
||||||
let end = Instant::now();
|
|
||||||
let dur = (end - start).as_micros();
|
|
||||||
|
|
||||||
for id in v.iter() {
|
|
||||||
eprintln!(
|
|
||||||
"{id}: created_at {}; counter: {}; sortable: {}",
|
|
||||||
id.created_at(),
|
|
||||||
id.counter(),
|
|
||||||
id.sortable()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
println!("{num} Julids generated in {dur}us");
|
|
||||||
}
|
|
|
@ -65,7 +65,7 @@ impl fmt::Display for DecodeError {
|
||||||
DecodeError::InvalidLength(len) => format!("invalid length: {len}"),
|
DecodeError::InvalidLength(len) => format!("invalid length: {len}"),
|
||||||
DecodeError::InvalidChar(c) => format!("invalid character: {c}"),
|
DecodeError::InvalidChar(c) => format!("invalid character: {c}"),
|
||||||
};
|
};
|
||||||
write!(f, "{}", text)
|
write!(f, "{text}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,17 +2,50 @@ use clap::Parser;
|
||||||
use julid::Julid;
|
use julid::Julid;
|
||||||
|
|
||||||
#[derive(Debug, Parser)]
|
#[derive(Debug, Parser)]
|
||||||
#[command(author, version = "1.61803", about = "Generate and print Julids")]
|
#[command(
|
||||||
|
author,
|
||||||
|
version = "1.618033988",
|
||||||
|
about = "Generate, print, and parse Julids"
|
||||||
|
)]
|
||||||
struct Cli {
|
struct Cli {
|
||||||
|
#[clap(
|
||||||
|
help = "Print the components of the given Julid",
|
||||||
|
short = 'd',
|
||||||
|
long = "decode"
|
||||||
|
)]
|
||||||
|
pub input: Option<String>,
|
||||||
#[clap(help = "Number of Julids to generate", default_value_t = 1)]
|
#[clap(help = "Number of Julids to generate", default_value_t = 1)]
|
||||||
pub num: usize,
|
pub num: usize,
|
||||||
|
#[clap(
|
||||||
|
help = "The answer to the meaning of Julid",
|
||||||
|
default_value_t = false,
|
||||||
|
short,
|
||||||
|
long
|
||||||
|
)]
|
||||||
|
pub answer: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
let num = cli.num;
|
if let Some(julid_string_input) = cli.input {
|
||||||
|
if let Ok(ts) = Julid::from_str(&julid_string_input) {
|
||||||
for _ in 0..num {
|
println!("Created at:\t\t{}", ts.created_at());
|
||||||
println!("{}", Julid::new());
|
println!("Monotonic counter:\t{}", ts.counter());
|
||||||
|
println!("Random:\t\t\t{}", ts.random());
|
||||||
|
} else {
|
||||||
|
eprintln!("Could not parse input '{julid_string_input}' as a Julid");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Just print some Julids
|
||||||
|
let num = cli.num;
|
||||||
|
for _ in 0..num {
|
||||||
|
let j = if cli.answer {
|
||||||
|
42u128.into()
|
||||||
|
} else {
|
||||||
|
Julid::new()
|
||||||
|
};
|
||||||
|
println!("{j}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
31
src/julid.rs
31
src/julid.rs
|
@ -6,20 +6,11 @@ use std::{
|
||||||
|
|
||||||
use rand::{random, thread_rng, Rng};
|
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.
|
/// This is used to ensure monotonicity for new IDs.
|
||||||
static LAST_MSB: AtomicU64 = AtomicU64::new(0);
|
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 {
|
macro_rules! bitmask {
|
||||||
($len:expr) => {
|
($len:expr) => {
|
||||||
((1 << $len) - 1)
|
((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'.
|
||||||
///
|
///
|
||||||
/// The Alpha Julid is special form of Julid that is specified to have
|
/// The Alpha Julid is special form of Julid that is specified to have
|
||||||
|
@ -140,7 +139,7 @@ impl Julid {
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// use julid::julid::Julid;
|
/// use julid::julid::Julid;
|
||||||
/// let text = "01D39ZY06FGSCTVN4T2V9PKHFZ";
|
/// let text = "01D39ZY06FGSCTVN4T2V9PKHFZ";
|
||||||
/// let id = Julid::from_string(text).unwrap();
|
/// let id = Julid::from_str(text).unwrap();
|
||||||
///
|
///
|
||||||
/// assert_eq!(&id.to_string(), text);
|
/// assert_eq!(&id.to_string(), text);
|
||||||
/// ```
|
/// ```
|
||||||
|
@ -157,12 +156,12 @@ impl Julid {
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// use julid::julid::Julid;
|
/// use julid::julid::Julid;
|
||||||
/// let text = "01D39ZY06FGSCTVN4T2V9PKHFZ";
|
/// let text = "01D39ZY06FGSCTVN4T2V9PKHFZ";
|
||||||
/// let result = Julid::from_string(text);
|
/// let result = Julid::from_str(text);
|
||||||
///
|
///
|
||||||
/// assert!(result.is_ok());
|
/// assert!(result.is_ok());
|
||||||
/// assert_eq!(&result.unwrap().to_string(), text);
|
/// assert_eq!(&result.unwrap().to_string(), text);
|
||||||
/// ```
|
/// ```
|
||||||
pub const fn from_string(encoded: &str) -> Result<Julid, DecodeError> {
|
pub const fn from_str(encoded: &str) -> Result<Self, DecodeError> {
|
||||||
match base32::decode(encoded) {
|
match base32::decode(encoded) {
|
||||||
Ok(int_val) => Ok(Julid(int_val)),
|
Ok(int_val) => Ok(Julid(int_val)),
|
||||||
Err(err) => Err(err),
|
Err(err) => Err(err),
|
||||||
|
@ -175,7 +174,7 @@ impl Julid {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a Julid using the provided bytes array, assumed big-endian.
|
/// 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))
|
Self(u128::from_be_bytes(bytes))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -232,7 +231,7 @@ impl FromStr for Julid {
|
||||||
type Err = DecodeError;
|
type Err = DecodeError;
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
Julid::from_string(s)
|
Julid::from_str(s)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -249,7 +248,7 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_static() {
|
fn test_static() {
|
||||||
let s = Julid(0x41414141414141414141414141414141).as_string();
|
let s = Julid(0x41414141414141414141414141414141).as_string();
|
||||||
let u = Julid::from_string(&s).unwrap();
|
let u = Julid::from_str(&s).unwrap();
|
||||||
assert_eq!(&s, "21850M2GA1850M2GA1850M2GA1");
|
assert_eq!(&s, "21850M2GA1850M2GA1850M2GA1");
|
||||||
assert_eq!(u.0, 0x41414141414141414141414141414141);
|
assert_eq!(u.0, 0x41414141414141414141414141414141);
|
||||||
}
|
}
|
||||||
|
|
64
src/lib.rs
64
src/lib.rs
|
@ -2,26 +2,52 @@
|
||||||
use sqlite_loadable::prelude::{c_char, c_uint, sqlite3, sqlite3_api_routines};
|
use sqlite_loadable::prelude::{c_char, c_uint, sqlite3, sqlite3_api_routines};
|
||||||
|
|
||||||
mod base32;
|
mod base32;
|
||||||
|
|
||||||
|
/// Contains the [`Julid`] type, which is publicly exported at the top level.
|
||||||
pub mod julid;
|
pub mod julid;
|
||||||
#[cfg(feature = "serde")]
|
|
||||||
/// Serialization into bytes, and deserialization from a variety of formats,
|
/// Serialization into bytes, and deserialization from a variety of formats,
|
||||||
/// with Serde (feature `serde` (default))
|
/// with Serde (feature `serde` (default))
|
||||||
|
#[cfg(feature = "serde")]
|
||||||
pub mod serde;
|
pub mod serde;
|
||||||
#[cfg(feature = "sqlx")]
|
|
||||||
/// Traits from the SQLx crate for getting Julids into and out of SQLite
|
/// Traits from the SQLx crate for getting Julids into and out of SQLite
|
||||||
/// databases from normal Rust applications. (feature `sqlx` (default))
|
/// databases from normal Rust applications. (feature `sqlx` (default))
|
||||||
|
#[cfg(feature = "sqlx")]
|
||||||
pub mod sqlx;
|
pub mod sqlx;
|
||||||
|
|
||||||
|
/// UUIDv7s are almost as good as Julids, and can be interconverted almost
|
||||||
|
/// perfectly. (feature `uuid` (non-default))
|
||||||
|
///
|
||||||
|
/// See the [`Julid::as_uuid`] and [`Julid::from_uuid`] methods for
|
||||||
|
/// converting a Julid to a UUID and constructing a Julid from a UUID
|
||||||
|
/// respectively.
|
||||||
|
#[cfg(feature = "uuid")]
|
||||||
|
pub mod uuid;
|
||||||
|
|
||||||
#[doc(inline)]
|
#[doc(inline)]
|
||||||
pub use base32::DecodeError;
|
pub use base32::DecodeError;
|
||||||
#[doc(inline)]
|
#[doc(inline)]
|
||||||
pub use julid::Julid;
|
pub use julid::Julid;
|
||||||
|
|
||||||
|
/// The number of bits in a Julid's millisecond timestamp (48)
|
||||||
|
pub const TIME_BITS: u8 = 48;
|
||||||
|
/// The number of bits in the monotonic counter for intra-millisecond IDs (16)
|
||||||
|
pub const COUNTER_BITS: u8 = 16;
|
||||||
|
/// The number of random bits + bits in the monotonic counter (80)
|
||||||
|
pub const UNIQUE_BITS: u8 = 80;
|
||||||
|
/// The number of fully random bits (64)
|
||||||
|
pub const RANDOM_BITS: u8 = 64;
|
||||||
|
|
||||||
/// This `unsafe extern "C"` function is the main entry point into the loadable
|
/// 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
|
/// SQLite extension. By default, it and the `plugin` module it depends on will
|
||||||
/// not be built. Build with `cargo build --features plugin`
|
/// not be built. Build with `cargo build --features plugin`
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
/// This is FFI; it's inherently unsafe. But this function is called by
|
||||||
|
/// sqlite, not by a user, so it should be OK.
|
||||||
#[cfg(feature = "plugin")]
|
#[cfg(feature = "plugin")]
|
||||||
#[no_mangle]
|
#[unsafe(no_mangle)]
|
||||||
pub unsafe extern "C" fn sqlite3_julid_init(
|
pub unsafe extern "C" fn sqlite3_julid_init(
|
||||||
db: *mut sqlite3,
|
db: *mut sqlite3,
|
||||||
_pz_err_msg: *mut *mut c_char,
|
_pz_err_msg: *mut *mut c_char,
|
||||||
|
@ -35,24 +61,30 @@ pub unsafe extern "C" fn sqlite3_julid_init(
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The code for the SQLite plugin is kept in this module, and exposed via the
|
/// The code for the SQLite plugin is kept in this module, and exposed via the
|
||||||
/// `sqlite3_julid_init` function (feature `plugin`)
|
/// [`sqlite3_julid_init`] function (feature `plugin` (non-default))
|
||||||
#[cfg(feature = "plugin")]
|
#[cfg(feature = "plugin")]
|
||||||
pub mod sqlite_plugin {
|
pub mod sqlite_plugin {
|
||||||
use sqlite_loadable::{
|
use sqlite_loadable::{
|
||||||
api, define_scalar_function,
|
api, define_scalar_function,
|
||||||
prelude::{sqlite3, sqlite3_context, sqlite3_value, FunctionFlags},
|
prelude::{sqlite3_context, sqlite3_value, FunctionFlags},
|
||||||
Result,
|
Result,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
pub(super) fn init_rs(db: *mut sqlite3) -> Result<()> {
|
pub(super) fn init_rs(db: *mut sqlite3) -> Result<()> {
|
||||||
let flags = FunctionFlags::UTF8 | FunctionFlags::DETERMINISTIC;
|
let flags = FunctionFlags::UTF8 | FunctionFlags::DETERMINISTIC | FunctionFlags::INNOCUOUS;
|
||||||
define_scalar_function(db, "julid_new", 0, julid_new, FunctionFlags::INNOCUOUS)?;
|
define_scalar_function(db, "julid_new", 0, julid_new, FunctionFlags::INNOCUOUS)?;
|
||||||
define_scalar_function(db, "julid_seconds", 1, julid_seconds, flags)?;
|
define_scalar_function(db, "julid_seconds", 1, julid_seconds, flags)?;
|
||||||
define_scalar_function(db, "julid_counter", 1, julid_counter, flags)?;
|
define_scalar_function(db, "julid_counter", 1, julid_counter, flags)?;
|
||||||
define_scalar_function(db, "julid_sortable", 1, julid_sortable, flags)?;
|
define_scalar_function(db, "julid_sortable", 1, julid_sortable, flags)?;
|
||||||
define_scalar_function(db, "julid_string", 1, julid_string, flags)?;
|
define_scalar_function(
|
||||||
|
db,
|
||||||
|
"julid_string",
|
||||||
|
-1,
|
||||||
|
julid_string,
|
||||||
|
FunctionFlags::UTF8 | FunctionFlags::INNOCUOUS,
|
||||||
|
)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -76,13 +108,17 @@ pub mod sqlite_plugin {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the human-readable base32 Crockford encoding of this Julid.
|
/// Return the human-readable base32 Crockford encoding of the given Julid,
|
||||||
|
/// or create a new one if no arguments.
|
||||||
|
///
|
||||||
/// ```text
|
/// ```text
|
||||||
/// sqlite> select julid_string(julid_new());
|
/// sqlite> select julid_string(julid_new());
|
||||||
/// 01H6C7D9CT00009TF3EXXJHX4Y
|
/// 01H6C7D9CT00009TF3EXXJHX4Y
|
||||||
|
/// sqlite> select julid_string();
|
||||||
|
/// 01HJSHZ0PN000EKP3H94R6TPWH
|
||||||
/// ```
|
/// ```
|
||||||
pub fn julid_string(context: *mut sqlite3_context, id: &[*mut sqlite3_value]) -> Result<()> {
|
pub fn julid_string(context: *mut sqlite3_context, id: &[*mut sqlite3_value]) -> Result<()> {
|
||||||
if let Some(value) = id.get(0) {
|
if let Some(value) = id.first() {
|
||||||
let id = api::value_blob(value);
|
let id = api::value_blob(value);
|
||||||
let bytes: [u8; 16] = id.try_into().map_err(|_| {
|
let bytes: [u8; 16] = id.try_into().map_err(|_| {
|
||||||
sqlite_loadable::Error::new_message("Could not convert given value to Julid")
|
sqlite_loadable::Error::new_message("Could not convert given value to Julid")
|
||||||
|
@ -90,9 +126,7 @@ pub mod sqlite_plugin {
|
||||||
let id: Julid = bytes.into();
|
let id: Julid = bytes.into();
|
||||||
api::result_text(context, id.as_string())?;
|
api::result_text(context, id.as_string())?;
|
||||||
} else {
|
} else {
|
||||||
return Err(sqlite_loadable::Error::new_message(
|
api::result_text(context, Julid::new().as_string())?;
|
||||||
"Could not convert empty Julid to string",
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -108,7 +142,7 @@ pub mod sqlite_plugin {
|
||||||
/// 2023-07-27 17:47:50
|
/// 2023-07-27 17:47:50
|
||||||
/// ```
|
/// ```
|
||||||
pub fn julid_seconds(context: *mut sqlite3_context, id: &[*mut sqlite3_value]) -> Result<()> {
|
pub fn julid_seconds(context: *mut sqlite3_context, id: &[*mut sqlite3_value]) -> Result<()> {
|
||||||
if let Some(value) = id.get(0) {
|
if let Some(value) = id.first() {
|
||||||
let id = api::value_blob(value);
|
let id = api::value_blob(value);
|
||||||
let bytes: [u8; 16] = id.try_into().map_err(|_| {
|
let bytes: [u8; 16] = id.try_into().map_err(|_| {
|
||||||
sqlite_loadable::Error::new_message("Could not convert given value to Julid")
|
sqlite_loadable::Error::new_message("Could not convert given value to Julid")
|
||||||
|
@ -135,7 +169,7 @@ pub mod sqlite_plugin {
|
||||||
/// 0
|
/// 0
|
||||||
/// ```
|
/// ```
|
||||||
pub fn julid_counter(context: *mut sqlite3_context, id: &[*mut sqlite3_value]) -> Result<()> {
|
pub fn julid_counter(context: *mut sqlite3_context, id: &[*mut sqlite3_value]) -> Result<()> {
|
||||||
if let Some(value) = id.get(0) {
|
if let Some(value) = id.first() {
|
||||||
let id = api::value_blob(value);
|
let id = api::value_blob(value);
|
||||||
let bytes: [u8; 16] = id.try_into().map_err(|_| {
|
let bytes: [u8; 16] = id.try_into().map_err(|_| {
|
||||||
sqlite_loadable::Error::new_message("Could not convert given value to Julid")
|
sqlite_loadable::Error::new_message("Could not convert given value to Julid")
|
||||||
|
@ -159,7 +193,7 @@ pub mod sqlite_plugin {
|
||||||
/// 110787724287475712
|
/// 110787724287475712
|
||||||
/// ```
|
/// ```
|
||||||
pub fn julid_sortable(context: *mut sqlite3_context, id: &[*mut sqlite3_value]) -> Result<()> {
|
pub fn julid_sortable(context: *mut sqlite3_context, id: &[*mut sqlite3_value]) -> Result<()> {
|
||||||
if let Some(value) = id.get(0) {
|
if let Some(value) = id.first() {
|
||||||
let id = api::value_blob(value);
|
let id = api::value_blob(value);
|
||||||
let bytes: [u8; 16] = id.try_into().map_err(|_| {
|
let bytes: [u8; 16] = id.try_into().map_err(|_| {
|
||||||
sqlite_loadable::Error::new_message("Could not convert given value to Julid")
|
sqlite_loadable::Error::new_message("Could not convert given value to Julid")
|
||||||
|
|
10
src/serde.rs
10
src/serde.rs
|
@ -34,6 +34,7 @@ impl<'de> Visitor<'de> for JulidVisitor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "std")]
|
||||||
fn visit_byte_buf<E>(self, v: Vec<u8>) -> Result<Self::Value, E>
|
fn visit_byte_buf<E>(self, v: Vec<u8>) -> Result<Self::Value, E>
|
||||||
where
|
where
|
||||||
E: serde::de::Error,
|
E: serde::de::Error,
|
||||||
|
@ -97,10 +98,13 @@ impl<'de> Deserialize<'de> for Julid {
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
pub mod julid_as_str {
|
pub mod julid_as_str {
|
||||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
#[cfg(feature = "std")]
|
||||||
|
use serde::{Deserialize, Deserializer};
|
||||||
|
use serde::{Serialize, Serializer};
|
||||||
|
|
||||||
use crate::Julid;
|
use crate::Julid;
|
||||||
|
|
||||||
|
/// Serialize a Julid into a String
|
||||||
pub fn serialize<S>(value: &Julid, serializer: S) -> Result<S::Ok, S::Error>
|
pub fn serialize<S>(value: &Julid, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
where
|
where
|
||||||
S: Serializer,
|
S: Serializer,
|
||||||
|
@ -109,11 +113,13 @@ pub mod julid_as_str {
|
||||||
text.serialize(serializer)
|
text.serialize(serializer)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "std")]
|
||||||
|
/// Deserialize a String into a Julid (feature `std` only)
|
||||||
pub fn deserialize<'de, D>(deserializer: D) -> Result<Julid, D::Error>
|
pub fn deserialize<'de, D>(deserializer: D) -> Result<Julid, D::Error>
|
||||||
where
|
where
|
||||||
D: Deserializer<'de>,
|
D: Deserializer<'de>,
|
||||||
{
|
{
|
||||||
let deserialized_str = String::deserialize(deserializer)?;
|
let deserialized_str = String::deserialize(deserializer)?;
|
||||||
Julid::from_string(&deserialized_str).map_err(serde::de::Error::custom)
|
Julid::from_str(&deserialized_str).map_err(serde::de::Error::custom)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
27
src/sqlx.rs
27
src/sqlx.rs
|
@ -3,7 +3,7 @@ use std::borrow::Cow;
|
||||||
use sqlx::{
|
use sqlx::{
|
||||||
encode::IsNull,
|
encode::IsNull,
|
||||||
sqlite::{SqliteArgumentValue, SqliteValueRef},
|
sqlite::{SqliteArgumentValue, SqliteValueRef},
|
||||||
Decode, Encode, Sqlite,
|
Decode, Encode, Sqlite, Value, ValueRef,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::Julid;
|
use crate::Julid;
|
||||||
|
@ -15,18 +15,33 @@ impl sqlx::Type<sqlx::Sqlite> for Julid {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'q> Encode<'q, Sqlite> for Julid {
|
impl<'q> Encode<'q, Sqlite> for Julid {
|
||||||
fn encode_by_ref(&self, args: &mut Vec<SqliteArgumentValue<'q>>) -> IsNull {
|
fn encode_by_ref(
|
||||||
|
&self,
|
||||||
|
args: &mut Vec<SqliteArgumentValue<'q>>,
|
||||||
|
) -> std::result::Result<
|
||||||
|
sqlx::encode::IsNull,
|
||||||
|
std::boxed::Box<(dyn std::error::Error + std::marker::Send + std::marker::Sync + 'static)>,
|
||||||
|
> {
|
||||||
args.push(SqliteArgumentValue::Blob(Cow::Owned(
|
args.push(SqliteArgumentValue::Blob(Cow::Owned(
|
||||||
self.as_bytes().to_vec(),
|
self.as_bytes().to_vec(),
|
||||||
)));
|
)));
|
||||||
IsNull::No
|
Ok(IsNull::No)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Decode<'_, Sqlite> for Julid {
|
impl Decode<'_, Sqlite> for Julid {
|
||||||
fn decode(value: SqliteValueRef<'_>) -> Result<Self, sqlx::error::BoxDynError> {
|
fn decode(value: SqliteValueRef<'_>) -> Result<Self, sqlx::error::BoxDynError> {
|
||||||
let bytes = <&[u8] as Decode<Sqlite>>::decode(value)?;
|
let julid = match <&[u8] as Decode<Sqlite>>::decode(value.to_owned().as_ref()) {
|
||||||
let bytes: [u8; 16] = bytes.try_into().unwrap_or_default();
|
Ok(bytes) => {
|
||||||
Ok(bytes.into())
|
let bytes: [u8; 16] = bytes.try_into().unwrap_or_default();
|
||||||
|
Julid::from_bytes(bytes)
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
let string = <&str as Decode<Sqlite>>::decode(value)?;
|
||||||
|
Julid::from_str(string)?
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(julid)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
126
src/uuid.rs
Normal file
126
src/uuid.rs
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
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)));
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue