human_friendly_ids/
distribution.rs

1// src/distribution.rs
2//! Random generation of user-friendly IDs
3
4use rand::{Rng, distr::Distribution};
5
6use crate::{UploadId, alphabet};
7
8/// Distribution for generating IDs of specific length
9///
10/// # Example
11/// ```
12/// use human_friendly_ids::UploadIdDist;
13/// use rand::{Rng, distr::Distribution};
14///
15/// let dist = UploadIdDist::<8>;
16/// let id = dist.sample(&mut rand::thread_rng());
17/// ```
18#[derive(Debug, Clone)]
19pub struct UploadIdDist<const N: usize>;
20
21impl<const N: usize> UploadIdDist<N> {
22    /// Create new distribution with compile-time length check
23    #[must_use]
24    pub const fn new() -> Self {
25        assert!(N >= 3, "ID length must be at least 3 characters");
26        Self
27    }
28}
29
30impl<const N: usize> Default for UploadIdDist<N> {
31    fn default() -> Self {
32        Self::new()
33    }
34}
35
36impl<const N: usize> Distribution<UploadId> for UploadIdDist<N> {
37    fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> UploadId {
38        debug_assert!(N >= 3, "ID length must be at least 3 characters");
39
40        let mut body = String::with_capacity(N.saturating_sub(1));
41        let mut last_char = None;
42
43        while body.len() < N.saturating_sub(1) {
44            let idx = rng.random_range(0..alphabet::GEN_ALPHABET.len());
45            #[allow(clippy::indexing_slicing, reason = "index is generated within bounds")]
46            let c = alphabet::GEN_ALPHABET[idx];
47            // Avoid ambiguous sequences
48            match (last_char, c) {
49                (Some('r'), 'n') | (Some('v'), 'v') => {}
50                // Don't end with 'r' or 'v', because the check-bit could create an ambiguous sequence
51                (_, 'r' | 'v') if body.len() == N.saturating_sub(2) => {}
52                _ => {
53                    body.push(c);
54                    last_char = Some(c);
55                }
56            }
57        }
58
59        let check_char = alphabet::calculate_check_char(&body)
60            .expect("Generated body should be valid for check calculation");
61
62        UploadId(format!("{}{}", body, check_char))
63    }
64}