use rand::Rng;
use crate::id::{IdError, IdType};
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())
}
const CUID2_MIN_LEN: usize = 4;
const CUID2_MAX_LEN: usize = 32;
pub fn cuid2() -> String {
cuid2_with_length(24).expect("24 is within [4, 32]")
}
pub fn cuid2_with_length(length: usize) -> Result<String, IdError> {
if !(CUID2_MIN_LEN..=CUID2_MAX_LEN).contains(&length) {
return Err(IdError::LengthOutOfRange {
requested: length,
min: CUID2_MIN_LEN,
max: CUID2_MAX_LEN,
});
}
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();
Ok(format!("{first}{rest}"))
}
pub fn is_cuid2(s: &str) -> bool {
let len = s.len();
if !(CUID2_MIN_LEN..=CUID2_MAX_LEN).contains(&len) {
return false;
}
let bytes = s.as_bytes();
if !bytes[0].is_ascii_lowercase() {
return false;
}
bytes
.iter()
.all(|b| b.is_ascii_lowercase() || b.is_ascii_digit())
}
const NANOID_MIN_LEN: usize = 10;
const NANOID_MAX_LEN: usize = 64;
pub fn nanoid() -> String {
nanoid::nanoid!()
}
pub fn nanoid_with_length(length: usize) -> String {
nanoid::nanoid!(length)
}
pub fn is_nanoid(s: &str) -> bool {
let len = s.len();
if !(NANOID_MIN_LEN..=NANOID_MAX_LEN).contains(&len) {
return false;
}
s.bytes()
.all(|b| b.is_ascii_alphanumeric() || b == b'_' || b == b'-')
}
pub fn detect_id_type(s: &str) -> IdType {
if is_uuid(s) {
IdType::Uuid
} else if is_ulid(s) {
IdType::Ulid
} else if is_cuid2(s) {
IdType::Cuid2
} else if is_nanoid(s) {
IdType::NanoId
} else {
IdType::Custom
}
}
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_with_length_valid_range() {
for len in [4usize, 8, 16, 24, 32] {
let id = cuid2_with_length(len).unwrap_or_else(|e| panic!("len {len} failed: {e}"));
assert_eq!(id.len(), len, "length mismatch for requested {len}");
assert!(is_cuid2(&id), "is_cuid2 rejected id of len {len}");
}
}
#[test]
fn cuid2_with_length_out_of_range_errors() {
let too_small = cuid2_with_length(3);
assert!(
matches!(
too_small,
Err(IdError::LengthOutOfRange {
requested: 3,
min: 4,
max: 32
})
),
"expected LengthOutOfRange for len 3, got {too_small:?}"
);
let too_large = cuid2_with_length(33);
assert!(
matches!(
too_large,
Err(IdError::LengthOutOfRange {
requested: 33,
min: 4,
max: 32
})
),
"expected LengthOutOfRange for len 33, got {too_large:?}"
);
}
#[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 is_cuid2_bounds() {
assert!(!is_cuid2("abc"));
assert!(!is_cuid2("abcdefghijklmnopqrstuvwxyz1234567"));
assert!(!is_cuid2("Abcdefghijklmnopqrstuvwx"));
assert!(!is_cuid2("abcDef"));
assert!(is_cuid2("abcd"));
assert!(is_cuid2("abcdefghijklmnopqrstuvwxyz123456"));
}
#[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 nanoid_detection_bounds() {
let short = nanoid_with_length(9);
assert!(!is_nanoid(&short), "9-char nanoid should not be detected");
let long = nanoid_with_length(65);
assert!(!is_nanoid(&long), "65-char nanoid should not be detected");
assert!(is_nanoid(&nanoid_with_length(10)));
assert!(is_nanoid(&nanoid_with_length(64)));
}
#[test]
fn detect_types() {
assert_eq!(detect_id_type(&uuid_v4()), IdType::Uuid);
assert_eq!(detect_id_type(&uuid_v7()), IdType::Uuid);
assert_eq!(detect_id_type(&ulid()), IdType::Ulid);
assert_eq!(detect_id_type(&cuid2()), IdType::Cuid2);
assert_eq!(detect_id_type("not-a-valid-id!@#"), IdType::Custom);
}
#[test]
fn detect_id_type_exhaustive_match() {
let id_type = detect_id_type(&cuid2());
match id_type {
IdType::Uuid => {}
IdType::Ulid => {}
IdType::Cuid2 => {}
IdType::NanoId => {}
IdType::Custom => {}
}
}
#[test]
fn id_type_as_str() {
assert_eq!(IdType::Uuid.as_str(), "uuid");
assert_eq!(IdType::Ulid.as_str(), "ulid");
assert_eq!(IdType::Cuid2.as_str(), "cuid2");
assert_eq!(IdType::NanoId.as_str(), "nanoid");
assert_eq!(IdType::Custom.as_str(), "custom");
}
#[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())); }
}