orphanage 0.5.6

Random collection of stuff that is still searching for a home.
Documentation
//! Extended str/string functionality.

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 {
  /// Generate a random alphanumeric string of a requested length.
  fn rnd_alphanum(len: usize) -> String {
    rand::rng()
      .sample_iter(&Alphanumeric)
      .take(len)
      .map(char::from)
      .collect()
  }

  /// Generate a random string of a requested length, given an alphabet of
  /// acceptable characters.
  ///
  /// ```
  /// use orphanage::strx::RndStr;
  ///
  /// const CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ\
  ///                          abcdefghijklmnopqrstuvwxyz\
  ///                          0123456789-_";
  ///
  /// let s = String::rnd_from_alphabet(16, CHARSET);
  /// assert_eq!(s.len(), 16);
  /// ```
  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()
  }

  /// Generate random strings containing alphanumeric characters, but exclude
  /// commonly ambiguous characters.
  ///
  /// The `unambig` argument is used to control the "strictness" of how to
  /// interpret "ambiguous characters".
  fn rnd_unambig(len: usize, unambig: Unambig) -> String {
    unambig_rnd_str(len, unambig)
  }
}


#[derive(Copy, Clone, Debug, Default)]
pub enum Unambig {
  /// Exlcude a smaller set of what is, subjectively, the most common
  /// ambiguous characters.
  ///
  /// These can be difficult to distinguish even with relatively good fonts.
  #[default]
  Relax,

  /// Exclude a greater set of ambigious characters.  This set includes all of
  /// the "relaxed" characters as well.
  ///
  /// These can be difficult to distinguish with bad fonts.
  Strict
}


/// # Excluded
/// `{I, l, 1}`, `{O, o, 0}`
const UNAMBIG_RELAX: &[u8] = b"ABCDEFGHJKLMNPQRSTUVWXYZ\
                               abcdefghijkmnpqrstuvwxyz\
                               23456789";

/// # Excluded
/// `{A, 4}`, `{B, 8}`, `{I, l, 1}`, `{O, o, 0}`, `{P, p}`, `{U, V, u, v}`,
/// `{s, S, 5}`
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-_";

/// Generate a random (valid) object name
///
/// # Panics
/// The requested length must not be zero.
#[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();
  }
}

// vim: set ft=rust et sw=2 ts=2 sts=2 cinoptions=2 tw=79 :