skytool 0.1.0-pre.2

an experimental API client for BlueSky / ATProto
Documentation
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);
}