individual-identifiers 0.1.2

A library for creating unique, human-readable alliterative identifiers.
Documentation
use core::fmt;

use rand::seq::SliceRandom;
use reqwest::blocking::Client;
use serde_json::Value;

use uuid::Uuid;

const ERROR_FETCHING_WORD: &str = "Error fetching word.";
const ERROR_PARSING_API: &str = "Error parsing API response.";
const ERROR_API_REQUEST: &str = "Error making API request.";

/// Filters the given words based on the specified letter and tag.
///
/// # Arguments
///
/// * `words` - A slice of word data from the API.
/// * `letter` - The letter that the word must start with.
/// * `tag` - The tag that the word must have.
fn filter_words(words: &[Value], letter: &char, tag: &str) -> Vec<String> {
    words
        .iter()
        .filter_map(|word| {
            let w = word["word"].as_str()?;
            let tags = word["tags"].as_array()?;
            (w.len() >= 4
                && w.len() <= 8
                && w.starts_with(*letter)
                && tags.contains(&Value::String(tag.to_string())))
            .then(|| w.to_owned())
        })
        .collect()
}

/// An identifier that can be in one of three states: Default, Success, or Failure.
#[derive(Clone, PartialEq)]
pub enum Identifier {
    /// The default state when the identifier is first created.
    Default { uuid: Uuid },
    /// The success state when a name has been successfully generated for the identifier.
    Success { uuid: Uuid, name: String },
    /// The failure state when an error occurs while generating a name for the identifier.
    Failure { uuid: Uuid, error: String },
}

impl Identifier {
    /// Creates a new Identifier in the default state.
    pub fn new() -> Self {
        Self::Default {
            uuid: Uuid::new_v4(),
        }
    }

    /// Tries to set a name for the Identifier, transitioning it from the default state into either the success state or the failure state.
    pub fn set(&mut self) -> Self {
        match self {
            Identifier::Success { .. } => self.clone(),
            Identifier::Default { uuid } | Identifier::Failure { uuid, .. } => {
                let letter = (rand::random::<u8>() % 26 + b'a') as char;
                let url = format!(
                    "https://api.datamuse.com/words?sp={0}*&&md=p&max=1000",
                    letter
                );
                let client = Client::new();
                let response = client.get(&url).send();

                match response {
                    Ok(res) => match res.json::<Vec<Value>>() {
                        Ok(words) => {
                            let adjectives = filter_words(&words, &letter, "adj");
                            let nouns = filter_words(&words, &letter, "n");

                            if let (Some(adj), Some(noun)) = (
                                adjectives.choose(&mut rand::thread_rng()).cloned(),
                                nouns.choose(&mut rand::thread_rng()).cloned(),
                            ) {
                                *self = Identifier::Success {
                                    uuid: *uuid,
                                    name: format!("{} {}", adj, noun),
                                };
                            } else {
                                *self = Identifier::Failure {
                                    uuid: *uuid,
                                    error: ERROR_FETCHING_WORD.to_string(),
                                };
                            }
                        }
                        Err(_) => {
                            *self = Identifier::Failure {
                                uuid: *uuid,
                                error: ERROR_PARSING_API.to_string(),
                            };
                        }
                    },
                    Err(_) => {
                        *self = Identifier::Failure {
                            uuid: *uuid,
                            error: ERROR_API_REQUEST.to_string(),
                        };
                    }
                }

                return self.clone();
            }
        }
    }

    /// Returns Uuid value from Identifier for external use.
    fn uuid(&self) -> Uuid {
        self.uuid().clone()
    }

}

impl fmt::Display for Identifier {
    /// Formats the Identifier for display.
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Identifier::Default { uuid } => {
                write!(f, "{}", uuid)
            }

            Identifier::Success { uuid, name } => {
                write!(f, "{}\t({})", name, uuid)
            }

