crazy_train/
generator.rs

1//! This module provides functionality for generating random strings based on specified criteria.
2//!
3//! The [`StringDef`] struct defines the configuration for string generation, including options
4//! for length, character types, and more. The [`StringDefBuilder`] allows for a convenient way
5//! to build and customize a [`StringDef`] instance. The module also includes various utility
6//! functions to check for specific character types in a string.
7
8use std::cell::RefCell;
9
10use rand::prelude::IteratorRandom;
11use rand::{Rng, RngCore};
12
13use crate::Randomizer;
14
15const SYMBOLS: &str = r##"!\"#$%&'()*+,-./:;<=>?@[\]^_`{|}~"##;
16
17/// Defines the criteria for generating random strings.
18#[derive(Clone)]
19#[allow(clippy::struct_excessive_bools)]
20pub struct StringDef {
21    /// The desired length of the generated string.
22    pub length: u32,
23    /// Whether to include Unicode characters in the generated string.
24    pub include_unicode: bool,
25    /// Whether to include symbols in the generated string.
26    pub include_symbol: bool,
27    /// Whether to include capital letters in the generated string.
28    pub include_capital_letters: bool,
29    /// Whether to include numeric characters in the generated string.
30    pub include_numbers: bool,
31}
32
33/// Provides a builder for constructing a [`StringDef`] instance.
34impl Default for StringDef {
35    fn default() -> Self {
36        Self {
37            length: 6,
38            include_unicode: false,
39            include_symbol: false,
40            include_capital_letters: false,
41            include_numbers: false,
42        }
43    }
44}
45
46/// A builder for configuring a [`StringDef`].
47pub struct StringDefBuilder<'a> {
48    pub string_def: StringDef,
49    pub rng: &'a RefCell<dyn RngCore + Send>,
50}
51
52impl StringDefBuilder<'_> {
53    /// Sets the length of the generated string.
54    #[must_use]
55    pub const fn length(mut self, length: u32) -> Self {
56        self.string_def.length = length;
57        self
58    }
59
60    /// Specifies whether to include Unicode characters.
61    #[must_use]
62    pub const fn include_unicode(mut self, yes: bool) -> Self {
63        self.string_def.include_unicode = yes;
64        self
65    }
66
67    /// Specifies whether to include symbols.
68    #[must_use]
69    pub const fn include_symbol(mut self, yes: bool) -> Self {
70        self.string_def.include_symbol = yes;
71        self
72    }
73
74    /// Specifies whether to include capital letters.
75    #[must_use]
76    pub const fn include_capital_letters(mut self, yes: bool) -> Self {
77        self.string_def.include_capital_letters = yes;
78        self
79    }
80
81    /// Specifies whether to include numbers.
82    #[must_use]
83    pub const fn include_numbers(mut self, yes: bool) -> Self {
84        self.string_def.include_numbers = yes;
85        self
86    }
87}
88
89impl std::fmt::Display for StringDefBuilder<'_> {
90    /// Displays the generated string based on the current configuration of the builder.
91    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
92        let mut rng = self.rng.borrow_mut();
93        let result = self.string_def.generate(&mut *rng);
94        write!(f, "{result}")
95    }
96}
97
98impl StringDef {
99    /// Creates a [`StringDef`] from a given [`Randomizer`].
100    pub fn from_randomizer(randomizer: &Randomizer) -> Self {
101        Self {
102            length: randomizer.number_between(1, 50),
103            include_unicode: randomizer.bool(),
104            include_symbol: randomizer.bool(),
105            include_capital_letters: randomizer.bool(),
106            include_numbers: randomizer.bool(),
107        }
108    }
109
110    /// Generates a random string based on the current configuration.
111    ///
112    /// # Example
113    ///
114    /// ```rust
115    /// use crazy_train::{Randomizer, StringDef};
116    /// let string_def = StringDef::default();
117    /// let randomizer = Randomizer::with_seed(42);
118    /// let mut rng = randomizer.rng.borrow_mut();
119    /// assert_eq!(string_def.generate(&mut *rng), "noqkak");
120    /// assert_eq!(string_def.generate(&mut *rng), "twdayn");
121    /// assert_eq!(string_def.generate(&mut *rng), "kdnfan");
122    /// ```
123    pub fn generate(&self, rng: &mut dyn RngCore) -> String {
124        let mut result = String::new();
125        let length: usize = self.length as usize;
126
127        while result.len() < length {
128            let choice: u8 = rng.gen_range(0..100);
129
130            if self.include_unicode && choice < 20 {
131                if let Some(unicode_char) = std::char::from_u32(rng.gen_range(0x1F600..0x1F64F)) {
132                    result.push(unicode_char);
133                } else {
134                    result.push('?');
135                }
136            } else if self.include_symbol && choice < 40 {
137                if let Some(symbol) = SYMBOLS.chars().choose(rng) {
138                    result.push(symbol);
139                } else {
140                    result.push('#');
141                }
142            } else if self.include_capital_letters && choice < 60 {
143                let capital_letter = rng.gen_range(b'A'..=b'Z') as char;
144                result.push(capital_letter);
145            } else if self.include_numbers && choice < 80 {
146                let number = rng.gen_range(b'0'..=b'9') as char;
147                result.push(number);
148            } else {
149                let lowercase_letter = rng.gen_range(b'a'..=b'z') as char;
150                result.push(lowercase_letter);
151            }
152        }
153
154        result
155    }
156
157    /// Checks if a given string contains only lowercase letters.
158    ///
159    /// # Example
160    ///
161    /// ```rust
162    /// use crazy_train::StringDef;
163    /// assert!(StringDef::contains_only_lowercase("test"));
164    /// assert!(!StringDef::contains_only_lowercase("1test"));
165    /// assert!(!StringDef::contains_only_lowercase("🙆test"));
166    /// assert!(!StringDef::contains_only_lowercase("#test"));
167    /// assert!(!StringDef::contains_only_lowercase("Test"));
168    /// ```
169    #[must_use]
170    pub fn contains_only_lowercase(s: &str) -> bool {
171        !Self::contains_capital_letters(s)
172            && !Self::contains_numbers(s)
173            && !Self::contains_symbols(s)
174            && !Self::contains_unicode(s)
175    }
176
177    /// Checks if a given string contains any Unicode characters.
178    ///
179    /// # Example
180    ///
181    /// ```rust
182    /// use crazy_train::StringDef;
183    /// assert!(!StringDef::contains_unicode("test"));
184    /// assert!(StringDef::contains_unicode("🙆Test"));
185    /// ```
186    #[must_use]
187    pub fn contains_unicode(s: &str) -> bool {
188        s.chars().any(|ch| !ch.is_ascii())
189    }
190
191    /// Checks if a given string contains any symbols.
192    ///
193    /// # Example
194    ///
195    /// ```rust
196    /// use crazy_train::StringDef;
197    /// assert!(!StringDef::contains_symbols("test"));
198    /// assert!(StringDef::contains_symbols("#Test"));
199    /// ```
200    #[must_use]
201    pub fn contains_symbols(s: &str) -> bool {
202        s.chars().any(|ch| SYMBOLS.contains(ch))
203    }
204
205    /// Checks if a given string contains any numeric characters.
206    ///
207    /// # Example
208    ///
209    /// ```rust
210    /// use crazy_train::StringDef;
211    /// assert!(!StringDef::contains_numbers("test"));
212    /// assert!(StringDef::contains_numbers("1Test"));
213    /// ```
214    #[must_use]
215    pub fn contains_numbers(s: &str) -> bool {
216        s.chars().any(char::is_numeric)
217    }
218
219    /// Checks if a given string contains any capital letters.
220    /// # Example
221    ///
222    /// ```rust
223    /// use crazy_train::StringDef;
224    /// assert!(!StringDef::contains_capital_letters("test"));
225    /// assert!(StringDef::contains_capital_letters("Test"));
226    /// ```
227    #[must_use]
228    pub fn contains_capital_letters(s: &str) -> bool {
229        s.chars().any(char::is_uppercase)
230    }
231}
232
233#[cfg(test)]
234mod tests {
235
236    use super::*;
237    use rand::{rngs::StdRng, SeedableRng};
238
239    #[test]
240    fn has_unicode() {
241        assert!(!StringDef::contains_unicode("test"));
242        assert!(StringDef::contains_unicode("🙆test"));
243    }
244
245    #[test]
246    fn has_symbols() {
247        assert!(!StringDef::contains_symbols("test"));
248        assert!(StringDef::contains_symbols("test#"));
249    }
250
251    #[test]
252    fn has_numbers() {
253        assert!(!StringDef::contains_numbers("test"));
254        assert!(StringDef::contains_numbers("test1"));
255    }
256
257    #[test]
258    fn has_capital_letters() {
259        assert!(!StringDef::contains_capital_letters("test"));
260        assert!(StringDef::contains_capital_letters("Test1"));
261    }
262
263    #[test]
264    fn string_def_default() {
265        let string_def = StringDef::default();
266        let randomizer = Randomizer::with_seed(42);
267        let mut rng = randomizer.rng.borrow_mut();
268        assert_eq!(string_def.generate(&mut *rng), "noqkak");
269        assert_eq!(string_def.generate(&mut *rng), "twdayn");
270        assert_eq!(string_def.generate(&mut *rng), "kdnfan");
271    }
272
273    #[test]
274    fn string_def_with_length() {
275        let string_def = StringDef {
276            length: 10,
277            include_unicode: false,
278            include_symbol: false,
279            include_capital_letters: false,
280            include_numbers: false,
281        };
282        let mut rand = Box::new(StdRng::seed_from_u64(42));
283        assert_eq!(string_def.generate(&mut rand), "noqkaktwda");
284        assert_eq!(string_def.generate(&mut rand), "ynkdnfanbq");
285        assert_eq!(string_def.generate(&mut rand), "vmnbjlufkr");
286    }
287
288    #[test]
289    fn string_def_include_unicode() {
290        let string_def = StringDef {
291            length: 6,
292            include_unicode: true,
293            include_symbol: false,
294            include_capital_letters: false,
295            include_numbers: false,
296        };
297        let mut rand = Box::new(StdRng::seed_from_u64(42));
298        assert_eq!(string_def.generate(&mut rand), "😩oq");
299        assert_eq!(string_def.generate(&mut rand), "kakt🙃");
300        assert_eq!(string_def.generate(&mut rand), "daynkd");
301    }
302
303    #[test]
304    fn string_def_include_symbol() {
305        let string_def = StringDef {
306            length: 6,
307            include_unicode: false,
308            include_symbol: true,
309            include_capital_letters: false,
310            include_numbers: false,
311        };
312        let mut rand = Box::new(StdRng::seed_from_u64(42));
313        assert_eq!(string_def.generate(&mut rand), "\"eq)a)");
314        assert_eq!(string_def.generate(&mut rand), "=wqf`g");
315        assert_eq!(string_def.generate(&mut rand), "/uzw=d");
316    }
317
318    #[test]
319    fn string_def_include_capital_letters() {
320        let string_def = StringDef {
321            length: 6,
322            include_unicode: false,
323            include_symbol: false,
324            include_capital_letters: true,
325            include_numbers: false,
326        };
327        let mut rand = Box::new(StdRng::seed_from_u64(42));
328        assert_eq!(string_def.generate(&mut rand), "NOqkak");
329        assert_eq!(string_def.generate(&mut rand), "TWdAyN");
330        assert_eq!(string_def.generate(&mut rand), "kdnfaN");
331    }
332
333    #[test]
334    fn string_def_include_numbers() {
335        let string_def = StringDef {
336            length: 6,
337            include_unicode: false,
338            include_symbol: false,
339            include_capital_letters: false,
340            include_numbers: true,
341        };
342        let mut rand = Box::new(StdRng::seed_from_u64(42));
343        assert_eq!(string_def.generate(&mut rand), "55qka4");
344        assert_eq!(string_def.generate(&mut rand), "7810y5");
345        assert_eq!(string_def.generate(&mut rand), "k1nf05");
346    }
347}