dspg 1.0.1

Dan's Simple Password Generator. Generate passwords in a format I like.
Documentation
mod words;

use crate::words::WORDS;
use clap::Parser;
use rand::seq::{IteratorRandom, SliceRandom};
use rand::Rng;

/// Dan’s Password Generator
#[derive(Parser, Debug)]
#[command(version, about, long_about = None)]
pub struct Args {
    /// Number of words to include in the password
    #[arg(
        short = 'n',
        long = "num",
        default_value_t = 5,
        value_parser = clap::value_parser!(u8).range(3..)
    )]
    number_of_words: u8,

    /// Minimum word length to use
    #[arg(
        short = 'm',
        long = "min",
        default_value_t = 3,
        value_parser = clap::value_parser!(u8).range(3..=8)
    )]
    min_word_length: u8,

    /// Maximum word length to use
    #[arg(
        short = 'x',
        long = "max",
        default_value_t = 8,
        value_parser = clap::value_parser!(u8).range(3..=8)
    )]
    max_word_length: u8,
}

const SYMBOLS: &str = "-!@$%&*_+=#^.";
const DIGITS: &str = "0123456789";

pub fn password(args: Args) -> String {
    let number_of_words = args.number_of_words;
    let caps_word = rand::thread_rng().gen_range(0..number_of_words);

    let filtered_words = words(args.min_word_length, args.max_word_length);

    let number_of_digits = number_of_words - 2;
    let symbol_joiner = rand::thread_rng().gen_range(0..number_of_digits);

    (0..number_of_words)
        .map(|n| {
            let word = if n == caps_word {
                filtered_words
                    .choose(&mut rand::thread_rng())
                    .unwrap()
                    .to_uppercase()
            } else {
                filtered_words
                    .choose(&mut rand::thread_rng())
                    .unwrap()
                    .to_string()
            };

            if n + 1 < number_of_words {
                let joiner = if n == symbol_joiner {
                    random_char(SYMBOLS)
                } else {
                    random_char(DIGITS)
                };
                format!("{}{}", word, joiner)
            } else {
                word
            }
        })
        .collect::<Vec<String>>()
        .join("")
}

fn random_char(input: &str) -> char {
    input.chars().choose(&mut rand::thread_rng()).unwrap()
}

fn words(min: u8, max: u8) -> Vec<&'static str> {
    WORDS
        .iter()
        .filter(|&&word| {
            let len: u8 = word.len().try_into().unwrap();
            len >= min && len <= max
        })
        .copied()
        .collect()
}