bento_kit/id/random.rs
1//! Random number / string helpers.
2//!
3//! Direct ports of `randomInt` and `randomString` from
4//! [`du-node-utils/lib/id.js`](https://github.com/imcooder/du-node-utils/blob/main/lib/id.js).
5//!
6//! These are **not** cryptographically secure (matching the Node behavior,
7//! which uses `Math.random()`). For tokens and secrets, use a CSPRNG via
8//! the [`rand`](https://docs.rs/rand) crate's `OsRng` directly.
9
10use rand::Rng;
11
12/// Alphabet used by [`random_string`], identical to the Node version's
13/// keyboard-order 36-char set.
14pub(crate) const RANDOM_STRING_ALPHABET: &[u8] = b"0123456789qwertyuioplkjhgfdsazxcvbnm";
15
16/// Base36 alphabet used by the internal random-string helper, mirroring
17/// the output of Node's `Math.random().toString(36)`.
18pub(crate) const BASE36_ALPHABET: &[u8] = b"0123456789abcdefghijklmnopqrstuvwxyz";
19
20/// Generate a random integer in the half-open range `[min, max)`.
21///
22/// Mirrors `id.randomInt(min, max)` from the Node library.
23///
24/// # Panics
25///
26/// Panics if `min >= max` (consistent with `rand::Rng::gen_range`).
27///
28/// ```
29/// let n = bento_kit::id::random_int(0, 10);
30/// assert!((0..10).contains(&n));
31/// ```
32pub fn random_int(min: i64, max: i64) -> i64 {
33 rand::thread_rng().gen_range(min..max)
34}
35
36/// Generate a random string of length `len` using the same 36-char
37/// alphabet as Node's `id.randomString(len)`:
38/// `0-9` plus `qwertyuioplkjhgfdsazxcvbnm` (keyboard order).
39///
40/// ```
41/// let s = bento_kit::id::random_string(20);
42/// assert_eq!(s.len(), 20);
43/// ```
44pub fn random_string(len: usize) -> String {
45 pick_chars(RANDOM_STRING_ALPHABET, len)
46}
47
48/// Internal helper matching Node's private `generateRandomString(length)`,
49/// which produces a base36 string. Used to build session prefixes and
50/// session-ID random suffixes.
51pub(crate) fn generate_random_string(len: usize) -> String {
52 pick_chars(BASE36_ALPHABET, len)
53}
54
55fn pick_chars(alphabet: &[u8], len: usize) -> String {
56 let mut rng = rand::thread_rng();
57 (0..len)
58 .map(|_| {
59 let idx = rng.gen_range(0..alphabet.len());
60 alphabet[idx] as char
61 })
62 .collect()
63}
64
65#[cfg(test)]
66mod tests {
67 use super::*;
68
69 #[test]
70 fn random_int_is_in_range() {
71 for _ in 0..1000 {
72 let n = random_int(5, 10);
73 assert!((5..10).contains(&n), "{n} out of range");
74 }
75 }
76
77 #[test]
78 fn random_int_negative_range() {
79 for _ in 0..200 {
80 let n = random_int(-10, -1);
81 assert!((-10..-1).contains(&n));
82 }
83 }
84
85 #[test]
86 fn random_string_has_expected_length() {
87 for len in [0_usize, 1, 8, 32, 256] {
88 assert_eq!(random_string(len).len(), len);
89 }
90 }
91
92 #[test]
93 fn random_string_uses_node_alphabet() {
94 let s = random_string(500);
95 for c in s.chars() {
96 assert!(
97 RANDOM_STRING_ALPHABET.contains(&(c as u8)),
98 "char {c:?} not in alphabet"
99 );
100 }
101 }
102
103 #[test]
104 fn generate_random_string_is_base36() {
105 let s = generate_random_string(500);
106 for c in s.chars() {
107 assert!(
108 c.is_ascii_digit() || c.is_ascii_lowercase(),
109 "char {c:?} is not base36"
110 );
111 }
112 }
113}