1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
|
use rand::{seq::SliceRandom, thread_rng};
use std::fmt;
// Make IDs that:
//
// * Do not require escaping in URLs
// * Do not require escaping in hostnames
// * Are unique up to case conversion
// * Are relatively unlikely to contain cursewords
// * Are relatively unlikely to contain visually similar characters in most typefaces
// * Are not sequential
//
// This leaves 23 ASCII characters, or about 4.52 bits of entropy per character
// if generated with uniform probability.
pub const ALPHABET: [char; 23] = [
'1', '2', '3', '4', '6', '7', '8', '9', 'b', 'c', 'd', 'f', 'h', 'j', 'k', 'n', 'p', 'r', 's',
't', 'w', 'x', 'y',
];
// Pick enough characters per ID to make accidental collisions "acceptably" unlikely
// without also making them _too_ unwieldy. This gives a fraction under 68 bits per ID.
pub const ID_SIZE: usize = 15;
// Intended to be wrapped in a newtype that provides both type-based separation
// from other identifier types, and a unique prefix to allow the intended type
// of an ID to be determined by eyeball when debugging.
//
// By convention, the prefix should be UPPERCASE - note that the alphabet for this
// is entirely lowercase.
#[derive(Debug, Hash, PartialEq, Eq, sqlx::Type, serde::Deserialize)]
#[sqlx(transparent)]
#[serde(transparent)]
pub struct Id(String);
impl fmt::Display for Id {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
impl Id {
pub fn generate<T>(prefix: &str) -> T
where
T: From<Self>,
{
let mut rng = thread_rng();
let id = prefix
.chars()
.chain(
(0..ID_SIZE)
.flat_map(|_| ALPHABET.choose(&mut rng)) /* usize -> &char */
.cloned(), /* &char -> char */
)
.collect::<String>();
T::from(Self(id))
}
}
|