2023-05-26 10:00:09 +02:00
|
|
|
//! ID generation utility based on [cuid2]
|
|
|
|
|
2024-04-24 00:37:16 +02:00
|
|
|
use crate::config::CONFIG;
|
2023-08-06 08:34:44 +02:00
|
|
|
use basen::BASE36;
|
2024-04-24 00:02:06 +02:00
|
|
|
use chrono::{DateTime, NaiveDateTime, Utc};
|
2023-05-26 10:00:09 +02:00
|
|
|
use once_cell::sync::OnceCell;
|
2023-07-10 07:39:33 +02:00
|
|
|
use std::cmp;
|
2023-05-26 10:00:09 +02:00
|
|
|
|
2023-06-02 17:55:14 +02:00
|
|
|
static FINGERPRINT: OnceCell<String> = OnceCell::new();
|
|
|
|
static GENERATOR: OnceCell<cuid2::CuidConstructor> = OnceCell::new();
|
2023-05-26 10:00:09 +02:00
|
|
|
|
2023-07-10 07:39:33 +02:00
|
|
|
const TIME_2000: i64 = 946_684_800_000;
|
2024-04-24 00:37:16 +02:00
|
|
|
const TIMESTAMP_LENGTH: u8 = 8;
|
2023-07-10 07:39:33 +02:00
|
|
|
|
2024-04-24 00:37:16 +02:00
|
|
|
/// Initializes Cuid2 generator.
|
|
|
|
fn init_id_generator(length: u8, fingerprint: &str) {
|
2023-07-10 07:39:33 +02:00
|
|
|
FINGERPRINT.get_or_init(move || format!("{}{}", fingerprint, cuid2::create_id()));
|
2023-06-02 17:55:14 +02:00
|
|
|
GENERATOR.get_or_init(move || {
|
|
|
|
cuid2::CuidConstructor::new()
|
2023-07-10 07:39:33 +02:00
|
|
|
// length to pass shoule be greater than or equal to 8.
|
2024-04-24 00:37:16 +02:00
|
|
|
.with_length(cmp::max(length - TIMESTAMP_LENGTH, 8).into())
|
2023-06-02 17:55:14 +02:00
|
|
|
.with_fingerprinter(|| FINGERPRINT.get().unwrap().clone())
|
|
|
|
});
|
2023-05-26 10:00:09 +02:00
|
|
|
}
|
|
|
|
|
2024-04-24 00:37:16 +02:00
|
|
|
/// Returns Cuid2 with the length specified by [init_id_generator].
|
|
|
|
/// It automatically calls [init_id_generator], if the generator has not been initialized.
|
|
|
|
fn create_id(datetime: &NaiveDateTime) -> String {
|
|
|
|
if GENERATOR.get().is_none() {
|
|
|
|
let length = match &CONFIG.cuid {
|
|
|
|
Some(cuid) => cmp::min(cmp::max(cuid.length.unwrap_or(16), 16), 24),
|
|
|
|
None => 16,
|
|
|
|
};
|
|
|
|
let fingerprint = match &CONFIG.cuid {
|
|
|
|
Some(cuid) => cuid.fingerprint.as_deref().unwrap_or_default(),
|
|
|
|
None => "",
|
|
|
|
};
|
|
|
|
init_id_generator(length, fingerprint);
|
2023-05-26 10:00:09 +02:00
|
|
|
}
|
2024-04-24 00:37:16 +02:00
|
|
|
let date_num = cmp::max(0, datetime.and_utc().timestamp_millis() - TIME_2000) as u64;
|
|
|
|
format!(
|
|
|
|
"{:0>8}{}",
|
|
|
|
BASE36.encode_var_len(&date_num),
|
|
|
|
GENERATOR.get().unwrap().create_id()
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(thiserror::Error, Debug)]
|
|
|
|
#[error("Invalid ID: {id}")]
|
|
|
|
pub struct InvalidIdErr {
|
|
|
|
id: String,
|
2023-05-25 18:11:59 +02:00
|
|
|
}
|
|
|
|
|
2024-04-12 05:24:57 +02:00
|
|
|
#[crate::export]
|
2024-04-24 00:37:16 +02:00
|
|
|
pub fn get_timestamp(id: &str) -> Result<i64, InvalidIdErr> {
|
2023-08-06 08:34:44 +02:00
|
|
|
let n: Option<u64> = BASE36.decode_var_len(&id[0..8]);
|
2024-04-24 00:37:16 +02:00
|
|
|
if let Some(n) = n {
|
|
|
|
Ok(n as i64 + TIME_2000)
|
|
|
|
} else {
|
|
|
|
Err(InvalidIdErr { id: id.to_string() })
|
2023-08-06 08:34:44 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-04-24 00:02:06 +02:00
|
|
|
/// The generated ID results in the form of `[8 chars timestamp] + [cuid2]`.
|
|
|
|
/// The minimum and maximum lengths are 16 and 24, respectively.
|
|
|
|
/// With the length of 16, namely 8 for cuid2, roughly 1427399 IDs are needed
|
|
|
|
/// in the same millisecond to reach 50% chance of collision.
|
|
|
|
///
|
|
|
|
/// Ref: https://github.com/paralleldrive/cuid2#parameterized-length
|
|
|
|
#[crate::export]
|
|
|
|
pub fn gen_id() -> String {
|
2024-04-24 00:37:16 +02:00
|
|
|
create_id(&Utc::now().naive_utc())
|
2024-04-24 00:02:06 +02:00
|
|
|
}
|
2023-06-02 17:55:14 +02:00
|
|
|
|
2024-04-24 00:02:06 +02:00
|
|
|
/// Generate an ID using a specific datetime
|
|
|
|
#[crate::export]
|
|
|
|
pub fn gen_id_at(date: DateTime<Utc>) -> String {
|
2024-04-24 00:37:16 +02:00
|
|
|
create_id(&date.naive_utc())
|
2023-06-02 17:55:14 +02:00
|
|
|
}
|
|
|
|
|
2023-05-25 18:11:59 +02:00
|
|
|
#[cfg(test)]
|
2023-05-27 12:52:15 +02:00
|
|
|
mod unit_test {
|
2024-04-24 00:37:16 +02:00
|
|
|
use super::{gen_id, gen_id_at, get_timestamp};
|
|
|
|
use chrono::{Duration, Utc};
|
2023-06-02 13:08:58 +02:00
|
|
|
use pretty_assertions::{assert_eq, assert_ne};
|
2023-05-26 10:00:09 +02:00
|
|
|
use std::thread;
|
2023-05-25 18:11:59 +02:00
|
|
|
|
2023-07-10 07:39:33 +02:00
|
|
|
#[test]
|
2024-04-02 12:08:32 +02:00
|
|
|
fn can_create_and_decode_id() {
|
2024-04-24 00:37:16 +02:00
|
|
|
let now = Utc::now();
|
|
|
|
assert_eq!(gen_id().len(), 16);
|
|
|
|
assert_ne!(gen_id_at(now), gen_id_at(now));
|
|
|
|
assert_ne!(gen_id(), gen_id());
|
|
|
|
|
|
|
|
let id1 = thread::spawn(move || gen_id_at(now));
|
|
|
|
let id2 = thread::spawn(move || gen_id_at(now));
|
2023-07-10 07:39:33 +02:00
|
|
|
assert_ne!(id1.join().unwrap(), id2.join().unwrap());
|
2023-08-06 08:34:44 +02:00
|
|
|
|
2024-04-24 00:37:16 +02:00
|
|
|
let test_id = gen_id_at(now);
|
|
|
|
let timestamp = get_timestamp(&test_id).unwrap();
|
|
|
|
assert_eq!(now.timestamp_millis(), timestamp);
|
|
|
|
|
|
|
|
let now_id = gen_id_at(now);
|
|
|
|
let old_id = gen_id_at(now - Duration::milliseconds(1));
|
|
|
|
let future_id = gen_id_at(now + Duration::milliseconds(1));
|
|
|
|
assert!(old_id < now_id);
|
|
|
|
assert!(now_id < future_id);
|
2023-05-25 18:11:59 +02:00
|
|
|
}
|
|
|
|
}
|