1use rand::Rng;
9
10pub fn uuid_v4() -> String {
14 uuid::Uuid::new_v4().to_string()
15}
16
17pub fn uuid_v7() -> String {
22 uuid::Uuid::now_v7().to_string()
23}
24
25pub fn is_uuid(s: &str) -> bool {
27 uuid::Uuid::parse_str(s).is_ok()
28}
29
30pub fn uuid_version(s: &str) -> u8 {
32 uuid::Uuid::parse_str(s)
33 .map(|u| u.get_version_num())
34 .unwrap_or(0) as u8
35}
36
37pub fn ulid() -> String {
44 ulid::Ulid::new().to_string()
45}
46
47pub fn is_ulid(s: &str) -> bool {
49 ulid::Ulid::from_string(s).is_ok()
50}
51
52pub fn ulid_timestamp_ms(s: &str) -> Option<u64> {
54 ulid::Ulid::from_string(s).ok().map(|u| u.timestamp_ms())
55}
56
57pub fn cuid2() -> String {
66 cuid2_with_length(24)
67}
68
69pub fn cuid2_with_length(length: usize) -> String {
71 let length = length.clamp(4, 64);
72 let mut rng = rand::rng();
73
74 let first = (b'a' + rng.random_range(0..26)) as char;
76
77 const ALPHABET: &[u8] = b"0123456789abcdefghijklmnopqrstuvwxyz";
79 let rest: String = (1..length)
80 .map(|_| {
81 let idx = rng.random_range(0..ALPHABET.len());
82 ALPHABET[idx] as char
83 })
84 .collect();
85
86 format!("{first}{rest}")
87}
88
89pub fn is_cuid2(s: &str) -> bool {
91 if s.len() < 4 {
92 return false;
93 }
94 let first = s.as_bytes()[0];
95 if !first.is_ascii_lowercase() {
96 return false;
97 }
98 s.bytes().all(|b| b.is_ascii_alphanumeric())
99}
100
101pub fn nanoid() -> String {
108 nanoid::nanoid!()
109}
110
111pub fn nanoid_with_length(length: usize) -> String {
113 nanoid::nanoid!(length)
114}
115
116pub fn is_nanoid(s: &str) -> bool {
118 if s.is_empty() || s.len() > 128 {
119 return false;
120 }
121 s.bytes()
122 .all(|b| b.is_ascii_alphanumeric() || b == b'_' || b == b'-')
123}
124
125pub fn detect_id_type(s: &str) -> &'static str {
132 if is_uuid(s) {
133 "uuid"
134 } else if is_ulid(s) {
135 "ulid"
136 } else if is_cuid2(s) && s.len() >= 20 {
137 "cuid2"
138 } else if is_nanoid(s) {
139 "nanoid"
140 } else {
141 "unknown"
142 }
143}
144
145pub fn generate_by_type(id_type: &str) -> Option<String> {
150 match id_type {
151 "uuidv7" => Some(uuid_v7()),
152 "uuidv4" => Some(uuid_v4()),
153 "ulid" => Some(ulid()),
154 "cuid2" => Some(cuid2()),
155 "nanoid" => Some(nanoid()),
156 _ => None,
157 }
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163
164 #[test]
165 fn uuid_v4_valid() {
166 let id = uuid_v4();
167 assert!(is_uuid(&id));
168 assert_eq!(uuid_version(&id), 4);
169 assert_eq!(id.len(), 36); }
171
172 #[test]
173 fn uuid_v7_valid_and_sortable() {
174 let id1 = uuid_v7();
175 std::thread::sleep(std::time::Duration::from_millis(2));
176 let id2 = uuid_v7();
177 assert!(is_uuid(&id1));
178 assert!(is_uuid(&id2));
179 assert_eq!(uuid_version(&id1), 7);
180 assert!(id1 < id2, "v7 should be time-sortable: {id1} < {id2}");
182 }
183
184 #[test]
185 fn ulid_valid_and_sortable() {
186 let id1 = ulid();
187 std::thread::sleep(std::time::Duration::from_millis(2));
188 let id2 = ulid();
189 assert!(is_ulid(&id1));
190 assert!(is_ulid(&id2));
191 assert_eq!(id1.len(), 26); assert!(id1 < id2, "ULID should be time-sortable: {id1} < {id2}");
193 }
194
195 #[test]
196 fn ulid_timestamp() {
197 let id = ulid();
198 let ts = ulid_timestamp_ms(&id).unwrap();
199 let now_ms = std::time::SystemTime::now()
200 .duration_since(std::time::UNIX_EPOCH)
201 .unwrap()
202 .as_millis() as u64;
203 assert!(ts <= now_ms);
204 assert!(now_ms - ts < 1000); }
206
207 #[test]
208 fn cuid2_valid() {
209 let id = cuid2();
210 assert!(is_cuid2(&id));
211 assert_eq!(id.len(), 24);
212 assert!(id.as_bytes()[0].is_ascii_lowercase()); }
214
215 #[test]
216 fn cuid2_custom_length() {
217 let short = cuid2_with_length(8);
218 assert_eq!(short.len(), 8);
219 assert!(is_cuid2(&short));
220
221 let long = cuid2_with_length(48);
222 assert_eq!(long.len(), 48);
223 assert!(is_cuid2(&long));
224 }
225
226 #[test]
227 fn cuid2_uniqueness() {
228 let mut ids: Vec<String> = (0..1000).map(|_| cuid2()).collect();
229 ids.sort();
230 ids.dedup();
231 assert_eq!(ids.len(), 1000); }
233
234 #[test]
235 fn nanoid_valid() {
236 let id = nanoid();
237 assert!(is_nanoid(&id));
238 assert_eq!(id.len(), 21);
239 }
240
241 #[test]
242 fn nanoid_custom_length() {
243 let id = nanoid_with_length(32);
244 assert!(is_nanoid(&id));
245 assert_eq!(id.len(), 32);
246 }
247
248 #[test]
249 fn detect_types() {
250 assert_eq!(detect_id_type(&uuid_v4()), "uuid");
251 assert_eq!(detect_id_type(&uuid_v7()), "uuid");
252 assert_eq!(detect_id_type(&ulid()), "ulid");
253 assert_eq!(detect_id_type("not-a-valid-id!@#"), "unknown");
254 }
255
256 #[test]
257 fn is_uuid_rejects_invalid() {
258 assert!(!is_uuid("not-a-uuid"));
259 assert!(!is_uuid(""));
260 assert!(!is_uuid("12345"));
261 }
262
263 #[test]
264 fn is_ulid_rejects_invalid() {
265 assert!(!is_ulid("not-a-ulid"));
266 assert!(!is_ulid(""));
267 assert!(!is_ulid(&uuid_v4())); }
269}