mod cli;
use cli::Cli;
use petname::Alliterations;
use petname::{Generator, Petnames};
use std::fmt;
use std::fs;
use std::io;
use std::path;
use std::process;
use clap::Parser;
use rand::SeedableRng;
fn main() {
let cli = Cli::parse();
let result = {
let stdout = io::stdout();
let mut writer = io::BufWriter::new(stdout.lock());
run(cli, &mut writer)
};
match result {
Ok(()) | Err(Error::Disconnected) => {
process::exit(0);
}
Err(e) => {
eprintln!("Error: {e}");
process::exit(1);
}
}
}
#[derive(Debug)]
enum Error {
Io(io::Error),
FileIo(path::PathBuf, io::Error),
Cardinality(String),
Alliteration(String),
Disconnected,
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
Error::Io(ref e) => write!(f, "{e}"),
Error::FileIo(ref path, ref e) => write!(f, "{e}: {}", path.display()),
Error::Cardinality(ref message) => write!(f, "cardinality is zero: {message}"),
Error::Alliteration(ref message) => write!(f, "cannot alliterate: {message}"),
Error::Disconnected => write!(f, "caller disconnected / stopped reading"),
}
}
}
impl From<io::Error> for Error {
fn from(error: io::Error) -> Self {
Error::Io(error)
}
}
fn run<OUT>(cli: Cli, writer: &mut OUT) -> Result<(), Error>
where
OUT: io::Write,
{
let words = match cli.directory {
Some(dirname) => Words::load(dirname)?,
None => Words::Builtin,
};
let mut petnames = match words {
Words::Custom(ref adjectives, ref adverbs, ref nouns) => Petnames::new(adjectives, adverbs, nouns),
Words::Builtin => match cli.lists {
cli::WordList::Small => Petnames::small(),
cli::WordList::Medium => Petnames::medium(),
cli::WordList::Large => Petnames::large(),
},
};
let letters = cli.letters;
if letters != 0 {
petnames.retain(|s| s.len() <= letters);
}
if petnames.cardinality(cli.words) == 0 {
return Err(Error::Cardinality("no petnames to choose from; try relaxing constraints".to_string()));
}
let mut rng =
cli.seed.map(rand::rngs::StdRng::seed_from_u64).unwrap_or_else(rand::rngs::StdRng::from_entropy);
let count = if cli.stream { None } else { Some(cli.count) };
if cli.alliterate || cli.ubuntu {
let mut alliterations: Alliterations = petnames.into();
alliterations.retain(|_, group| group.cardinality(cli.words) > 0);
if alliterations.cardinality(cli.words) == 0 {
return Err(Error::Alliteration("word lists have no initial letters in common".to_string()));
}
printer(writer, alliterations.iter(&mut rng, cli.words, &cli.separator), count)
} else if let Some(alliterate_with) = cli.alliterate_with {
let mut alliterations: Alliterations = petnames.into();
alliterations.retain(|first_letter, group| {
*first_letter == alliterate_with && group.cardinality(cli.words) > 0
});
if alliterations.cardinality(cli.words) == 0 {
return Err(Error::Alliteration(
"no petnames begin with the chosen alliteration character".to_string(),
));
}
printer(writer, alliterations.iter(&mut rng, cli.words, &cli.separator), count)
} else {
printer(writer, petnames.iter(&mut rng, cli.words, &cli.separator), count)
}
}
fn printer<OUT, NAMES>(writer: &mut OUT, names: NAMES, count: Option<usize>) -> Result<(), Error>
where
OUT: io::Write,
NAMES: Iterator<Item = String>,
{
match count {
None => {
for name in names {
writeln!(writer, "{name}").map_err(suppress_disconnect)?;
}
}
Some(n) => {
for name in names.take(n) {
writeln!(writer, "{name}")?;
}
}
}
writer.flush().map_err(suppress_disconnect)?;
Ok(())
}
enum Words {
Custom(String, String, String),
Builtin,
}
impl Words {
fn load<T: AsRef<path::Path>>(dirname: T) -> Result<Self, Error> {
let dirname = dirname.as_ref();
Ok(Self::Custom(
read_file_to_string(dirname.join("adjectives.txt"))?,
read_file_to_string(dirname.join("adverbs.txt"))?,
match read_file_to_string(dirname.join("nouns.txt")) {
Ok(nouns) => nouns,
Err(err) => match read_file_to_string(dirname.join("names.txt")) {
Ok(nouns) => nouns,
Err(_) => Err(err)?, },
},
))
}
}
fn read_file_to_string<P: AsRef<path::Path>>(path: P) -> Result<String, Error> {
fs::read_to_string(&path).map_err(|error| Error::FileIo(path.as_ref().to_path_buf(), error))
}
fn suppress_disconnect(err: io::Error) -> Error {
match err.kind() {
io::ErrorKind::BrokenPipe => Error::Disconnected,
_ => err.into(),
}
}
#[cfg(test)]
mod integration {
use std::fs;
use clap::Parser;
fn run_and_capture(cli: super::Cli) -> String {
let mut stdout = Vec::new();
super::run(cli, &mut stdout).unwrap();
String::from_utf8(stdout).unwrap()
}
#[test]
fn option_words() {
let cli = super::Cli::parse_from(["petname", "--words=5"]);
assert_eq!(run_and_capture(cli).split('-').count(), 5);
}
#[test]
fn option_letters() {
let cli = super::Cli::parse_from(["petname", "--letters=3", "--count=100", "--separator= "]);
assert_eq!(run_and_capture(cli).split_whitespace().map(str::len).max(), Some(3))
}
#[test]
fn option_separator() {
let cli = super::Cli::parse_from(["petname", "--separator=<:>"]);
assert_eq!(run_and_capture(cli).split("<:>").count(), 2)
}
#[test]
fn option_dir_nouns() -> anyhow::Result<()> {
let dir = tempdir::TempDir::new("petname")?;
fs::write(dir.path().join("adverbs.txt"), "adverb")?;
fs::write(dir.path().join("adjectives.txt"), "adjective")?;
fs::write(dir.path().join("nouns.txt"), "noun")?;
let args: &[std::ffi::OsString] =
&["petname".into(), "--dir".into(), dir.path().into(), "--words=3".into()];
let cli = super::Cli::parse_from(args);
assert_eq!(run_and_capture(cli), "adverb-adjective-noun\n");
Ok(())
}
#[test]
fn compat_dir_nouns_before_names() -> anyhow::Result<()> {
let dir = tempdir::TempDir::new("petname")?;
fs::write(dir.path().join("adverbs.txt"), "adverb")?;
fs::write(dir.path().join("adjectives.txt"), "adjective")?;
fs::write(dir.path().join("nouns.txt"), "noun")?;
fs::write(dir.path().join("names.txt"), "name")?;
let args: &[std::ffi::OsString] =
&["petname".into(), "--dir".into(), dir.path().into(), "--words=3".into()];
let cli = super::Cli::parse_from(args);
assert_eq!(run_and_capture(cli), "adverb-adjective-noun\n");
Ok(())
}
#[test]
fn compat_dir_names() -> anyhow::Result<()> {
let dir = tempdir::TempDir::new("petname")?;
fs::write(dir.path().join("adverbs.txt"), "adverb")?;
fs::write(dir.path().join("adjectives.txt"), "adjective")?;
fs::write(dir.path().join("names.txt"), "name")?;
let args: &[std::ffi::OsString] =
&["petname".into(), "--dir".into(), dir.path().into(), "--words=3".into()];
let cli = super::Cli::parse_from(args);
assert_eq!(run_and_capture(cli), "adverb-adjective-name\n");
Ok(())
}
#[test]
fn option_lists() {
let cli = super::Cli::parse_from(["petname", "--lists=large"]);
assert!(!run_and_capture(cli).is_empty());
}
#[test]
fn compat_complexity() {
let cli = super::Cli::parse_from(["petname", "--complexity=2"]);
assert!(!run_and_capture(cli).is_empty());
}
#[test]
fn option_alliterate() {
let cli = super::Cli::parse_from(["petname", "--alliterate", "--words=3"]);
let first_letters: std::collections::HashSet<char> =
run_and_capture(cli).split('-').map(|word| word.chars().next().unwrap()).collect();
assert_eq!(first_letters.len(), 1);
}
#[test]
fn option_alliterate_with() {
let cli = super::Cli::parse_from(["petname", "--alliterate-with=a", "--words=3"]);
let first_letters: std::collections::HashSet<char> =
run_and_capture(cli).split('-').map(|word| word.chars().next().unwrap()).collect();
assert_eq!(first_letters, ['a'].into());
}
#[test]
fn compat_ubuntu() {
let cli = super::Cli::parse_from(["petname", "--ubuntu", "--words=3"]);
let first_letters: std::collections::HashSet<char> =
run_and_capture(cli).split('-').map(|word| word.chars().next().unwrap()).collect();
assert_eq!(first_letters.len(), 1);
}
#[test]
fn option_seed() {
let cli = super::Cli::parse_from(["petname", "--seed=12345", "--words=3"]);
assert_eq!(run_and_capture(cli), "meaningfully-enthralled-pinscher\n");
}
}