goofy_animals/lib.rs
1#![allow(clippy::needless_doctest_main)]
2#![doc = include_str!("../README.md")]
3#![cfg_attr(docsrs, feature(doc_cfg))]
4#![cfg_attr(not(feature = "std"), no_std)]
5
6#[cfg(feature = "alloc")]
7extern crate alloc;
8
9use core::fmt::{Debug, Formatter};
10
11use rand::Rng;
12
13/// A default instance of `GoofyAnimals` initialized with the built-in English word lists.
14///
15/// This constant provides convenient access to a pre-configured `GoofyAnimals` instance
16/// that uses the included animal and adjective lists.
17pub const DEFAULT_GOOFY_ANIMALS: GoofyAnimals<'static> = GoofyAnimals::new(
18 &const_str::split!(include_str!("data/en_animals.txt"), "\n"),
19 &const_str::split!(include_str!("data/en_adjectives.txt"), "\n"),
20);
21
22/// A struct that manages lists of adjectives and animals for generating goofy names.
23///
24/// `GoofyAnimals` allows you to generate random names in the format
25/// `adjective-adjective-animal` using custom word lists or the default ones.
26pub struct GoofyAnimals<'a> {
27 animals: &'a [&'a str],
28 adjectives: &'a [&'a str],
29}
30
31impl<'a> GoofyAnimals<'a> {
32 /// Creates a new `GoofyAnimals` instance with the given animal and adjective lists.
33 ///
34 /// This constructor performs several checks at compile time to ensure the
35 /// provided lists are valid:
36 /// - Verifies that the animals list is not empty
37 /// - Ensures there are at least two adjectives
38 /// - Checks that there are no trailing newlines in either list
39 ///
40 /// # Arguments
41 ///
42 /// * `animals` - A slice of string slices containing animal names
43 /// * `adjectives` - A slice of string slices containing adjectives
44 ///
45 /// # Returns
46 ///
47 /// A new `GoofyAnimals` instance.
48 ///
49 /// # Panics
50 ///
51 /// This function will panic at compile time if:
52 /// - The animals list is empty
53 /// - The adjectives list has fewer than 2 entries
54 /// - Either list has trailing newlines
55 pub const fn new(animals: &'a [&'a str], adjectives: &'a [&'a str]) -> Self {
56 let total_animals = animals.len();
57 let total_adjectives = adjectives.len();
58
59 if total_animals < 1 {
60 panic!("empty animals");
61 }
62
63 if total_adjectives < 2 {
64 panic!("must have at least two adjectives");
65 }
66
67 if const_str::equal!(*animals.last().unwrap(), "") {
68 panic!("trailing newline in animals");
69 }
70
71 if const_str::equal!(*adjectives.last().unwrap(), "") {
72 panic!("trailing newline in adjectives");
73 }
74
75 Self::new_unchecked(animals, adjectives)
76 }
77
78 /// Creates a new `GoofyAnimals` instance without performing any validity checks.
79 ///
80 /// This constructor is useful when you're certain that your word lists are valid
81 /// or when you want to defer validation to runtime.
82 ///
83 /// # Arguments
84 ///
85 /// * `animals` - A slice of string slices containing animal names
86 /// * `adjectives` - A slice of string slices containing adjectives
87 ///
88 /// # Returns
89 ///
90 /// A new `GoofyAnimals` instance.
91 ///
92 /// # Safety
93 ///
94 /// This function does not check if:
95 /// - The animals list is empty
96 /// - The adjectives list has at least 2 entries
97 /// - Either list has trailing newlines
98 ///
99 /// Using invalid inputs may result in panics or unexpected behavior when
100 /// generating names.
101 pub const fn new_unchecked(animals: &'a [&'a str], adjectives: &'a [&'a str]) -> Self {
102 Self {
103 animals,
104 adjectives,
105 }
106 }
107
108 /// Returns a reference to the list of animal names.
109 ///
110 /// This can be useful for inspecting or using the animal names directly.
111 ///
112 /// # Returns
113 ///
114 /// A slice of string slices containing the animal names.
115 pub fn get_animals(&self) -> &'a [&'a str] {
116 self.animals
117 }
118
119 /// Returns a reference to the list of adjectives.
120 ///
121 /// This can be useful for inspecting or using the adjectives directly.
122 ///
123 /// # Returns
124 ///
125 /// A slice of string slices containing the adjectives.
126 pub fn get_adjectives(&self) -> &'a [&'a str] {
127 self.adjectives
128 }
129
130 /// Generates the individual parts of a goofy name: two adjectives and an animal.
131 ///
132 /// This function selects two different adjectives and one animal randomly using the
133 /// provided random number generator. It ensures the two adjectives are not the same.
134 ///
135 /// # Arguments
136 ///
137 /// * `rng` - A mutable reference to any random number generator that implements the `Rng` trait.
138 ///
139 /// # Returns
140 ///
141 /// A tuple containing three string slices: `(adjective1, adjective2, animal)`.
142 ///
143 /// # Examples
144 ///
145 /// ```rust
146 /// use rand::SeedableRng;
147 /// use rand_chacha::ChaCha20Rng;
148 /// use goofy_animals::DEFAULT_GOOFY_ANIMALS;
149 ///
150 /// // Use a seeded RNG for deterministic output
151 /// let mut rng = ChaCha20Rng::seed_from_u64(0x1337);
152 /// let (adj1, adj2, animal) = DEFAULT_GOOFY_ANIMALS.generate_name_parts(&mut rng);
153 /// assert_eq!(adj1, "dismal");
154 /// assert_eq!(adj2, "outlying");
155 /// assert_eq!(animal, "moth");
156 /// ```
157 #[cfg_attr(feature = "tracing", tracing::instrument(skip(rng), level = tracing::Level::TRACE))]
158 pub fn generate_name_parts(&self, rng: &mut impl Rng) -> (&'a str, &'a str, &'a str) {
159 let (adjective_one, adjective_two) = loop {
160 let one = rng.random_range(0..self.adjectives.len());
161 let two = rng.random_range(0..self.adjectives.len());
162
163 if one == two {
164 continue;
165 }
166
167 break (one, two);
168 };
169
170 let animal = rng.random_range(0..self.animals.len());
171
172 #[cfg(feature = "tracing")]
173 tracing::trace!(adjective_one, adjective_two, animal, "generated name");
174
175 (
176 self.adjectives[adjective_one],
177 self.adjectives[adjective_two],
178 self.animals[animal],
179 )
180 }
181
182 /// Generates a complete goofy name as a string in the format `adjective-adjective-animal`.
183 ///
184 /// This function combines two randomly selected adjectives with a randomly selected animal name,
185 /// joining them with hyphens to form a single string.
186 ///
187 /// # Arguments
188 ///
189 /// * `rng` - A mutable reference to any random number generator that implements the `Rng` trait.
190 ///
191 /// # Returns
192 ///
193 /// A `String` containing the generated name in the format `adjective-adjective-animal`.
194 ///
195 /// # Examples
196 ///
197 /// ```rust
198 /// use rand::SeedableRng;
199 /// use rand_chacha::ChaCha20Rng;
200 /// use goofy_animals::DEFAULT_GOOFY_ANIMALS;
201 ///
202 /// // Use a seeded RNG for deterministic output
203 /// let mut rng = ChaCha20Rng::seed_from_u64(0x1337);
204 /// let name = DEFAULT_GOOFY_ANIMALS.generate_name(&mut rng);
205 /// assert_eq!(name, "dismal-outlying-moth");
206 /// ```
207 ///
208 /// # Feature Flag
209 ///
210 /// This function is only available when the `alloc` feature is enabled.
211 #[inline]
212 #[cfg(feature = "alloc")]
213 #[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
214 pub fn generate_name(&self, rng: &mut impl Rng) -> ::alloc::string::String {
215 let (adjective_one, adjective_two, animal) = self.generate_name_parts(rng);
216
217 ::alloc::format!("{adjective_one}-{adjective_two}-{animal}")
218 }
219}
220
221impl Debug for GoofyAnimals<'_> {
222 fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
223 f.debug_struct("GoofyAnimals")
224 .field("total_adjectives", &self.adjectives.len())
225 .field("total_animals", &self.animals.len())
226 .finish()
227 }
228}
229
230/// Generates the individual parts of a goofy name using the default word lists.
231///
232/// This is a convenience function that calls `generate_name_parts` on the
233/// `DEFAULT_GOOFY_ANIMALS` instance.
234///
235/// # Arguments
236///
237/// * `rng` - A mutable reference to any random number generator that implements the `Rng` trait.
238///
239/// # Returns
240///
241/// A tuple containing three string slices: `(adjective1, adjective2, animal)`.
242///
243/// # Examples
244///
245/// ```rust
246/// use rand::SeedableRng;
247/// use rand_chacha::ChaCha20Rng;
248/// use goofy_animals::generate_name_parts;
249///
250/// // Use a seeded RNG for deterministic output
251/// let mut rng = ChaCha20Rng::seed_from_u64(0x1337);
252/// let (adj1, adj2, animal) = generate_name_parts(&mut rng);
253/// assert_eq!(adj1, "dismal");
254/// assert_eq!(adj2, "outlying");
255/// assert_eq!(animal, "moth");
256/// ```
257///
258/// See [`GoofyAnimals::generate_name_parts`] for more details.
259#[inline]
260pub fn generate_name_parts(rng: &mut impl Rng) -> (&'static str, &'static str, &'static str) {
261 DEFAULT_GOOFY_ANIMALS.generate_name_parts(rng)
262}
263
264/// Generates a complete goofy name as a string using the default word lists.
265///
266/// This is a convenience function that calls `generate_name` on the
267/// `DEFAULT_GOOFY_ANIMALS` instance.
268///
269/// # Arguments
270///
271/// * `rng` - A mutable reference to any random number generator that implements the `Rng` trait.
272///
273/// # Returns
274///
275/// A `String` containing the generated name in the format `adjective-adjective-animal`.
276///
277/// # Examples
278///
279/// ```rust
280/// use rand::SeedableRng;
281/// use rand_chacha::ChaCha20Rng;
282/// use goofy_animals::generate_name;
283///
284/// // Use a seeded RNG for deterministic output
285/// let mut rng = ChaCha20Rng::seed_from_u64(0x1337);
286/// let name = generate_name(&mut rng);
287/// assert_eq!(name, "dismal-outlying-moth");
288/// ```
289///
290/// # Feature Flag
291///
292/// This function is only available when the `alloc` feature is enabled.
293///
294/// See [`GoofyAnimals::generate_name`] for more details.
295#[inline]
296#[cfg(feature = "alloc")]
297#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
298pub fn generate_name(rng: &mut impl Rng) -> ::alloc::string::String {
299 DEFAULT_GOOFY_ANIMALS.generate_name(rng)
300}
301
302#[cfg(test)]
303mod test {
304 use super::DEFAULT_GOOFY_ANIMALS;
305
306 use pretty_assertions::assert_eq;
307
308 #[test]
309 fn animals() {
310 assert_eq!(DEFAULT_GOOFY_ANIMALS.get_animals().len(), 355);
311 }
312
313 #[test]
314 fn adjectives() {
315 assert_eq!(DEFAULT_GOOFY_ANIMALS.get_adjectives().len(), 1300);
316 }
317
318 #[test]
319 #[cfg_attr(feature = "tracing", tracing_test::traced_test)]
320 fn name_generation() {
321 use rand::SeedableRng;
322 use rand_chacha::ChaCha20Rng;
323
324 let mut rng = ChaCha20Rng::seed_from_u64(0x1337);
325
326 assert_eq!(
327 DEFAULT_GOOFY_ANIMALS.generate_name_parts(&mut rng),
328 ("dismal", "outlying", "moth"),
329 );
330 assert_eq!(
331 DEFAULT_GOOFY_ANIMALS.generate_name_parts(&mut rng),
332 ("healthy", "yellowish", "firefly"),
333 );
334 assert_eq!(
335 DEFAULT_GOOFY_ANIMALS.generate_name_parts(&mut rng),
336 ("flat", "faint", "squirrel"),
337 );
338 assert_eq!(
339 DEFAULT_GOOFY_ANIMALS.generate_name_parts(&mut rng),
340 ("glorious", "educated", "louse"),
341 );
342 assert_eq!(
343 DEFAULT_GOOFY_ANIMALS.generate_name_parts(&mut rng),
344 ("big", "glittering", "perch"),
345 );
346 assert_eq!(
347 DEFAULT_GOOFY_ANIMALS.generate_name_parts(&mut rng),
348 ("relieved", "shadowy", "booby"),
349 );
350 assert_eq!(
351 DEFAULT_GOOFY_ANIMALS.generate_name_parts(&mut rng),
352 ("simplistic", "thankful", "panther"),
353 );
354 assert_eq!(
355 DEFAULT_GOOFY_ANIMALS.generate_name_parts(&mut rng),
356 ("black", "serene", "marten"),
357 );
358
359 #[cfg(all(feature = "tracing", feature = "alloc"))]
360 {
361 logs_assert(|lines: &[&str]| {
362 const EXPECTED: usize = 8;
363
364 match lines
365 .iter()
366 .filter(|line| line.contains("generated name"))
367 .count()
368 {
369 EXPECTED => Ok(()),
370 n => Err(::alloc::format!("Expected {EXPECTED} logs, but got {n}")),
371 }
372 });
373 }
374 }
375
376 #[test]
377 #[cfg(feature = "alloc")]
378 fn name_generation_alloc() {
379 use rand::SeedableRng;
380 use rand_chacha::ChaCha20Rng;
381
382 let mut rng = ChaCha20Rng::seed_from_u64(0x1337);
383
384 assert_eq!(
385 DEFAULT_GOOFY_ANIMALS.generate_name(&mut rng),
386 "dismal-outlying-moth",
387 );
388 }
389}