use rand::seq::IndexedRandom;
use std::collections::HashSet;
mod constraints;
pub mod errors;
mod utils;
pub mod words;
use crate::{
constraints::{DEFAULT_SEPARATOR, MIN_SEGMENTS},
errors::ValidationError,
utils::*,
words::{MAX_SEGMENTS, PREFIXES},
};
#[derive(Debug)]
pub struct Options {
pub segments: usize,
pub separator: char,
pub max_length: Option<usize>,
}
impl Default for Options {
fn default() -> Self {
Self {
segments: MIN_SEGMENTS,
separator: DEFAULT_SEPARATOR,
max_length: None,
}
}
}
impl Options {
fn validate(&self) -> Option<ValidationError> {
if self.segments > *MAX_SEGMENTS {
return Some(ValidationError::TooManySegments);
}
if self.segments < MIN_SEGMENTS {
return Some(ValidationError::TooFewSegments);
}
if let Some(v) = self.max_length
&& v < 1
{
return Some(ValidationError::InvalidValue(v));
}
None
}
}
pub fn joyful(options: Options) -> Result<String, ValidationError> {
if let Some(validation_error) = options.validate() {
return Err(validation_error);
}
let mut used = HashSet::new();
let mut words: Vec<String> = vec![];
let mut add_word = |prefix: String| {
used.insert(prefix.clone());
words.push(prefix);
};
let Some(max_length) = options.max_length else {
let prefix = PREFIXES.choose(&mut rand::rng()).unwrap().to_owned();
add_word(prefix);
for _ in 1..options.segments {
let word = get_unique_category(&used);
used.insert(word.clone());
words.push(word)
}
return Ok(words.join(&options.separator.to_string()));
};
let prefix_budget = calc_budget(max_length, 0, options.segments - 1, false);
let prefix =
get_prefix_with_budget(prefix_budget).ok_or(ValidationError::LengthConstraintImpossible)?;
let mut used_length = prefix.len();
add_word(prefix);
for i in 1..options.segments {
let words_still_needed = options.segments - i - 1;
let next_word_budget = calc_budget(max_length, used_length, words_still_needed, true);
let word = get_unique_word_with_budget(&used, next_word_budget)
.ok_or(ValidationError::LengthConstraintImpossible)?;
used_length += word.len() + 1;
used.insert(word.clone());
words.push(word);
}
Ok(words.join(&options.separator.to_string()))
}
#[cfg(test)]
mod test {
use super::*;
use crate::words::PREFIXES;
#[test]
fn it_works_with_defaults() {
let generated = joyful(Options::default()).unwrap();
let splitted = generated.split(DEFAULT_SEPARATOR);
assert_eq!(splitted.count(), MIN_SEGMENTS)
}
#[test]
fn ensure_randomness() {
let mut map: HashSet<String> = HashSet::new();
for _ in 0..20 {
let r = joyful(Options::default()).unwrap();
match map.get(&r) {
Some(_) => {
panic!("broken randomness");
}
None => {
map.insert(r);
}
}
}
assert_eq!(map.len(), 20);
}
#[test]
fn word_uniqness() {
let mut dataset: HashSet<String> = HashSet::new();
let generated = joyful(Options {
segments: 300,
..Default::default()
})
.unwrap();
let words = generated.split(DEFAULT_SEPARATOR);
for word in words {
let None = dataset.get(word) else {
panic!("word uniqness broken!");
};
dataset.insert(word.to_string());
}
assert_eq!(dataset.len(), 300)
}
#[test]
fn first_word_is_always_prefix() {
let generated = joyful(Options::default()).unwrap();
let parts: Vec<String> = generated
.split(DEFAULT_SEPARATOR)
.map(|s| s.to_string())
.collect();
let prefix = parts.first().unwrap();
assert!(PREFIXES.contains(prefix));
}
#[test]
fn respects_max_length() {
let options = Options {
segments: 3,
max_length: Some(20),
..Default::default()
};
let result = joyful(options);
assert!(result.is_ok());
let result = result.unwrap();
assert!(result.len() <= 20);
assert_eq!(result.split(DEFAULT_SEPARATOR).count(), 3);
}
#[test]
fn fails_on_impossible_constraints() {
let options = Options {
segments: 10,
max_length: Some(3),
..Default::default()
};
let result = joyful(options);
assert!(matches!(
result,
Err(ValidationError::LengthConstraintImpossible)
))
}
#[test]
fn options_segments_validation_min() {
let options = Options {
segments: 0,
..Default::default()
};
if let Some(e) = options.validate() {
assert!(matches!(e, ValidationError::TooFewSegments))
} else {
panic!("should have returned an error");
}
}
#[test]
fn options_segments_validation_max() {
let options = Options {
segments: 10_000,
..Default::default()
};
if let Some(e) = options.validate() {
assert!(matches!(e, ValidationError::TooManySegments))
} else {
panic!("should have returned an error");
}
}
}