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 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 let timestamp = if let Some(uuid::Version::Mac) = uuid.get_version() {
27 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 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 let object_id_regex = Regex::new(r"^[0-9a-fA-F]{24}$").unwrap();
67 if object_id_regex.is_match(id) {
68 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 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, variant: None,
90 };
91 }
92
93 if id.len() == 24
96 && id
97 .chars()
98 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit())
99 {
100 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 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}