idgen_cli/
inspector.rs

1use chrono::{DateTime, TimeZone, Utc};
2use regex::Regex;
3use serde::Serialize;
4use uuid::Uuid;
5
6#[derive(Serialize, Debug)]
7pub struct InspectionResult {
8    pub valid: bool,
9    pub id_type: String,
10    #[serde(skip_serializing_if = "Option::is_none")]
11    pub version: Option<String>,
12    #[serde(skip_serializing_if = "Option::is_none")]
13    pub timestamp: Option<String>,
14    #[serde(skip_serializing_if = "Option::is_none")]
15    pub variant: Option<String>,
16}
17
18pub fn inspect_id(id: &str) -> InspectionResult {
19    // 1. Try UUID
20    if let Ok(uuid) = Uuid::parse_str(id) {
21        let version = uuid.get_version().map(|v| format!("{:?}", v));
22        let variant = format!("{:?}", uuid.get_variant());
23
24        // Extract timestamp for v1 and v7 (if supported by crate, v1 is standard)
25        // Note: uuid crate v1.0+ supports getting timestamp from v1, v6, v7
26        let timestamp = if let Some(uuid::Version::Mac) = uuid.get_version() {
27            // UUID v1 timestamp extraction is complex without direct crate support in older versions
28            // For now, we'll skip complex timestamp extraction for UUIDs to keep it simple
29            // unless we upgrade to uuid v1.0+ features explicitly.
30            // Actually, let's try a best effort for v1 if the crate allows,
31            // but the current uuid crate version in Cargo.toml is 1.18.1 which is good.
32
33            // uuid 1.x exposes get_timestamp() which returns a Timestamp struct
34            uuid.get_timestamp().and_then(|ts| {
35                let (secs, nanos) = ts.to_unix();
36                Utc.timestamp_opt(secs as i64, nanos)
37                    .single()
38                    .map(|dt| dt.to_rfc3339())
39            })
40        } else {
41            None
42        };
43
44        return InspectionResult {
45            valid: true,
46            id_type: "UUID".to_string(),
47            version,
48            timestamp,
49            variant: Some(variant),
50        };
51    }
52
53    // 2. Try ULID
54    if let Ok(ulid) = ulid::Ulid::from_string(id) {
55        let datetime: DateTime<Utc> = ulid.datetime().into();
56        return InspectionResult {
57            valid: true,
58            id_type: "ULID".to_string(),
59            version: None,
60            timestamp: Some(datetime.to_rfc3339()),
61            variant: None,
62        };
63    }
64
65    // 3. Try MongoDB ObjectId (24 hex chars)
66    let object_id_regex = Regex::new(r"^[0-9a-fA-F]{24}$").unwrap();
67    if object_id_regex.is_match(id) {
68        // Extract timestamp (first 4 bytes / 8 hex chars)
69        if let Ok(timestamp_hex) = u32::from_str_radix(&id[0..8], 16) {
70            let datetime = Utc.timestamp_opt(timestamp_hex as i64, 0).single();
71            return InspectionResult {
72                valid: true,
73                id_type: "ObjectId".to_string(),
74                version: None,
75                timestamp: datetime.map(|dt| dt.to_rfc3339()),
76                variant: None,
77            };
78        }
79    }
80
81    // 4. Try CUID (v1 starts with 'c', v2 is 24 chars usually)
82    // CUID v1
83    if id.starts_with('c') && id.len() >= 25 {
84        return InspectionResult {
85            valid: true,
86            id_type: "CUID".to_string(),
87            version: Some("v1".to_string()),
88            timestamp: None, // CUID v1 timestamp is base36 encoded, doable but custom logic
89            variant: None,
90        };
91    }
92
93    // CUID v2 (24 chars, usually starts with lowercase letter)
94    // This is a weak heuristic, so we put it last-ish
95    if id.len() == 24
96        && id
97            .chars()
98            .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit())
99    {
100        // Could be CUID v2 or just a random string.
101        // Since ObjectId is also 24 hex, we checked that first.
102        // If it wasn't hex, it might be CUID v2.
103        return InspectionResult {
104            valid: true,
105            id_type: "CUID".to_string(),
106            version: Some("v2".to_string()),
107            timestamp: None,
108            variant: None,
109        };
110    }
111
112    // 5. NanoID (Hard to detect definitively as it's just random chars)
113    // We can just check for URL-safe chars and length
114    let nanoid_regex = Regex::new(r"^[A-Za-z0-9_-]{21}$").unwrap();
115    if nanoid_regex.is_match(id) {
116        return InspectionResult {
117            valid: true,
118            id_type: "NanoID".to_string(),
119            version: None,
120            timestamp: None,
121            variant: None,
122        };
123    }
124
125    InspectionResult {
126        valid: false,
127        id_type: "Unknown".to_string(),
128        version: None,
129        timestamp: None,
130        variant: None,
131    }
132}