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_snowflake_format(input) {
results.push(DetectionResult::new(IdKind::Snowflake, 0.8));
}
if is_nanoid_format(input) {
results.push(DetectionResult::new(IdKind::NanoId, 0.6));
}
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 == '-')
}
#[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);
}
}