1use rand::seq::IndexedRandom;
7use rand::RngCore;
8use std::sync::Arc;
9
10use super::loader::{MergeStrategy, TemplateData, TemplateLoader};
11use super::names::NameCulture;
12use crate::models::BusinessProcess;
13
14pub trait TemplateProvider: Send + Sync {
22 fn get_person_first_name(
24 &self,
25 culture: NameCulture,
26 is_male: bool,
27 rng: &mut dyn RngCore,
28 ) -> String;
29
30 fn get_person_last_name(&self, culture: NameCulture, rng: &mut dyn RngCore) -> String;
32
33 fn get_vendor_name(&self, category: &str, rng: &mut dyn RngCore) -> String;
35
36 fn get_customer_name(&self, industry: &str, rng: &mut dyn RngCore) -> String;
38
39 fn get_material_description(&self, material_type: &str, rng: &mut dyn RngCore) -> String;
41
42 fn get_asset_description(&self, category: &str, rng: &mut dyn RngCore) -> String;
44
45 fn get_line_text(
47 &self,
48 process: BusinessProcess,
49 account_type: &str,
50 rng: &mut dyn RngCore,
51 ) -> String;
52
53 fn get_header_template(&self, process: BusinessProcess, rng: &mut dyn RngCore) -> String;
55}
56
57pub struct DefaultTemplateProvider {
59 template_data: Option<TemplateData>,
61 merge_strategy: MergeStrategy,
63}
64
65impl DefaultTemplateProvider {
66 pub fn new() -> Self {
68 Self {
69 template_data: None,
70 merge_strategy: MergeStrategy::Extend,
71 }
72 }
73
74 pub fn with_templates(template_data: TemplateData, strategy: MergeStrategy) -> Self {
76 Self {
77 template_data: Some(template_data),
78 merge_strategy: strategy,
79 }
80 }
81
82 pub fn from_file(path: &std::path::Path) -> Result<Self, super::loader::TemplateError> {
84 let data = TemplateLoader::load_from_file(path)?;
85 Ok(Self::with_templates(data, MergeStrategy::Extend))
86 }
87
88 pub fn from_directory(path: &std::path::Path) -> Result<Self, super::loader::TemplateError> {
90 let data = TemplateLoader::load_from_directory(path)?;
91 Ok(Self::with_templates(data, MergeStrategy::Extend))
92 }
93
94 pub fn with_merge_strategy(mut self, strategy: MergeStrategy) -> Self {
96 self.merge_strategy = strategy;
97 self
98 }
99
100 fn embedded_german_first_names_male() -> Vec<&'static str> {
102 vec![
103 "Hans", "Klaus", "Wolfgang", "Dieter", "Michael", "Stefan", "Thomas", "Andreas",
104 "Peter", "Jürgen", "Matthias", "Frank", "Martin", "Bernd",
105 ]
106 }
107
108 fn embedded_german_first_names_female() -> Vec<&'static str> {
109 vec![
110 "Anna",
111 "Maria",
112 "Elisabeth",
113 "Ursula",
114 "Monika",
115 "Petra",
116 "Karin",
117 "Sabine",
118 "Andrea",
119 "Christine",
120 "Gabriele",
121 "Heike",
122 "Birgit",
123 ]
124 }
125
126 fn embedded_german_last_names() -> Vec<&'static str> {
127 vec![
128 "Müller",
129 "Schmidt",
130 "Schneider",
131 "Fischer",
132 "Weber",
133 "Meyer",
134 "Wagner",
135 "Becker",
136 "Schulz",
137 "Hoffmann",
138 "Schäfer",
139 "Koch",
140 "Bauer",
141 "Richter",
142 ]
143 }
144
145 fn embedded_us_first_names_male() -> Vec<&'static str> {
146 vec![
147 "James",
148 "John",
149 "Robert",
150 "Michael",
151 "William",
152 "David",
153 "Richard",
154 "Joseph",
155 "Thomas",
156 "Charles",
157 "Christopher",
158 "Daniel",
159 "Matthew",
160 ]
161 }
162
163 fn embedded_us_first_names_female() -> Vec<&'static str> {
164 vec![
165 "Mary",
166 "Patricia",
167 "Jennifer",
168 "Linda",
169 "Barbara",
170 "Elizabeth",
171 "Susan",
172 "Jessica",
173 "Sarah",
174 "Karen",
175 "Lisa",
176 "Nancy",
177 "Betty",
178 "Margaret",
179 ]
180 }
181
182 fn embedded_us_last_names() -> Vec<&'static str> {
183 vec![
184 "Smith",
185 "Johnson",
186 "Williams",
187 "Brown",
188 "Jones",
189 "Garcia",
190 "Miller",
191 "Davis",
192 "Rodriguez",
193 "Martinez",
194 "Hernandez",
195 "Lopez",
196 "Gonzalez",
197 ]
198 }
199
200 fn embedded_vendor_names_manufacturing() -> Vec<&'static str> {
201 vec![
202 "Precision Parts Inc.",
203 "Industrial Components LLC",
204 "Advanced Materials Corp.",
205 "Steel Solutions GmbH",
206 "Quality Fasteners Ltd.",
207 "Machining Excellence Inc.",
208 ]
209 }
210
211 fn embedded_vendor_names_services() -> Vec<&'static str> {
212 vec![
213 "Consulting Partners LLP",
214 "Technical Services Inc.",
215 "Professional Solutions LLC",
216 "Business Advisory Group",
217 "Strategic Consulting Co.",
218 "Expert Services Ltd.",
219 ]
220 }
221
222 fn embedded_customer_names_automotive() -> Vec<&'static str> {
223 vec![
224 "AutoWerke Industries",
225 "Vehicle Tech Solutions",
226 "Motor Parts Direct",
227 "Automotive Excellence Corp.",
228 "Drive Systems Inc.",
229 "Engine Components Ltd.",
230 ]
231 }
232
233 fn embedded_customer_names_retail() -> Vec<&'static str> {
234 vec![
235 "Retail Solutions Corp.",
236 "Consumer Goods Direct",
237 "Shop Smart Inc.",
238 "Merchandise Holdings LLC",
239 "Retail Distribution Co.",
240 "Store Systems Ltd.",
241 ]
242 }
243
244 fn culture_to_key(culture: NameCulture) -> &'static str {
245 match culture {
246 NameCulture::WesternUs => "us",
247 NameCulture::German => "german",
248 NameCulture::Hispanic => "hispanic",
249 NameCulture::French => "french",
250 NameCulture::Chinese => "chinese",
251 NameCulture::Japanese => "japanese",
252 NameCulture::Indian => "indian",
253 }
254 }
255
256 fn process_to_key(process: BusinessProcess) -> &'static str {
257 match process {
258 BusinessProcess::P2P => "p2p",
259 BusinessProcess::O2C => "o2c",
260 BusinessProcess::H2R => "h2r",
261 BusinessProcess::R2R => "r2r",
262 _ => "other",
263 }
264 }
265}
266
267impl Default for DefaultTemplateProvider {
268 fn default() -> Self {
269 Self::new()
270 }
271}
272
273impl TemplateProvider for DefaultTemplateProvider {
274 fn get_person_first_name(
275 &self,
276 culture: NameCulture,
277 is_male: bool,
278 rng: &mut dyn RngCore,
279 ) -> String {
280 let key = Self::culture_to_key(culture);
281
282 if let Some(ref data) = self.template_data {
284 if let Some(culture_names) = data.person_names.cultures.get(key) {
285 let names = if is_male {
286 &culture_names.male_first_names
287 } else {
288 &culture_names.female_first_names
289 };
290 if !names.is_empty() {
291 if let Some(name) = names.choose(rng) {
292 return name.clone();
293 }
294 }
295 }
296 }
297
298 let embedded = match culture {
300 NameCulture::German => {
301 if is_male {
302 Self::embedded_german_first_names_male()
303 } else {
304 Self::embedded_german_first_names_female()
305 }
306 }
307 _ => {
308 if is_male {
309 Self::embedded_us_first_names_male()
310 } else {
311 Self::embedded_us_first_names_female()
312 }
313 }
314 };
315
316 embedded.choose(rng).unwrap_or(&"Unknown").to_string()
317 }
318
319 fn get_person_last_name(&self, culture: NameCulture, rng: &mut dyn RngCore) -> String {
320 let key = Self::culture_to_key(culture);
321
322 if let Some(ref data) = self.template_data {
324 if let Some(culture_names) = data.person_names.cultures.get(key) {
325 if !culture_names.last_names.is_empty() {
326 if let Some(name) = culture_names.last_names.choose(rng) {
327 return name.clone();
328 }
329 }
330 }
331 }
332
333 let embedded = match culture {
335 NameCulture::German => Self::embedded_german_last_names(),
336 _ => Self::embedded_us_last_names(),
337 };
338
339 embedded.choose(rng).unwrap_or(&"Unknown").to_string()
340 }
341
342 fn get_vendor_name(&self, category: &str, rng: &mut dyn RngCore) -> String {
343 if let Some(ref data) = self.template_data {
345 if let Some(names) = data.vendor_names.categories.get(category) {
346 if !names.is_empty() {
347 if let Some(name) = names.choose(rng) {
348 return name.clone();
349 }
350 }
351 }
352 }
353
354 let embedded = match category {
356 "manufacturing" => Self::embedded_vendor_names_manufacturing(),
357 "services" => Self::embedded_vendor_names_services(),
358 _ => {
359 tracing::debug!(
360 "Unknown vendor name category '{}', falling back to manufacturing",
361 category
362 );
363 Self::embedded_vendor_names_manufacturing()
364 }
365 };
366
367 embedded
368 .choose(rng)
369 .unwrap_or(&"Unknown Vendor")
370 .to_string()
371 }
372
373 fn get_customer_name(&self, industry: &str, rng: &mut dyn RngCore) -> String {
374 if let Some(ref data) = self.template_data {
376 if let Some(names) = data.customer_names.industries.get(industry) {
377 if !names.is_empty() {
378 if let Some(name) = names.choose(rng) {
379 return name.clone();
380 }
381 }
382 }
383 }
384
385 let embedded = match industry {
387 "automotive" => Self::embedded_customer_names_automotive(),
388 "retail" => Self::embedded_customer_names_retail(),
389 _ => {
390 tracing::debug!(
391 "Unknown customer name industry '{}', falling back to retail",
392 industry
393 );
394 Self::embedded_customer_names_retail()
395 }
396 };
397
398 embedded
399 .choose(rng)
400 .unwrap_or(&"Unknown Customer")
401 .to_string()
402 }
403
404 fn get_material_description(&self, material_type: &str, rng: &mut dyn RngCore) -> String {
405 if let Some(ref data) = self.template_data {
407 if let Some(descs) = data.material_descriptions.by_type.get(material_type) {
408 if !descs.is_empty() {
409 if let Some(desc) = descs.choose(rng) {
410 return desc.clone();
411 }
412 }
413 }
414 }
415
416 format!("{} material", material_type)
418 }
419
420 fn get_asset_description(&self, category: &str, rng: &mut dyn RngCore) -> String {
421 if let Some(ref data) = self.template_data {
423 if let Some(descs) = data.asset_descriptions.by_category.get(category) {
424 if !descs.is_empty() {
425 if let Some(desc) = descs.choose(rng) {
426 return desc.clone();
427 }
428 }
429 }
430 }
431
432 format!("{} asset", category)
434 }
435
436 fn get_line_text(
437 &self,
438 process: BusinessProcess,
439 account_type: &str,
440 rng: &mut dyn RngCore,
441 ) -> String {
442 let key = Self::process_to_key(process);
443
444 if let Some(ref data) = self.template_data {
446 let descs_map = match process {
447 BusinessProcess::P2P => &data.line_item_descriptions.p2p,
448 BusinessProcess::O2C => &data.line_item_descriptions.o2c,
449 BusinessProcess::H2R => &data.line_item_descriptions.h2r,
450 BusinessProcess::R2R => &data.line_item_descriptions.r2r,
451 _ => &data.line_item_descriptions.p2p,
452 };
453
454 if let Some(descs) = descs_map.get(account_type) {
455 if !descs.is_empty() {
456 if let Some(desc) = descs.choose(rng) {
457 return desc.clone();
458 }
459 }
460 }
461 }
462
463 format!("{} posting", key.to_uppercase())
465 }
466
467 fn get_header_template(&self, process: BusinessProcess, rng: &mut dyn RngCore) -> String {
468 let key = Self::process_to_key(process);
469
470 if let Some(ref data) = self.template_data {
472 if let Some(templates) = data.header_text_templates.by_process.get(key) {
473 if !templates.is_empty() {
474 if let Some(template) = templates.choose(rng) {
475 return template.clone();
476 }
477 }
478 }
479 }
480
481 format!("{} Transaction", key.to_uppercase())
483 }
484}
485
486pub type SharedTemplateProvider = Arc<dyn TemplateProvider>;
488
489pub fn default_provider() -> SharedTemplateProvider {
491 Arc::new(DefaultTemplateProvider::new())
492}
493
494pub fn provider_from_file(
496 path: &std::path::Path,
497) -> Result<SharedTemplateProvider, super::loader::TemplateError> {
498 Ok(Arc::new(DefaultTemplateProvider::from_file(path)?))
499}
500
501#[cfg(test)]
502#[allow(clippy::unwrap_used)]
503mod tests {
504 use super::*;
505 use rand::SeedableRng;
506 use rand_chacha::ChaCha8Rng;
507
508 #[test]
509 fn test_default_provider() {
510 let provider = DefaultTemplateProvider::new();
511 let mut rng = ChaCha8Rng::seed_from_u64(12345);
512
513 let name = provider.get_person_first_name(NameCulture::German, true, &mut rng);
514 assert!(!name.is_empty());
515
516 let last_name = provider.get_person_last_name(NameCulture::German, &mut rng);
517 assert!(!last_name.is_empty());
518 }
519
520 #[test]
521 fn test_vendor_names() {
522 let provider = DefaultTemplateProvider::new();
523 let mut rng = ChaCha8Rng::seed_from_u64(12345);
524
525 let name = provider.get_vendor_name("manufacturing", &mut rng);
526 assert!(!name.is_empty());
527 assert!(!name.contains("Unknown"));
528 }
529
530 #[test]
531 fn test_shared_provider() {
532 let provider = default_provider();
533 let mut rng = ChaCha8Rng::seed_from_u64(12345);
534
535 let name = provider.get_customer_name("retail", &mut rng);
536 assert!(!name.is_empty());
537 }
538}