Skip to main content

rok_ids/
lib.rs

1pub mod cuid2;
2pub mod nanoid;
3pub mod snowflake;
4pub mod ulid;
5pub mod uuid_v7;
6
7pub use cuid2::Cuid2;
8pub use nanoid::NanoId;
9pub use snowflake::{Snowflake, SnowflakeConfig};
10pub use ulid::Ulid;
11pub use uuid_v7::UuidV7;
12
13use thiserror::Error;
14
15#[derive(Debug, Error)]
16pub enum IdError {
17    #[error("{0}: {1}")]
18    InvalidFormat(&'static str, &'static str),
19}
20
21#[cfg(test)]
22mod tests {
23    use std::collections::HashSet;
24
25    use super::*;
26
27    // ── CUID2 ─────────────────────────────────────────────────────────────────
28
29    #[test]
30    fn cuid2_length() {
31        let id = Cuid2::generate();
32        assert_eq!(id.as_str().len(), 24, "CUID2 must be 24 chars");
33    }
34
35    #[test]
36    fn cuid2_starts_with_letter() {
37        for _ in 0..20 {
38            let id = Cuid2::generate();
39            assert!(
40                id.as_str().chars().next().unwrap().is_ascii_alphabetic(),
41                "CUID2 first char must be a letter"
42            );
43        }
44    }
45
46    #[test]
47    fn cuid2_uniqueness() {
48        let ids: HashSet<String> = (0..1000).map(|_| Cuid2::generate().to_string()).collect();
49        assert_eq!(ids.len(), 1000, "CUID2 collisions detected");
50    }
51
52    #[test]
53    fn cuid2_roundtrip() {
54        let id = Cuid2::generate();
55        let s = id.to_string();
56        let parsed: Cuid2 = s.parse().unwrap();
57        assert_eq!(id, parsed);
58    }
59
60    #[test]
61    fn cuid2_serde() {
62        let id = Cuid2::generate();
63        let json = serde_json::to_string(&id).unwrap();
64        let back: Cuid2 = serde_json::from_str(&json).unwrap();
65        assert_eq!(id, back);
66    }
67
68    // ── ULID ──────────────────────────────────────────────────────────────────
69
70    #[test]
71    fn ulid_length() {
72        let id = Ulid::generate();
73        assert_eq!(id.as_str().len(), 26, "ULID must be 26 chars");
74    }
75
76    #[test]
77    fn ulid_uniqueness() {
78        let ids: HashSet<String> = (0..1000).map(|_| Ulid::generate().to_string()).collect();
79        assert_eq!(ids.len(), 1000, "ULID collisions detected");
80    }
81
82    #[test]
83    fn ulid_sorted_by_time() {
84        // Generate ULIDs with a small sleep to ensure different ms buckets
85        let a = Ulid::generate();
86        std::thread::sleep(std::time::Duration::from_millis(2));
87        let b = Ulid::generate();
88        assert!(
89            a.to_string() < b.to_string(),
90            "ULIDs must be lexicographically ordered"
91        );
92    }
93
94    #[test]
95    fn ulid_timestamp_roundtrip() {
96        let before = std::time::SystemTime::now()
97            .duration_since(std::time::UNIX_EPOCH)
98            .unwrap()
99            .as_millis() as u64;
100        let id = Ulid::generate();
101        let after = std::time::SystemTime::now()
102            .duration_since(std::time::UNIX_EPOCH)
103            .unwrap()
104            .as_millis() as u64;
105        let ts = id.timestamp_ms();
106        assert!(ts >= before && ts <= after, "ULID timestamp out of range");
107    }
108
109    #[test]
110    fn ulid_monotonic_within_ms() {
111        let mut prev = Ulid::monotonic();
112        for _ in 0..50 {
113            let next = Ulid::monotonic();
114            assert!(
115                prev.to_string() < next.to_string(),
116                "monotonic ULIDs must be strictly increasing"
117            );
118            prev = next;
119        }
120    }
121
122    #[test]
123    fn ulid_roundtrip() {
124        let id = Ulid::generate();
125        let s = id.to_string();
126        let parsed: Ulid = s.parse().unwrap();
127        assert_eq!(id, parsed);
128    }
129
130    #[test]
131    fn ulid_serde() {
132        let id = Ulid::generate();
133        let json = serde_json::to_string(&id).unwrap();
134        let back: Ulid = serde_json::from_str(&json).unwrap();
135        assert_eq!(id, back);
136    }
137
138    // ── UUID v7 ───────────────────────────────────────────────────────────────
139
140    #[test]
141    fn uuid_v7_version() {
142        let id = UuidV7::generate();
143        assert_eq!(id.as_uuid().get_version_num(), 7);
144    }
145
146    #[test]
147    fn uuid_v7_uniqueness() {
148        let ids: HashSet<String> = (0..1000).map(|_| UuidV7::generate().to_string()).collect();
149        assert_eq!(ids.len(), 1000, "UUID v7 collisions detected");
150    }
151
152    #[test]
153    fn uuid_v7_ordered() {
154        let a = UuidV7::generate();
155        std::thread::sleep(std::time::Duration::from_millis(2));
156        let b = UuidV7::generate();
157        assert!(a.to_string() < b.to_string());
158    }
159
160    #[test]
161    fn uuid_v7_roundtrip() {
162        let id = UuidV7::generate();
163        let s = id.to_string();
164        let parsed: UuidV7 = s.parse().unwrap();
165        assert_eq!(id, parsed);
166    }
167
168    // ── Snowflake ─────────────────────────────────────────────────────────────
169
170    #[test]
171    fn snowflake_positive() {
172        let id = Snowflake::new();
173        assert!(id.value() > 0);
174    }
175
176    #[test]
177    fn snowflake_monotonic() {
178        let cfg = SnowflakeConfig::default();
179        let mut prev = Snowflake::generate(&cfg);
180        for _ in 0..100 {
181            let next = Snowflake::generate(&cfg);
182            assert!(
183                next.value() > prev.value(),
184                "Snowflake IDs must be monotonically increasing"
185            );
186            prev = next;
187        }
188    }
189
190    #[test]
191    fn snowflake_worker_id() {
192        let cfg = SnowflakeConfig {
193            worker_id: 42,
194            epoch_ms: 1_577_836_800_000,
195        };
196        let id = Snowflake::generate(&cfg);
197        assert_eq!(id.worker_id(), 42);
198    }
199
200    #[test]
201    fn snowflake_roundtrip() {
202        let id = Snowflake::new();
203        let s = id.to_string();
204        let parsed: Snowflake = s.parse().unwrap();
205        assert_eq!(id, parsed);
206    }
207
208    // ── NanoID ────────────────────────────────────────────────────────────────
209
210    #[test]
211    fn nanoid_default_length() {
212        let id = NanoId::generate();
213        assert_eq!(id.as_str().len(), 21);
214    }
215
216    #[test]
217    fn nanoid_custom_size() {
218        let id = NanoId::with_size(36);
219        assert_eq!(id.as_str().len(), 36);
220    }
221
222    #[test]
223    fn nanoid_custom_alphabet() {
224        let alpha = b"0123456789";
225        let id = NanoId::custom(alpha, 10);
226        assert!(id.as_str().chars().all(|c| c.is_ascii_digit()));
227        assert_eq!(id.as_str().len(), 10);
228    }
229
230    #[test]
231    fn nanoid_uniqueness() {
232        let ids: HashSet<String> = (0..1000).map(|_| NanoId::generate().to_string()).collect();
233        assert_eq!(ids.len(), 1000, "NanoID collisions detected");
234    }
235
236    #[test]
237    fn nanoid_serde() {
238        let id = NanoId::generate();
239        let json = serde_json::to_string(&id).unwrap();
240        let back: NanoId = serde_json::from_str(&json).unwrap();
241        assert_eq!(id, back);
242    }
243}