feat: add generic slice choice helper

Add OsRandom::choose for callers that want a uniformly selected reference from
any non-empty slice. This covers the common generic selection use case without
adding collection algorithms such as shuffle.

Keep OsRandom::pick as the byte-oriented helper by delegating it through
choose. This preserves the existing public API while sharing the usize range
sampler and avoiding the old u32 length limit.

Document choose, pick, and string_from with working examples, and add a choices
example that demonstrates generic slice selection alongside byte alphabets.

Test Plan:
- cargo test
- cargo clippy
- cargo clippy --benches
- cargo clippy --tests
- cargo +nightly fmt

Refs: IDEAS.md ergonomics backlog
This commit is contained in:
2026-04-28 20:04:57 +02:00
parent b8f9fa9b1b
commit 0365ab86d1
2 changed files with 71 additions and 6 deletions
+55 -6
View File
@@ -13,6 +13,8 @@
//! - Convenience byte readers fill caller-provided buffers.
//! - Sampling helpers build unbiased higher-level choices from those primitive
//! readers.
//! - Range helpers sample integer spans.
//! - Slice helpers choose caller-owned values by reference.
//! - [`charset`] contains reusable ASCII alphabets for string generation.
use std::{
@@ -423,16 +425,51 @@ impl OsRandom {
/// Returns a uniformly chosen byte from `set`.
///
/// # Panics
/// Panics if `set` is empty or longer than `u32::MAX`.
/// Panics if `set` is empty.
///
/// # Errors
/// Returns any I/O error produced while reading from `/dev/urandom`.
///
/// # Examples
///
/// ```
/// # fn main() -> std::io::Result<()> {
/// let mut rng = ez_urandom::OsRandom::try_new()?;
/// let digit = rng.pick(ez_urandom::charset::DIGITS)?;
/// assert!(digit.is_ascii_digit());
/// # Ok(())
/// # }
/// ```
pub fn pick(&mut self, set: &[u8]) -> io::Result<u8> {
assert!(!set.is_empty(), "set must not be empty");
let n = u32::try_from(set.len()).expect("set must fit in u32");
let i = usize::try_from(self.gen_range_u32(n)?)
.expect("u32 fits in usize on supported platforms");
Ok(set[i])
Ok(*self.choose(set)?)
}
/// Returns a uniformly chosen item from `items`.
///
/// The returned reference points into the caller-provided slice; no item is
/// cloned or moved.
///
/// # Panics
/// Panics if `items` is empty.
///
/// # Errors
/// Returns any I/O error produced while reading from `/dev/urandom`.
///
/// # Examples
///
/// ```
/// # fn main() -> std::io::Result<()> {
/// let mut rng = ez_urandom::OsRandom::try_new()?;
/// let colors = ["red", "green", "blue"];
/// let color = rng.choose(&colors)?;
/// assert!(colors.contains(color));
/// # Ok(())
/// # }
/// ```
pub fn choose<'a, T>(&mut self, items: &'a [T]) -> io::Result<&'a T> {
assert!(!items.is_empty(), "items must not be empty");
let i = self.gen_range_usize(items.len())?;
Ok(&items[i])
}
/// Returns a `String` of `len` characters drawn uniformly from `set`.
@@ -443,6 +480,18 @@ impl OsRandom {
///
/// # Errors
/// Returns any I/O error produced while reading from `/dev/urandom`.
///
/// # Examples
///
/// ```
/// # fn main() -> std::io::Result<()> {
/// let mut rng = ez_urandom::OsRandom::try_new()?;
/// let token = rng.string_from(ez_urandom::charset::ALPHANUMERIC, 24)?;
/// assert_eq!(token.len(), 24);
/// assert!(token.is_ascii());
/// # Ok(())
/// # }
/// ```
pub fn string_from(&mut self, set: &[u8], len: usize) -> io::Result<String> {
assert!(set.is_ascii(), "set must contain only ASCII bytes");
let mut buf = vec![0u8; len];