use rand::Rng;
pub fn uuid_v4() -> String {
uuid::Uuid::new_v4().to_string()
}
pub fn uuid_v7() -> String {
uuid::Uuid::now_v7().to_string()
}
pub fn is_uuid(s: &str) -> bool {
uuid::Uuid::parse_str(s).is_ok()
}
pub fn uuid_version(s: &str) -> u8 {
uuid::Uuid::parse_str(s)
.map(|u| u.get_version_num())
.unwrap_or(0) as u8
}
pub fn ulid() -> String {
ulid::Ulid::new().to_string()
}
pub fn is_ulid(s: &str) -> bool {
ulid::Ulid::from_string(s).is_ok()
}
pub fn ulid_timestamp_ms(s: &str) -> Option<u64> {
ulid::Ulid::from_string(s).ok().map(|u| u.timestamp_ms())
}
pub fn cuid2() -> String {
cuid2_with_length(24)
}
pub fn cuid2_with_length(length: usize) -> String {
let length = length.clamp(4, 64);
let mut rng = rand::rng();
let first = (b'a' + rng.random_range(0..26)) as char;
const ALPHABET: &[u8] = b"0123456789abcdefghijklmnopqrstuvwxyz";
let rest: String = (1..length)
.map(|_| {
let idx = rng.random_range(0..ALPHABET.len());
ALPHABET[idx] as char
})
.collect();
format!("{first}{rest}")
}
pub fn is_cuid2(s: &str) -> bool {
if s.len() < 4 {
return false;
}
let first = s.as_bytes()[0];
if !first.is_ascii_lowercase() {
return false;
}
s.bytes().all(|b| b.is_ascii_alphanumeric())
}
pub fn nanoid() -> String {
nanoid::nanoid!()
}
pub fn nanoid_with_length(length: usize) -> String {
nanoid::nanoid!(length)
}
pub fn is_nanoid(s: &str) -> bool {
if s.is_empty() || s.len() > 128 {
return false;
}
s.bytes()
.all(|b| b.is_ascii_alphanumeric() || b == b'_' || b == b'-')
}
pub fn detect_id_type(s: &str) -> &'static str {
if is_uuid(s) {
"uuid"
} else if is_ulid(s) {
"ulid"
} else if is_cuid2(s) && s.len() >= 20 {
"cuid2"
} else if is_nanoid(s) {
"nanoid"
} else {
"unknown"
}
}
pub fn generate_by_type(id_type: &str) -> Option<String> {
match id_type {
"uuidv7" => Some(uuid_v7()),
"uuidv4" => Some(uuid_v4()),
"ulid" => Some(ulid()),
"cuid2" => Some(cuid2()),
"nanoid" => Some(nanoid()),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn uuid_v4_valid() {
let id = uuid_v4();
assert!(is_uuid(&id));
assert_eq!(uuid_version(&id), 4);
assert_eq!(id.len(), 36); }
#[test]
fn uuid_v7_valid_and_sortable() {
let id1 = uuid_v7();
std::thread::sleep(std::time::Duration::from_millis(2));
let id2 = uuid_v7();
assert!(is_uuid(&id1));
assert!(is_uuid(&id2));
assert_eq!(uuid_version(&id1), 7);
assert!(id1 < id2, "v7 should be time-sortable: {id1} < {id2}");
}
#[test]
fn ulid_valid_and_sortable() {
let id1 = ulid();
std::thread::sleep(std::time::Duration::from_millis(2));
let id2 = ulid();
assert!(is_ulid(&id1));
assert!(is_ulid(&id2));
assert_eq!(id1.len(), 26); assert!(id1 < id2, "ULID should be time-sortable: {id1} < {id2}");
}
#[test]
fn ulid_timestamp() {
let id = ulid();
let ts = ulid_timestamp_ms(&id).unwrap();
let now_ms = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis() as u64;
assert!(ts <= now_ms);
assert!(now_ms - ts < 1000); }
#[test]
fn cuid2_valid() {
let id = cuid2();
assert!(is_cuid2(&id));
assert_eq!(id.len(), 24);
assert!(id.as_bytes()[0].is_ascii_lowercase()); }
#[test]
fn cuid2_custom_length() {
let short = cuid2_with_length(8);
assert_eq!(short.len(), 8);
assert!(is_cuid2(&short));
let long = cuid2_with_length(48);
assert_eq!(long.len(), 48);
assert!(is_cuid2(&long));
}
#[test]
fn cuid2_uniqueness() {
let mut ids: Vec<String> = (0..1000).map(|_| cuid2()).collect();
ids.sort();
ids.dedup();
assert_eq!(ids.len(), 1000); }
#[test]
fn nanoid_valid() {
let id = nanoid();
assert!(is_nanoid(&id));
assert_eq!(id.len(), 21);
}
#[test]
fn nanoid_custom_length() {
let id = nanoid_with_length(32);
assert!(is_nanoid(&id));
assert_eq!(id.len(), 32);
}
#[test]
fn detect_types() {
assert_eq!(detect_id_type(&uuid_v4()), "uuid");
assert_eq!(detect_id_type(&uuid_v7()), "uuid");
assert_eq!(detect_id_type(&ulid()), "ulid");
assert_eq!(detect_id_type("not-a-valid-id!@#"), "unknown");
}
#[test]
fn is_uuid_rejects_invalid() {
assert!(!is_uuid("not-a-uuid"));
assert!(!is_uuid(""));
assert!(!is_uuid("12345"));
}
#[test]
fn is_ulid_rejects_invalid() {
assert!(!is_ulid("not-a-ulid"));
assert!(!is_ulid(""));
assert!(!is_ulid(&uuid_v4())); }
}