hubworld 0.3.1

A Rust library for generating random data: UUIDs, names, passwords, and credit cards
Documentation
use crate::credit_card::CreditCardOutputFormat;
use rand::seq::SliceRandom;
use rand::Rng;

const LOWERCASE: &[u8] = b"abcdefghijklmnopqrstuvwxyz";
const UPPERCASE: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ";
const NUMBERS: &[u8] = b"0123456789";
const SYMBOLS: &[u8] = b"!@#$%^&*()_+-=[]{}|;:,.<>?";

/// Generates a random password with default length of 16 characters
pub fn generate_password() -> String {
    generate_password_with_length(16)
}

/// Generates a random password with default length and returns in the selected format.
pub fn generate_password_formatted(format: CreditCardOutputFormat) -> String {
    let password = generate_password();
    format_password(&password, format)
}

/// Generates a random password with a specified length (minimum 4)
pub fn generate_password_with_length(length: usize) -> String {
    let length = length.max(4);
    let mut rng = rand::thread_rng();
    let mut password = Vec::with_capacity(length);

    password.push(*LOWERCASE.choose(&mut rng).unwrap());
    password.push(*UPPERCASE.choose(&mut rng).unwrap());
    password.push(*NUMBERS.choose(&mut rng).unwrap());
    password.push(*SYMBOLS.choose(&mut rng).unwrap());

    let all_chars: Vec<u8> = [LOWERCASE, UPPERCASE, NUMBERS, SYMBOLS]
        .iter()
        .flat_map(|&set| set.iter())
        .copied()
        .collect();

    while password.len() < length {
        password.push(*all_chars.choose(&mut rng).unwrap());
    }

    for i in (1..password.len()).rev() {
        let j = rng.gen_range(0..=i);
        password.swap(i, j);
    }

    String::from_utf8(password).unwrap()
}

/// Generates a random password with a specified length and returns in the selected format.
pub fn generate_password_with_length_formatted(
    length: usize,
    format: CreditCardOutputFormat,
) -> String {
    let password = generate_password_with_length(length);
    format_password(&password, format)
}

fn format_password(password: &str, format: CreditCardOutputFormat) -> String {
    match format {
        CreditCardOutputFormat::Plain => password.to_string(),
        CreditCardOutputFormat::Json => format!("{{\"password\":\"{}\"}}", password),
        CreditCardOutputFormat::JsonLd => format!(
            "{{\"@context\":\"https://schema.org\",\"@type\":\"PropertyValue\",\"name\":\"password\",\"value\":\"{}\"}}",
            password
        ),
    }
}

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

    #[test]
    fn test_generate_password_default_length() {
        let password = generate_password();
        assert_eq!(password.len(), 16);
    }

    #[test]
    fn test_generate_password_custom_length() {
        let password = generate_password_with_length(20);
        assert_eq!(password.len(), 20);
    }

    #[test]
    fn test_generate_password_minimum_length() {
        let password = generate_password_with_length(2);
        assert_eq!(password.len(), 4);
    }

    #[test]
    fn test_password_contains_all_categories() {
        let password = generate_password();

        let has_lowercase = password.chars().any(|c| c.is_ascii_lowercase());
        let has_uppercase = password.chars().any(|c| c.is_ascii_uppercase());
        let has_number = password.chars().any(|c| c.is_ascii_digit());
        let has_symbol = password.chars().any(|c| SYMBOLS.contains(&(c as u8)));

        assert!(has_lowercase, "Password should contain lowercase");
        assert!(has_uppercase, "Password should contain uppercase");
        assert!(has_number, "Password should contain number");
        assert!(has_symbol, "Password should contain symbol");
    }

    #[test]
    fn test_passwords_are_different() {
        let p1 = generate_password();
        let p2 = generate_password();
        let p3 = generate_password();

        assert!(!(p1 == p2 && p2 == p3));
    }

    #[test]
    fn test_generate_password_formatted_json() {
        let output = generate_password_formatted(CreditCardOutputFormat::Json);
        assert!(output.contains("\"password\":\""));
    }

    #[test]
    fn test_generate_password_formatted_jsonld() {
        let output = generate_password_formatted(CreditCardOutputFormat::JsonLd);
        assert!(output.contains("\"@context\":\"https://schema.org\""));
        assert!(output.contains("\"name\":\"password\""));
    }
}