use crate::core::error::Result;
use crate::core::id::IdKind;
#[derive(Debug, Clone)]
pub struct DetectionResult {
pub kind: IdKind,
pub confidence: f32,
}
impl DetectionResult {
pub fn new(kind: IdKind, confidence: f32) -> Self {
Self { kind, confidence }
}
}
pub fn detect_id_type(input: &str) -> Result<Vec<DetectionResult>> {
let input = input.trim();
let mut results = Vec::new();
if is_uuid_format(input) {
if let Some(version) = detect_uuid_version(input) {
results.push(DetectionResult::new(version, 1.0));
} else {
results.push(DetectionResult::new(IdKind::Uuid, 0.9));
}
}
if input.len() == 32 && input.chars().all(|c| c.is_ascii_hexdigit()) {
results.push(DetectionResult::new(IdKind::Uuid, 0.7));
}
if is_ulid_format(input) {
results.push(DetectionResult::new(IdKind::Ulid, 0.95));
}
if is_typeid_format(input) {
results.push(DetectionResult::new(IdKind::TypeId, 0.95));
}
if is_objectid_format(input) {
results.push(DetectionResult::new(IdKind::ObjectId, 0.85));
}
if is_ksuid_format(input) {
results.push(DetectionResult::new(IdKind::Ksuid, 0.8));
}
if is_xid_format(input) {
results.push(DetectionResult::new(IdKind::Xid, 0.8));
}
if is_snowflake_format(input) {
results.push(DetectionResult::new(IdKind::Snowflake, 0.8));
}
if is_tsid_format(input) {
results.push(DetectionResult::new(IdKind::Tsid, 0.75));
}
if is_cuid_format(input) {
results.push(DetectionResult::new(IdKind::Cuid, 0.75));
}
if is_nanoid_format(input) {
results.push(DetectionResult::new(IdKind::NanoId, 0.6));
}
if is_cuid2_format(input) {
results.push(DetectionResult::new(IdKind::Cuid2, 0.4));
}
results.sort_by(|a, b| b.confidence.partial_cmp(&a.confidence).unwrap());
if results.is_empty() {
Err(crate::core::error::IdtError::DetectionFailed)
} else {
Ok(results)
}
}
fn is_uuid_format(input: &str) -> bool {
if input.len() != 36 {
return false;
}
let parts: Vec<&str> = input.split('-').collect();
if parts.len() != 5 {
return false;
}
let expected_lens = [8, 4, 4, 4, 12];
for (part, &expected_len) in parts.iter().zip(&expected_lens) {
if part.len() != expected_len || !part.chars().all(|c| c.is_ascii_hexdigit()) {
return false;
}
}
true
}
fn detect_uuid_version(input: &str) -> Option<IdKind> {
let input = input.replace('-', "");
if input.len() != 32 {
return None;
}
if input.chars().all(|c| c == '0') {
return Some(IdKind::UuidNil);
}
if input.to_lowercase().chars().all(|c| c == 'f') {
return Some(IdKind::UuidMax);
}
let version_char = input.chars().nth(12)?;
let version = version_char.to_digit(16)?;
let variant_char = input.chars().nth(16)?;
let variant = variant_char.to_digit(16)?;
if !(8..=11).contains(&variant) {
return Some(IdKind::Uuid); }
match version {
1 => Some(IdKind::UuidV1),
3 => Some(IdKind::UuidV3),
4 => Some(IdKind::UuidV4),
5 => Some(IdKind::UuidV5),
6 => Some(IdKind::UuidV6),
7 => Some(IdKind::UuidV7),
_ => Some(IdKind::Uuid),
}
}
fn is_ulid_format(input: &str) -> bool {
if input.len() != 26 {
return false;
}
let input_upper = input.to_uppercase();
let first = input_upper.chars().next().unwrap();
if !('0'..='7').contains(&first) {
return false;
}
const CROCKFORD: &str = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
input_upper.chars().all(|c| CROCKFORD.contains(c))
}
fn is_snowflake_format(input: &str) -> bool {
if input.is_empty() {
return false;
}
if !input.chars().all(|c| c.is_ascii_digit()) {
return false;
}
let len = input.len();
(15..=19).contains(&len)
}
fn is_nanoid_format(input: &str) -> bool {
if input.len() != 21 {
return false;
}
input
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
}
fn is_objectid_format(input: &str) -> bool {
input.len() == 24 && input.chars().all(|c| c.is_ascii_hexdigit())
}
fn is_ksuid_format(input: &str) -> bool {
input.len() == 27 && input.chars().all(|c| c.is_ascii_alphanumeric())
}
fn is_xid_format(input: &str) -> bool {
input.len() == 20 && input.chars().all(|c| matches!(c, '0'..='9' | 'a'..='v'))
}
fn is_tsid_format(input: &str) -> bool {
if input.len() != 13 {
return false;
}
const CROCKFORD_CHARS: &str = "0123456789ABCDEFGHJKMNPQRSTVWXYZabcdefghjkmnpqrstvwxyz";
input.chars().all(|c| CROCKFORD_CHARS.contains(c))
}
fn is_cuid_format(input: &str) -> bool {
input.len() == 25
&& input.starts_with('c')
&& input
.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit())
}
fn is_typeid_format(input: &str) -> bool {
if let Some(pos) = input.rfind('_') {
let prefix = &input[..pos];
let suffix = &input[pos + 1..];
if prefix.is_empty() || !prefix.chars().all(|c| c.is_ascii_lowercase() || c == '_') {
return false;
}
if !prefix.starts_with(|c: char| c.is_ascii_lowercase()) {
return false;
}
if suffix.len() != 26 {
return false;
}
const TYPEID_CHARS: &str = "0123456789abcdefghjkmnpqrstvwxyz";
suffix.chars().all(|c| TYPEID_CHARS.contains(c))
} else {
false
}
}
fn is_cuid2_format(input: &str) -> bool {
input.len() == 24
&& input.starts_with(|c: char| c.is_ascii_lowercase())
&& input
.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_detect_uuidv4() {
let results = detect_id_type("550e8400-e29b-41d4-a716-446655440000").unwrap();
assert!(!results.is_empty());
assert_eq!(results[0].kind, IdKind::UuidV4);
}
#[test]
fn test_detect_ulid() {
let results = detect_id_type("01ARZ3NDEKTSV4RRFFQ69G5FAV").unwrap();
assert!(!results.is_empty());
assert_eq!(results[0].kind, IdKind::Ulid);
}
#[test]
fn test_detect_snowflake() {
let results = detect_id_type("1234567890123456789").unwrap();
assert!(!results.is_empty());
assert_eq!(results[0].kind, IdKind::Snowflake);
}
#[test]
fn test_detect_objectid() {
let results = detect_id_type("507f1f77bcf86cd799439011").unwrap();
assert!(!results.is_empty());
assert_eq!(results[0].kind, IdKind::ObjectId);
}
#[test]
fn test_detect_typeid() {
let results = detect_id_type("user_01h455vb4pex5vsknk084sn02q").unwrap();
assert!(!results.is_empty());
assert_eq!(results[0].kind, IdKind::TypeId);
}
#[test]
fn test_detect_xid() {
let results = detect_id_type("9m4e2mr0ui3e8a215n4g").unwrap();
assert!(!results.is_empty());
assert!(results.iter().any(|r| r.kind == IdKind::Xid));
}
#[test]
fn test_is_ksuid_format() {
assert!(is_ksuid_format("0ujtsYcgvSTl8PAuAdqWYSMnLOv"));
}
#[test]
fn test_is_tsid_format() {
assert!(is_tsid_format("0ARZJQ9V8G1FC"));
}
#[test]
fn test_is_cuid_format() {
assert!(is_cuid_format("cjld2cyuq0000t3rmniod1foy"));
}
}