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}