bento-kit 0.1.1

A bento box of common Rust utilities: id generation, timing, masking
Documentation
//! Random number / string helpers.
//!
//! Direct ports of `randomInt` and `randomString` from
//! [`du-node-utils/lib/id.js`](https://github.com/imcooder/du-node-utils/blob/main/lib/id.js).
//!
//! These are **not** cryptographically secure (matching the Node behavior,
//! which uses `Math.random()`). For tokens and secrets, use a CSPRNG via
//! the [`rand`](https://docs.rs/rand) crate's `OsRng` directly.

use rand::Rng;

/// Alphabet used by [`random_string`], identical to the Node version's
/// keyboard-order 36-char set.
pub(crate) const RANDOM_STRING_ALPHABET: &[u8] = b"0123456789qwertyuioplkjhgfdsazxcvbnm";

/// Base36 alphabet used by the internal random-string helper, mirroring
/// the output of Node's `Math.random().toString(36)`.
pub(crate) const BASE36_ALPHABET: &[u8] = b"0123456789abcdefghijklmnopqrstuvwxyz";

/// Generate a random integer in the half-open range `[min, max)`.
///
/// Mirrors `id.randomInt(min, max)` from the Node library.
///
/// # Panics
///
/// Panics if `min >= max` (consistent with `rand::Rng::gen_range`).
///
/// ```
/// let n = bento_kit::id::random_int(0, 10);
/// assert!((0..10).contains(&n));
/// ```
pub fn random_int(min: i64, max: i64) -> i64 {
    rand::thread_rng().gen_range(min..max)
}

/// Generate a random string of length `len` using the same 36-char
/// alphabet as Node's `id.randomString(len)`:
/// `0-9` plus `qwertyuioplkjhgfdsazxcvbnm` (keyboard order).
///
/// ```
/// let s = bento_kit::id::random_string(20);
/// assert_eq!(s.len(), 20);
/// ```
pub fn random_string(len: usize) -> String {
    pick_chars(RANDOM_STRING_ALPHABET, len)
}

/// Internal helper matching Node's private `generateRandomString(length)`,
/// which produces a base36 string. Used to build session prefixes and
/// session-ID random suffixes.
pub(crate) fn generate_random_string(len: usize) -> String {
    pick_chars(BASE36_ALPHABET, len)
}

fn pick_chars(alphabet: &[u8], len: usize) -> String {
    let mut rng = rand::thread_rng();
    (0..len)
        .map(|_| {
            let idx = rng.gen_range(0..alphabet.len());
            alphabet[idx] as char
        })
        .collect()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn random_int_is_in_range() {
        for _ in 0..1000 {
            let n = random_int(5, 10);
            assert!((5..10).contains(&n), "{n} out of range");
        }
    }

    #[test]
    fn random_int_negative_range() {
        for _ in 0..200 {
            let n = random_int(-10, -1);
            assert!((-10..-1).contains(&n));
        }
    }

    #[test]
    fn random_string_has_expected_length() {
        for len in [0_usize, 1, 8, 32, 256] {
            assert_eq!(random_string(len).len(), len);
        }
    }

    #[test]
    fn random_string_uses_node_alphabet() {
        let s = random_string(500);
        for c in s.chars() {
            assert!(
                RANDOM_STRING_ALPHABET.contains(&(c as u8)),
                "char {c:?} not in alphabet"
            );
        }
    }

    #[test]
    fn generate_random_string_is_base36() {
        let s = generate_random_string(500);
        for c in s.chars() {
            assert!(
                c.is_ascii_digit() || c.is_ascii_lowercase(),
                "char {c:?} is not base36"
            );
        }
    }
}