1struct SplitMix64 {
14 state: u64,
15}
16
17impl SplitMix64 {
18 fn new(seed: u64) -> Self {
19 Self { state: seed }
20 }
21
22 fn next_u64(&mut self) -> u64 {
23 self.state = self.state.wrapping_add(0x9E37_79B9_7F4A_7C15);
24 let mut z = self.state;
25 z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
26 z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
27 z ^ (z >> 31)
28 }
29
30 fn below(&mut self, n: u64) -> u64 {
31 self.next_u64() % n
32 }
33
34 fn pick<'a>(&mut self, items: &[&'a str]) -> &'a str {
35 items[usize::try_from(self.below(items.len() as u64)).unwrap_or(0)]
36 }
37}
38
39fn fnv1a(bytes: &[u8]) -> u64 {
41 let mut hash = 0xcbf2_9ce4_8422_2325_u64;
42 for &b in bytes {
43 hash ^= u64::from(b);
44 hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
45 }
46 hash
47}
48
49const FIRST_NAMES: &[&str] = &[
50 "Alice", "Bob", "Carol", "David", "Emma", "Finn", "Sofie", "Lars", "Noor", "Sem",
51];
52const LAST_NAMES: &[&str] = &[
53 "Jansen", "Vries", "Dekker", "Visser", "Smit", "Meijer", "Smith", "Brown", "Jones", "Mulder",
54];
55const CITIES: &[&str] = &[
56 "Amsterdam",
57 "Rotterdam",
58 "Utrecht",
59 "Den Haag",
60 "Eindhoven",
61 "Groningen",
62];
63const STREETS: &[&str] = &[
64 "Kerkstraat",
65 "Schoolstraat",
66 "Dorpsstraat",
67 "Molenweg",
68 "Hoofdstraat",
69 "Stationsweg",
70];
71const WORDS: &[&str] = &[
72 "lorem",
73 "ipsum",
74 "dolor",
75 "sit",
76 "amet",
77 "consectetur",
78 "adipiscing",
79 "elit",
80];
81
82pub const GENERATORS: &[&str] = &[
85 "firstName",
86 "lastName",
87 "name",
88 "fullName",
89 "email",
90 "username",
91 "phoneNL",
92 "postCode",
93 "city",
94 "street",
95 "int",
96 "number",
97 "digits4",
98 "digits8",
99 "bool",
100 "word",
101 "uuid",
102];
103
104pub fn is_known_generator(name: &str) -> bool {
106 GENERATORS.contains(&name)
107}
108
109pub fn generate(run_id: &str, occurrence: usize, generator: &str) -> Option<String> {
112 let seed = fnv1a(run_id.as_bytes())
113 ^ fnv1a(generator.as_bytes()).rotate_left(17)
114 ^ (occurrence as u64).wrapping_mul(0x9E37_79B9_7F4A_7C15);
115 let mut rng = SplitMix64::new(seed);
116 let value = match generator {
117 "firstName" => rng.pick(FIRST_NAMES).to_owned(),
118 "lastName" => rng.pick(LAST_NAMES).to_owned(),
119 "name" | "fullName" => format!("{} {}", rng.pick(FIRST_NAMES), rng.pick(LAST_NAMES)),
120 "email" => format!(
121 "{}.{}@example.com",
122 rng.pick(FIRST_NAMES).to_lowercase(),
123 rng.pick(LAST_NAMES).to_lowercase()
124 ),
125 "username" => format!(
126 "{}{}",
127 rng.pick(FIRST_NAMES).to_lowercase(),
128 rng.below(1000)
129 ),
130 "phoneNL" => format!("06{:08}", rng.below(100_000_000)),
131 "postCode" => format!(
132 "{:04} {}{}",
133 1000 + rng.below(9000),
134 upper_letter(&mut rng),
135 upper_letter(&mut rng)
136 ),
137 "city" => rng.pick(CITIES).to_owned(),
138 "street" => format!("{} {}", rng.pick(STREETS), 1 + rng.below(200)),
139 "int" | "number" => rng.below(10_000).to_string(),
140 "digits4" => format!("{:04}", rng.below(10_000)),
141 "digits8" => format!("{:08}", rng.below(100_000_000)),
142 "bool" => (rng.next_u64() & 1 == 1).to_string(),
143 "word" => rng.pick(WORDS).to_owned(),
144 "uuid" => {
145 let (a, b) = (rng.next_u64(), rng.next_u64());
146 format!(
147 "{:08x}-{:04x}-4{:03x}-{:04x}-{:012x}",
148 a >> 32,
149 (a >> 16) & 0xFFFF,
150 a & 0xFFF,
151 0x8000 | ((b >> 48) & 0x3FFF),
152 b & 0xFFFF_FFFF_FFFF
153 )
154 }
155 _ => return None,
156 };
157 Some(value)
158}
159
160fn upper_letter(rng: &mut SplitMix64) -> char {
161 char::from(b'A' + u8::try_from(rng.below(26)).unwrap_or(0))
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167
168 #[test]
169 fn every_listed_generator_generates() {
170 for generator in GENERATORS {
171 assert!(
172 generate("run-1", 0, generator).is_some(),
173 "generator `{generator}` listed but not implemented"
174 );
175 }
176 assert!(generate("run-1", 0, "nope").is_none());
177 }
178
179 #[test]
180 fn deterministic_per_run_id_and_occurrence() {
181 let a = generate("run-1", 0, "lastName");
182 let b = generate("run-1", 0, "lastName");
183 assert_eq!(a, b, "same run id + occurrence → same value");
184 let c = generate("run-2", 0, "lastName");
185 let d = generate("run-1", 1, "lastName");
186 assert!(a != c || a != d, "distinct run/occurrence should vary");
188 }
189
190 #[test]
191 fn uuid_shape_is_v4ish() {
192 let uuid = generate("run-1", 0, "uuid").unwrap_or_default();
193 assert_eq!(uuid.len(), 36, "{uuid}");
194 assert_eq!(uuid.as_bytes()[14], b'4', "{uuid}");
195 }
196}