use rand::{RngExt, distr::Alphanumeric};
pub trait RndStr {
fn rnd_alphanum(len: usize) -> String;
fn rnd_from_alphabet(len: usize, alpha: &[u8]) -> String;
fn rnd_unambig(len: usize, unambig: Unambig) -> String;
}
impl RndStr for String {
fn rnd_alphanum(len: usize) -> String {
rand::rng()
.sample_iter(&Alphanumeric)
.take(len)
.map(char::from)
.collect()
}
fn rnd_from_alphabet(len: usize, charset: &[u8]) -> String {
let mut rng = rand::rng();
(0..len)
.map(|_| {
let idx = rng.random_range(0..charset.len());
charset[idx] as char
})
.collect()
}
fn rnd_unambig(len: usize, unambig: Unambig) -> String {
unambig_rnd_str(len, unambig)
}
}
#[derive(Copy, Clone, Debug, Default)]
pub enum Unambig {
#[default]
Relax,
Strict
}
const UNAMBIG_RELAX: &[u8] = b"ABCDEFGHJKLMNPQRSTUVWXYZ\
abcdefghijkmnpqrstuvwxyz\
23456789";
const UNAMBIG_STRICT: &[u8] = b"CDEFGHJKLMNQRTWXYZ\
abcdefghijkmnqrtwxyz\
23679";
fn unambig_rnd_str(len: usize, unambig: Unambig) -> String {
let mut rng = rand::rng();
let charset = match unambig {
Unambig::Relax => UNAMBIG_RELAX,
Unambig::Strict => UNAMBIG_STRICT
};
(0..len)
.map(|_| {
let idx = rng.random_range(0..charset.len());
charset[idx] as char
})
.collect()
}
#[inline]
#[must_use]
pub fn is_name_leading_char(c: char) -> bool {
c.is_alphabetic()
}
#[inline]
#[must_use]
pub fn is_name_char(c: char) -> bool {
c.is_alphanumeric() || c == '_' || c == '-' || c == '.'
}
pub const OBJNAME_SPEC: &str = "must be non-empty, lead with an alphabetic \
character, with each following character \
being alphanumeric, '_', '-' or '.'";
#[allow(clippy::missing_errors_doc)]
pub fn validate_name<L, R>(s: &str, lead: L, rest: R) -> Result<&str, String>
where
L: Fn(char) -> bool,
R: Fn(char) -> bool
{
let mut chars = s.chars();
let Some(ch) = chars.next() else {
return Err("must not be empty".into());
};
if !lead(ch) {
return Err("invalid leading character".into());
}
if chars.any(|c| !rest(c)) {
return Err("invalid character".into());
}
Ok(s)
}
#[inline]
#[allow(clippy::missing_errors_doc)]
pub fn validate_objname(s: &str) -> Result<&str, String> {
validate_name(s, is_name_leading_char, is_name_char)
}
const OBJNAME_FIRST: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ\
abcdefghijklmnopqrstuvwxyz";
const OBJNAME_CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ\
abcdefghijklmnopqrstuvwxyz\
0123456789-_";
#[must_use]
pub fn random_objname(len: usize) -> String {
assert!(len > 0, "Length must not be zero");
let mut rng1 = rand::rng();
let mut rng2 = rand::rng();
(0..1)
.map(|_| {
let idx = rng1.random_range(0..OBJNAME_FIRST.len());
OBJNAME_FIRST[idx] as char
})
.chain((0..len - 1).map(|_| {
let idx = rng2.random_range(0..OBJNAME_CHARS.len());
OBJNAME_CHARS[idx] as char
}))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[should_panic(expected = "empty name")]
fn bad_empty_name() {
validate_name("", is_name_leading_char, is_name_char).expect("empty name");
}
#[test]
#[should_panic(expected = "invalid leading character")]
fn bad_initial_number_num() {
validate_name("0hello", is_name_leading_char, is_name_char)
.expect("invalid leading character");
}
#[test]
#[should_panic(expected = "invalid leading character")]
fn bad_initial_number_dash() {
validate_name("-hello", is_name_leading_char, is_name_char)
.expect("invalid leading character");
}
#[test]
#[should_panic(expected = "invalid leading character")]
fn bad_initial_number_underscore() {
validate_name("_hello", is_name_leading_char, is_name_char)
.expect("invalid leading character");
}
#[test]
fn good_names() {
validate_objname("hello").unwrap();
validate_objname("hell0").unwrap();
validate_objname("he_ll0").unwrap();
validate_objname("he_ll-0").unwrap();
}
}