summaryrefslogtreecommitdiff
path: root/src/id.rs
blob: aec7a671cc46f7cdff78236a078049bc32aff262 (plain)
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
58
59
60
61
62
63
64
65
66
67
68
69
70
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.
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.
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(
    Clone,
    Debug,
    Hash,
    Eq,
    Ord,
    PartialEq,
    PartialOrd,
    sqlx::Type,
    serde::Deserialize,
    serde::Serialize,
)]
#[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)
                    .filter_map(|_| ALPHABET.choose(&mut rng)) /* usize -> &char */
                    .copied(), /* &char -> char */
            )
            .collect::<String>();
        T::from(Self(id))
    }
}