            Identifier::Failure { uuid, error } => {
                write!(f, "{}\tError fetching name:\t{}", uuid, error)
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crossbeam::channel;
    use std::collections::HashMap;
    use std::io::{stdout, Write};
    use std::sync::{
        atomic::{AtomicBool, AtomicUsize, Ordering},
        Arc,
    };
    use std::thread;
    use std::time::{Duration, Instant};

    #[test]
    /// Tests the creation and name-setting of an Identifier.
    fn test_create() {
        // Construct identifier
        let mut identifier = Identifier::new();
        match identifier {
            Identifier::Default { uuid: _ } => {}
            _ => panic!("Expected Default variant."),
        }

        // Set unique name
        identifier.set();
        match identifier {
            Identifier::Success { uuid: _, name: _ } => {}
            _ => panic!("Expected Success variant."),
        }
    }

    #[test]
    /// Tests the uniqueness of the generated names by creating a large number of Identifiers and checking for repeated words and combinations.
    fn test_uniqueness() {
        let num_threads = 2 * num_cpus::get(); // double the number of logical cores
        let pool = rayon::ThreadPoolBuilder::new()
            .num_threads(num_threads)
            .build()
            .unwrap();

        let (tx, rx) = channel::unbounded();
        let total = 1000;
        let start = Arc::new(Instant::now());
        let counter = Arc::new(AtomicUsize::new(0));
        let stop = Arc::new(AtomicBool::new(false));

        let counter_clone = Arc::clone(&counter);
        let start_clone = Arc::clone(&start);
        let stop_clone = Arc::clone(&stop);
        thread::spawn(move || loop {
            thread::sleep(Duration::from_secs(1));
            let elapsed = start_clone.elapsed();
            let count = counter_clone.load(Ordering::SeqCst);
            print!(
                "\rElapsed time: {:0>2}:{:0>2}, Identifiers created: {}/{}",
                elapsed.as_secs() / 60,
                elapsed.as_secs() % 60,
                count,
                total
            );
            stdout().flush().unwrap();
            if count >= total {
                stop_clone.store(true, Ordering::SeqCst);
                break;
            } else if elapsed >= Duration::from_secs(60 * 60) {
                panic!("Test took too long.");
            }
        });

        let stop_clone = Arc::clone(&stop);
        pool.scope(|s| {
            for _ in 0..total {
                if stop_clone.load(Ordering::SeqCst) {
                    return;
                }
                let tx_clone = tx.clone();
                let counter_clone = Arc::clone(&counter);
                s.spawn(move |_| {
                    let mut identifier = Identifier::new();
                    identifier.set();
                    tx_clone.send(identifier).unwrap();
                    counter_clone.fetch_add(1, Ordering::SeqCst);
                });
            }
        });

        drop(tx); // close the channel

        let mut word_counts: HashMap<String, usize> = HashMap::new();
        let mut pair_counts: HashMap<String, usize> = HashMap::new();

        for identifier in rx {
            if let Identifier::Success { name, .. } = identifier {
                let words: Vec<&str> = name.split_whitespace().collect();
                if words.len() == 2 {
                    *word_counts.entry(words[0].to_string()).or_insert(0) += 1;
                    *word_counts.entry(words[1].to_string()).or_insert(0) += 1;
                    *pair_counts.entry(name).or_insert(0) += 1;
                }
            }
        }

        println!("\nUniqueness results:");
        println!(
            "\tRepeated words:\t\t{}",
            word_counts.iter().filter(|(_, count)| **count > 1).count()
        );
        println!(
            "\tRepeated combinations:\t{}",
            pair_counts.iter().filter(|(_, count)| **count > 1).count()
        );

        let mut word_counts_vec: Vec<(&String, &usize)> = word_counts.iter().collect();
        word_counts_vec.sort_by(|a, b| b.1.cmp(a.1)); // sort in descending order

        println!("\n\tTop words:");
        for (word, count) in word_counts_vec.iter().take(3) {
            // take the top 3
            println!("\t\t{}\t\t{}", word, count);
        }
    }
}