1use std::collections::BTreeMap;
8
9pub const ALPHABET: &[u8; 32] = b"0123456789abcdefghjkmnpqrstvwxyz";
11
12pub const ID_LEN: usize = 7;
14
15pub const PREFIXES: [&str; 11] = ["d", "b", "c", "r", "x", "col", "cp", "e", "rp", "v", "src"];
17
18pub trait IdMinter {
21 fn mint(&mut self, prefix: &str) -> String;
23}
24
25impl<F: FnMut(&str) -> String> IdMinter for F {
26 fn mint(&mut self, prefix: &str) -> String {
27 self(prefix)
28 }
29}
30
31#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
33pub struct RandomMinter;
34
35impl IdMinter for RandomMinter {
36 fn mint(&mut self, prefix: &str) -> String {
37 format!("{prefix}_{}", random_suffix())
38 }
39}
40
41#[derive(Clone, Debug, Default, PartialEq, Eq)]
43pub struct SequentialMinter {
44 counters: BTreeMap<String, u64>,
45}
46
47impl SequentialMinter {
48 #[must_use]
49 pub fn new() -> Self {
50 Self::default()
51 }
52
53 #[must_use]
55 pub fn next(&self, prefix: &str) -> u64 {
56 self.counters.get(prefix).copied().unwrap_or(0)
57 }
58}
59
60impl IdMinter for SequentialMinter {
61 fn mint(&mut self, prefix: &str) -> String {
62 let n = self.counters.entry(prefix.to_owned()).or_insert(0);
63 let id = format!("{prefix}_{n}");
64 *n += 1;
65 id
66 }
67}
68
69#[derive(Clone, Debug, Default, PartialEq, Eq)]
74pub struct RepeatingMinter {
75 next: SequentialMinter,
76 pending: BTreeMap<String, String>,
77}
78
79impl RepeatingMinter {
80 #[must_use]
81 pub fn new() -> Self {
82 Self::default()
83 }
84}
85
86impl IdMinter for RepeatingMinter {
87 fn mint(&mut self, prefix: &str) -> String {
88 if let Some(again) = self.pending.remove(prefix) {
89 return again;
90 }
91 let id = self.next.mint(prefix);
92 self.pending.insert(prefix.to_owned(), id.clone());
93 id
94 }
95}
96
97#[must_use]
104pub fn random_suffix() -> String {
105 let mut bytes = [0u8; ID_LEN];
106 getrandom::fill(&mut bytes).expect("the OS random source is available");
107 bytes
108 .iter()
109 .map(|b| ALPHABET[usize::from(b & 0x1f)] as char)
110 .collect()
111}
112
113#[must_use]
115pub fn is_valid_id(id: &str, prefix: Option<&str>) -> bool {
116 match prefix_of(id) {
117 Some(p) => prefix.is_none_or(|want| want == p),
118 None => false,
119 }
120}
121
122#[must_use]
124pub fn prefix_of(id: &str) -> Option<&str> {
125 let (prefix, suffix) = id.split_once('_')?;
126 if prefix.is_empty() || !prefix.bytes().all(|b| b.is_ascii_lowercase()) {
127 return None;
128 }
129 if suffix.len() != ID_LEN || !suffix.bytes().all(|b| ALPHABET.contains(&b)) {
130 return None;
131 }
132 Some(prefix)
133}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138 use std::collections::HashSet;
139
140 #[test]
141 fn mints_prefixed_seven_char_crockford_ids() {
142 let id = RandomMinter.mint("b");
143 assert!(is_valid_id(&id, Some("b")), "{id}");
144 assert_eq!(prefix_of(&id), Some("b"));
145 assert_eq!(id.len(), 2 + ID_LEN);
146 for _ in 0..100 {
147 let s = random_suffix();
148 assert_eq!(s.len(), ID_LEN);
149 assert!(!s.contains(['i', 'l', 'o', 'u']), "{s}");
150 }
151 }
152
153 #[test]
154 fn validates_prefix_mismatches() {
155 assert!(!is_valid_id("b_k7z2p9q", Some("d")));
156 assert!(is_valid_id("b_k7z2p9q", Some("b")));
157 assert!(is_valid_id("b_k7z2p9q", None));
158 assert!(!is_valid_id("nope", None));
159 assert!(!is_valid_id("b_TOOLONGX", None));
160 assert!(!is_valid_id("b_k7z2p9", None));
161 assert!(!is_valid_id("b_k7z2p9i", None), "i is not Crockford");
162 assert!(!is_valid_id("B_k7z2p9q", None), "prefix is lowercase");
163 assert!(!is_valid_id("_k7z2p9q", None));
164 assert_eq!(prefix_of("col_k7z2p9q"), Some("col"));
165 assert_eq!(prefix_of("b_0"), None, "fixture ids are not production ids");
166 }
167
168 #[test]
169 fn supports_multi_char_prefixes() {
170 assert!(is_valid_id(&RandomMinter.mint("col"), Some("col")));
171 assert!(is_valid_id(&RandomMinter.mint("cp"), Some("cp")));
172 assert!(is_valid_id(&RandomMinter.mint("src"), Some("src")));
173 }
174
175 #[test]
176 fn mints_with_high_uniqueness() {
177 let seen: HashSet<String> = (0..5000).map(|_| RandomMinter.mint("b")).collect();
178 assert_eq!(seen.len(), 5000);
179 }
180
181 #[test]
182 fn sequential_minter_counts_per_prefix_from_zero() {
183 let mut m = SequentialMinter::new();
184 assert_eq!(m.mint("rp"), "rp_0");
185 assert_eq!(m.mint("b"), "b_0");
186 assert_eq!(m.mint("b"), "b_1");
187 assert_eq!(m.mint("d"), "d_0");
188 assert_eq!(m.mint("b"), "b_2");
189 assert_eq!(m.next("b"), 3);
190 assert_eq!(m.next("c"), 0);
191 let mut closure = |p: &str| format!("{p}_x");
192 let dynamic: &mut dyn IdMinter = &mut closure;
193 assert_eq!(dynamic.mint("q"), "q_x");
194 }
195
196 #[test]
197 fn repeating_minter_offers_every_id_twice_per_prefix() {
198 let mut m = RepeatingMinter::new();
199 assert_eq!(m.mint("rp"), "rp_0");
200 assert_eq!(m.mint("rp"), "rp_0");
201 assert_eq!(m.mint("b"), "b_0");
202 assert_eq!(m.mint("d"), "d_0", "pending is per prefix");
203 assert_eq!(m.mint("b"), "b_0");
204 assert_eq!(m.mint("b"), "b_1");
205 assert_eq!(m.mint("d"), "d_0");
206 assert_eq!(m.mint("b"), "b_1");
207 assert_eq!(m.mint("b"), "b_2");
208 }
209}