use clap::{
ArgAction::{Set, SetFalse, SetTrue},
Parser,
};
use tap::{Pipe, Tap};
use crate::{input, options::output};
#[derive(Debug, thiserror::Error, miette::Diagnostic)]
#[error(transparent)]
pub(crate) enum Error {
#[error("password generation error: {0}")]
PasswordGeneration(&'static str),
Input(#[from] input::Error),
Io(#[from] std::io::Error),
Json(#[from] serde_json::Error),
Output(#[from] output::Error),
Yaml(#[from] serde_yml::Error),
}
#[derive(Debug, Clone, clap::Parser)]
#[command(about = "generates random password(s)")]
#[rustfmt::skip]
pub(crate) struct Command {
#[clap(
env = "PASSWORD_COUNT",
long = "count",
short = '#',
action = Set,
help = "how many passwords to generate",
long_help = "how many passwords to generate.",
value_name = "INTEGER",
default_value = "1",
visible_aliases = ["#AhAhAh"],
visible_short_aliases = ['๐ง'],
)]
count: usize,
#[clap(flatten)]
input: input::InputOrOptions<Options>,
#[clap(flatten)]
output: output::Options,
}
impl crate::Output for Options {}
#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::Parser, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
#[remain::sorted]
#[rustfmt::skip]
#[schemars(rename = "PasswordGenerationOptions")]
pub(crate) struct Options {
#[arg(
group = "usability",
env = "PASSWORD_INCLUDE_SIMILAR_CHARACTERS",
long = "include-similar-characters",
short = '?',
action = SetFalse,
visible_aliases = ["#OnlyBots"],
visible_short_aliases = ['๐ค', '๐ค'],
help_heading = "Password Options",
help = "allow passwords to contain visually similar characters",
long_help = "allow passwords to contain visually similar characters, such as iI1loO0 (and others).",
)]
#[serde(default = "crate::default::bool::<true>")]
exclude_similar_characters: bool,
#[arg(
env = "PASSWORD_LENGTH",
long = "length",
short = '@',
action = Set,
value_name = "INTEGER",
default_value = "16",
visible_aliases = ["#Predictable"],
visible_short_aliases = ['๐', '๐'],
help_heading = "Password Options",
help = "how long the generated password should be",
long_help = "how long the generated password should be. That's it.",
)]
#[serde(default = "crate::default::usize::<16>")]
length: usize,
#[arg(
group = "Password Options",
env = "PASSWORD_EXCLUDE_LOWERCASE",
long = "exclude-lowercase",
short = 'l',
action = SetFalse,
visible_aliases = ["#Normcore"],
visible_short_aliases = ['๐', '๐'],
help_heading = "Password Options",
help = "disallow lowercase letters in passwords",
long_help = "disallow lowercase letters in passwords... but why?",
)]
#[serde(default = "crate::default::bool::<true>")]
lowercase_letters: bool,
#[arg(
group = "Password Options",
env = "PASSWORD_EXCLUDE_NUMBERS",
long = "exclude-numbers",
short = 'N',
action = SetFalse,
visible_aliases = ["#MathIsHard"],
visible_short_aliases = ['๐งฎ', '๐'],
help_heading = "Password Options",
help = "disallow numbers in passwords",
long_help = "disallow numbers in passwords. Did you roll your own parser again?",
)]
#[serde(default = "crate::default::bool::<true>")]
numbers: bool,
#[arg(
group = "Password Options",
env = "PASSWORD_INCLUDE_SPACES",
long = "include-spaces",
short = ' ',
action = SetTrue,
visible_aliases = ["#FML"],
visible_short_aliases = ['๐', '๐คจ'],
help_heading = "Password Options",
help = "allow passwords to contain spaces",
long_help = "allow passwords to contain spaces. Debugging argument splitting is fun, right?",
)]
#[serde(default = "crate::default::bool::<false>")]
spaces: bool,
#[arg(
env = "PASSWORD_STRICT_MODE",
long = "strict",
short = '!',
action = SetTrue,
visible_aliases = ["#SayPlease"],
visible_short_aliases = ['๐ข', '๐'],
help_heading = "Password Options",
help = "enable strict mode",
long_help = "enable strict mode, in which all ALLOWED character sets become REQUIRED.",
)]
#[serde(default = "crate::default::bool::<false>")]
strict: bool,
#[arg(
group = "Password Options",
env = "PASSWORD_INCLUDE_SYMBOLS",
long = "include-symbols",
short = '$',
action = SetTrue,
visible_aliases = ["#EscapeRoom"],
visible_short_aliases = ['๐', '๐'],
help_heading = "Password Options",
help = "allow passwords to include symbols",
long_help = "allow passwords to include symbols. Debugging shell quoting is fun, right?",
)]
#[serde(default = "crate::default::bool::<false>")]
symbols: bool,
#[arg(
group = "Password Options",
env = "PASSWORD_EXCLUDE_UPPERCASE",
long = "exclude-uppercase",
short = 'U',
action = SetFalse,
visible_aliases = ["#CAPSLOCK"],
visible_short_aliases = ['๐ฃ', '๐'],
help_heading = "Password Options",
help = "disallow uppercase letters in passwords",
long_help = "disallow uppercase letters in passwords. Sure... you do you.",
)]
#[serde(default = "crate::default::bool::<true>")]
uppercase_letters: bool,
}
impl Default for Options {
fn default() -> Self {
Self {
exclude_similar_characters: true,
length: 16,
lowercase_letters: true,
numbers: true,
spaces: false,
strict: false,
symbols: false,
uppercase_letters: true,
}
}
}
impl Command {
pub(crate) async fn run(
self,
valkey: &crate::integrations::fred::ValkeyOptions,
) -> Result<Box<dyn crate::Output>, Error> {
let Self { count, input, output } = self;
let generator = input.consume()?.pipe(passwords::PasswordGenerator::from);
output.produce::<_, _, _, Options>("passwords", generator, |generator, object, key| {
generator
.generate(count)
.map_err(Error::PasswordGeneration)
.map(|passwords| {
object.insert(key.plural, Box::new(passwords));
})
})?;
Ok(Box::new(()))
}
}
crate::macros::impl_from! {
Options => passwords::PasswordGenerator {
move {
exclude_similar_characters,
length,
lowercase_letters,
numbers,
spaces,
strict,
symbols,
uppercase_letters
}
}
}
crate::macros::impl_from! {
passwords::PasswordGenerator => Options {
move {
exclude_similar_characters,
length,
lowercase_letters,
numbers,
spaces,
strict,
symbols,
uppercase_letters
}
}
}
#[cfg(test)]
mod tests {
use super::*;
crate::macros::fn_default_matches_clap_parser!(Options);
crate::macros::fn_default_matches_serde!(Options);
}