Skip to main content

protest_extras/generators/
text.rs

1//! Text and string generators
2//!
3//! This module provides generators for various text patterns:
4//! - Alphabetic strings (a-z, A-Z)
5//! - Alphanumeric strings (a-z, A-Z, 0-9)
6//! - Programming identifiers (valid Rust/C/etc identifiers)
7//! - Sentences (realistic sentence-like text)
8//! - Paragraphs (multiple sentences)
9//!
10//! All generators use std library only.
11
12use protest::{Generator, GeneratorConfig};
13use rand::Rng;
14
15// ============================================================================
16// Alphabetic Generator
17// ============================================================================
18
19/// Generator for alphabetic strings (only letters a-z, A-Z)
20#[derive(Debug, Clone)]
21pub struct AlphabeticGenerator {
22    min_len: usize,
23    max_len: usize,
24    lowercase_only: bool,
25    uppercase_only: bool,
26}
27
28impl AlphabeticGenerator {
29    /// Create a new alphabetic generator with length bounds
30    pub fn new(min_len: usize, max_len: usize) -> Self {
31        Self {
32            min_len,
33            max_len,
34            lowercase_only: false,
35            uppercase_only: false,
36        }
37    }
38
39    /// Generate only lowercase letters
40    pub fn lowercase(min_len: usize, max_len: usize) -> Self {
41        Self {
42            min_len,
43            max_len,
44            lowercase_only: true,
45            uppercase_only: false,
46        }
47    }
48
49    /// Generate only uppercase letters
50    pub fn uppercase(min_len: usize, max_len: usize) -> Self {
51        Self {
52            min_len,
53            max_len,
54            lowercase_only: false,
55            uppercase_only: true,
56        }
57    }
58
59    fn generate_char(&self, rng: &mut dyn rand::RngCore) -> char {
60        let lowercase = b"abcdefghijklmnopqrstuvwxyz";
61        let uppercase = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ";
62
63        if self.lowercase_only {
64            lowercase[rng.r#gen_range(0..lowercase.len())] as char
65        } else if self.uppercase_only {
66            uppercase[rng.r#gen_range(0..uppercase.len())] as char
67        } else {
68            // Mix of both
69            let all = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
70            all[rng.r#gen_range(0..all.len())] as char
71        }
72    }
73}
74
75impl Generator<String> for AlphabeticGenerator {
76    fn generate(&self, rng: &mut dyn rand::RngCore, _config: &GeneratorConfig) -> String {
77        let len = rng.r#gen_range(self.min_len..=self.max_len);
78        (0..len).map(|_| self.generate_char(rng)).collect()
79    }
80
81    fn shrink(&self, value: &String) -> Box<dyn Iterator<Item = String>> {
82        let mut shrinks = Vec::new();
83
84        // Try empty string
85        if !value.is_empty() && self.min_len == 0 {
86            shrinks.push(String::new());
87        }
88
89        // Try shrinking to min length
90        if value.len() > self.min_len {
91            shrinks.push(value.chars().take(self.min_len).collect());
92        }
93
94        // Try removing one character at a time
95        if value.len() > self.min_len {
96            for i in 0..value.len().min(3) {
97                let mut chars: Vec<char> = value.chars().collect();
98                if !chars.is_empty() {
99                    chars.remove(i);
100                    let shrunk: String = chars.into_iter().collect();
101                    if shrunk.len() >= self.min_len {
102                        shrinks.push(shrunk);
103                    }
104                }
105            }
106        }
107
108        Box::new(shrinks.into_iter())
109    }
110}
111
112// ============================================================================
113// Alphanumeric Generator
114// ============================================================================
115
116/// Generator for alphanumeric strings (letters and digits)
117#[derive(Debug, Clone)]
118pub struct AlphanumericGenerator {
119    min_len: usize,
120    max_len: usize,
121}
122
123impl AlphanumericGenerator {
124    /// Create a new alphanumeric generator with length bounds
125    pub fn new(min_len: usize, max_len: usize) -> Self {
126        Self { min_len, max_len }
127    }
128
129    fn generate_char(&self, rng: &mut dyn rand::RngCore) -> char {
130        let chars = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
131        chars[rng.r#gen_range(0..chars.len())] as char
132    }
133}
134
135impl Generator<String> for AlphanumericGenerator {
136    fn generate(&self, rng: &mut dyn rand::RngCore, _config: &GeneratorConfig) -> String {
137        let len = rng.r#gen_range(self.min_len..=self.max_len);
138        (0..len).map(|_| self.generate_char(rng)).collect()
139    }
140
141    fn shrink(&self, value: &String) -> Box<dyn Iterator<Item = String>> {
142        let mut shrinks = Vec::new();
143
144        if !value.is_empty() && self.min_len == 0 {
145            shrinks.push(String::new());
146        }
147
148        if value.len() > self.min_len {
149            shrinks.push(value.chars().take(self.min_len).collect());
150        }
151
152        if value.len() > self.min_len {
153            for i in 0..value.len().min(3) {
154                let mut chars: Vec<char> = value.chars().collect();
155                if !chars.is_empty() {
156                    chars.remove(i);
157                    let shrunk: String = chars.into_iter().collect();
158                    if shrunk.len() >= self.min_len {
159                        shrinks.push(shrunk);
160                    }
161                }
162            }
163        }
164
165        Box::new(shrinks.into_iter())
166    }
167}
168
169// ============================================================================
170// Identifier Generator
171// ============================================================================
172
173/// Generator for valid programming identifiers (Rust/C style)
174///
175/// Generates strings that:
176/// - Start with a letter or underscore
177/// - Contain only letters, digits, and underscores
178/// - Are valid identifiers in most programming languages
179#[derive(Debug, Clone)]
180pub struct IdentifierGenerator {
181    min_len: usize,
182    max_len: usize,
183}
184
185impl IdentifierGenerator {
186    /// Create a new identifier generator with length bounds
187    pub fn new(min_len: usize, max_len: usize) -> Self {
188        Self { min_len, max_len }
189    }
190
191    fn generate_first_char(&self, rng: &mut dyn rand::RngCore) -> char {
192        // First char must be letter or underscore
193        let chars = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_";
194        chars[rng.r#gen_range(0..chars.len())] as char
195    }
196
197    fn generate_char(&self, rng: &mut dyn rand::RngCore) -> char {
198        // Subsequent chars can be letters, digits, or underscore
199        let chars = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_";
200        chars[rng.r#gen_range(0..chars.len())] as char
201    }
202}
203
204impl Generator<String> for IdentifierGenerator {
205    fn generate(&self, rng: &mut dyn rand::RngCore, _config: &GeneratorConfig) -> String {
206        let len = rng.r#gen_range(self.min_len.max(1)..=self.max_len.max(1));
207        let mut result = String::with_capacity(len);
208
209        // First character
210        result.push(self.generate_first_char(rng));
211
212        // Remaining characters
213        for _ in 1..len {
214            result.push(self.generate_char(rng));
215        }
216
217        result
218    }
219
220    fn shrink(&self, value: &String) -> Box<dyn Iterator<Item = String>> {
221        let mut shrinks = Vec::new();
222
223        // Try simple identifiers
224        if value != "a" && self.min_len <= 1 {
225            shrinks.push("a".to_string());
226        }
227
228        if value != "x" && self.min_len <= 1 {
229            shrinks.push("x".to_string());
230        }
231
232        // Try shrinking to min length
233        if value.len() > self.min_len {
234            shrinks.push(value.chars().take(self.min_len.max(1)).collect());
235        }
236
237        // Try removing characters
238        if value.len() > self.min_len.max(1) {
239            for i in 1..value.len().min(4) {
240                let mut chars: Vec<char> = value.chars().collect();
241                chars.remove(i);
242                let shrunk: String = chars.into_iter().collect();
243                if shrunk.len() >= self.min_len.max(1) {
244                    shrinks.push(shrunk);
245                }
246            }
247        }
248
249        Box::new(shrinks.into_iter())
250    }
251}
252
253// ============================================================================
254// Sentence Generator
255// ============================================================================
256
257/// Generator for sentence-like text
258///
259/// Generates strings that resemble English sentences:
260/// - Start with capital letter
261/// - End with period
262/// - Contain words separated by spaces
263#[derive(Debug, Clone)]
264pub struct SentenceGenerator {
265    min_words: usize,
266    max_words: usize,
267}
268
269impl SentenceGenerator {
270    /// Create a new sentence generator
271    pub fn new(min_words: usize, max_words: usize) -> Self {
272        Self {
273            min_words,
274            max_words,
275        }
276    }
277
278    fn generate_word(&self, rng: &mut dyn rand::RngCore, is_first: bool) -> String {
279        let len = rng.r#gen_range(2..8);
280        let mut word = String::with_capacity(len);
281
282        for i in 0..len {
283            let c = if i == 0 && is_first {
284                // First letter of first word is capital
285                (b'A' + rng.r#gen_range(0..26)) as char
286            } else {
287                // Other letters are lowercase
288                (b'a' + rng.r#gen_range(0..26)) as char
289            };
290            word.push(c);
291        }
292
293        word
294    }
295}
296
297impl Generator<String> for SentenceGenerator {
298    fn generate(&self, rng: &mut dyn rand::RngCore, _config: &GeneratorConfig) -> String {
299        let num_words = rng.r#gen_range(self.min_words.max(1)..=self.max_words.max(1));
300        let mut sentence = String::new();
301
302        for i in 0..num_words {
303            if i > 0 {
304                sentence.push(' ');
305            }
306            sentence.push_str(&self.generate_word(rng, i == 0));
307        }
308
309        sentence.push('.');
310        sentence
311    }
312
313    fn shrink(&self, value: &String) -> Box<dyn Iterator<Item = String>> {
314        let mut shrinks = Vec::new();
315
316        // Try minimal sentence
317        if value != "A." && self.min_words <= 1 {
318            shrinks.push("A.".to_string());
319        }
320
321        // Try removing words
322        let words: Vec<&str> = value.trim_end_matches('.').split_whitespace().collect();
323        if words.len() > self.min_words {
324            // Take first min_words
325            let shrunk_words = &words[..self.min_words.max(1)];
326            let mut shrunk = shrunk_words.join(" ");
327            // Capitalize first letter if needed
328            if let Some(first_char) = shrunk.chars().next()
329                && first_char.is_lowercase()
330            {
331                let capitalized = first_char.to_uppercase().collect::<String>() + &shrunk[1..];
332                shrunk = capitalized;
333            }
334            shrunk.push('.');
335            shrinks.push(shrunk);
336        }
337
338        Box::new(shrinks.into_iter())
339    }
340}
341
342// ============================================================================
343// Paragraph Generator
344// ============================================================================
345
346/// Generator for paragraph-like text
347///
348/// Generates multiple sentences separated by spaces
349#[derive(Debug, Clone)]
350pub struct ParagraphGenerator {
351    min_sentences: usize,
352    max_sentences: usize,
353}
354
355impl ParagraphGenerator {
356    /// Create a new paragraph generator
357    pub fn new(min_sentences: usize, max_sentences: usize) -> Self {
358        Self {
359            min_sentences,
360            max_sentences,
361        }
362    }
363}
364
365impl Generator<String> for ParagraphGenerator {
366    fn generate(&self, rng: &mut dyn rand::RngCore, config: &GeneratorConfig) -> String {
367        let num_sentences = rng.r#gen_range(self.min_sentences.max(1)..=self.max_sentences.max(1));
368        let sentence_gen = SentenceGenerator::new(3, 10);
369
370        let sentences: Vec<String> = (0..num_sentences)
371            .map(|_| sentence_gen.generate(rng, config))
372            .collect();
373
374        sentences.join(" ")
375    }
376
377    fn shrink(&self, value: &String) -> Box<dyn Iterator<Item = String>> {
378        let mut shrinks = Vec::new();
379
380        // Try minimal paragraph
381        if value != "A." && self.min_sentences <= 1 {
382            shrinks.push("A.".to_string());
383        }
384
385        // Try removing sentences
386        let sentences: Vec<&str> = value.split(". ").collect();
387        if sentences.len() > self.min_sentences {
388            let shrunk_sentences = &sentences[..self.min_sentences.max(1)];
389            let mut shrunk = shrunk_sentences.join(". ");
390            if !shrunk.ends_with('.') {
391                shrunk.push('.');
392            }
393            shrinks.push(shrunk);
394        }
395
396        Box::new(shrinks.into_iter())
397    }
398}
399
400#[cfg(test)]
401mod tests {
402    use super::*;
403    use rand::thread_rng;
404
405    #[test]
406    fn test_alphabetic_generator() {
407        let generator = AlphabeticGenerator::new(5, 10);
408        let mut rng = thread_rng();
409        let config = GeneratorConfig::default();
410
411        for _ in 0..10 {
412            let text = generator.generate(&mut rng, &config);
413            assert!(text.len() >= 5 && text.len() <= 10);
414            assert!(text.chars().all(|c| c.is_alphabetic()));
415        }
416    }
417
418    #[test]
419    fn test_alphabetic_lowercase_only() {
420        let generator = AlphabeticGenerator::lowercase(5, 10);
421        let mut rng = thread_rng();
422        let config = GeneratorConfig::default();
423
424        for _ in 0..10 {
425            let text = generator.generate(&mut rng, &config);
426            assert!(text.chars().all(|c| c.is_lowercase()));
427        }
428    }
429
430    #[test]
431    fn test_alphanumeric_generator() {
432        let generator = AlphanumericGenerator::new(5, 10);
433        let mut rng = thread_rng();
434        let config = GeneratorConfig::default();
435
436        for _ in 0..10 {
437            let text = generator.generate(&mut rng, &config);
438            assert!(text.len() >= 5 && text.len() <= 10);
439            assert!(text.chars().all(|c| c.is_alphanumeric()));
440        }
441    }
442
443    #[test]
444    fn test_identifier_generator() {
445        let generator = IdentifierGenerator::new(3, 15);
446        let mut rng = thread_rng();
447        let config = GeneratorConfig::default();
448
449        for _ in 0..10 {
450            let id = generator.generate(&mut rng, &config);
451            assert!(id.len() >= 3 && id.len() <= 15);
452
453            // First char must be letter or underscore
454            let first = id.chars().next().unwrap();
455            assert!(first.is_alphabetic() || first == '_');
456
457            // All chars must be alphanumeric or underscore
458            assert!(id.chars().all(|c| c.is_alphanumeric() || c == '_'));
459        }
460    }
461
462    #[test]
463    fn test_sentence_generator() {
464        let generator = SentenceGenerator::new(3, 8);
465        let mut rng = thread_rng();
466        let config = GeneratorConfig::default();
467
468        for _ in 0..10 {
469            let sentence = generator.generate(&mut rng, &config);
470
471            // Should end with period
472            assert!(sentence.ends_with('.'));
473
474            // Should start with capital letter
475            assert!(sentence.chars().next().unwrap().is_uppercase());
476
477            // Should have words
478            let words: Vec<&str> = sentence.trim_end_matches('.').split_whitespace().collect();
479            assert!(words.len() >= 3 && words.len() <= 8);
480        }
481    }
482
483    #[test]
484    fn test_paragraph_generator() {
485        let generator = ParagraphGenerator::new(2, 5);
486        let mut rng = thread_rng();
487        let config = GeneratorConfig::default();
488
489        for _ in 0..5 {
490            let paragraph = generator.generate(&mut rng, &config);
491
492            // Should contain multiple periods
493            let period_count = paragraph.matches('.').count();
494            assert!((2..=5).contains(&period_count));
495        }
496    }
497}