Skip to main content

datasynth_core/templates/
provider.rs

1//! Template provider trait and implementations.
2//!
3//! This module defines the `TemplateProvider` trait for accessing template data,
4//! along with implementations that combine embedded and file-based templates.
5
6use rand::prelude::SliceRandom;
7use rand::RngCore;
8use std::sync::Arc;
9
10use super::loader::{MergeStrategy, TemplateData, TemplateLoader};
11use super::names::NameCulture;
12use crate::models::BusinessProcess;
13
14/// Trait for providing template data to generators.
15///
16/// This trait abstracts the source of template data, allowing generators
17/// to work with either embedded templates, file-based templates, or a
18/// combination of both.
19///
20/// Methods use `&mut dyn RngCore` to allow the trait to be dyn-compatible.
21pub trait TemplateProvider: Send + Sync {
22    /// Get a random person first name for the given culture and gender.
23    fn get_person_first_name(
24        &self,
25        culture: NameCulture,
26        is_male: bool,
27        rng: &mut dyn RngCore,
28    ) -> String;
29
30    /// Get a random person last name for the given culture.
31    fn get_person_last_name(&self, culture: NameCulture, rng: &mut dyn RngCore) -> String;
32
33    /// Get a random vendor name for the given category.
34    fn get_vendor_name(&self, category: &str, rng: &mut dyn RngCore) -> String;
35
36    /// Get a random customer name for the given industry.
37    fn get_customer_name(&self, industry: &str, rng: &mut dyn RngCore) -> String;
38
39    /// Get a random material description for the given type.
40    fn get_material_description(&self, material_type: &str, rng: &mut dyn RngCore) -> String;
41
42    /// Get a random asset description for the given category.
43    fn get_asset_description(&self, category: &str, rng: &mut dyn RngCore) -> String;
44
45    /// Get a random line text for the given process and account type.
46    fn get_line_text(
47        &self,
48        process: BusinessProcess,
49        account_type: &str,
50        rng: &mut dyn RngCore,
51    ) -> String;
52
53    /// Get a random header text template for the given process.
54    fn get_header_template(&self, process: BusinessProcess, rng: &mut dyn RngCore) -> String;
55}
56
57/// Default template provider using embedded templates with optional file overrides.
58pub struct DefaultTemplateProvider {
59    /// Loaded template data (file-based)
60    template_data: Option<TemplateData>,
61    /// Merge strategy for combining embedded and file templates
62    merge_strategy: MergeStrategy,
63}
64
65impl DefaultTemplateProvider {
66    /// Create a new provider with embedded templates only.
67    pub fn new() -> Self {
68        Self {
69            template_data: None,
70            merge_strategy: MergeStrategy::Extend,
71        }
72    }
73
74    /// Create a provider with file-based templates.
75    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    /// Load templates from a file path.
83    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    /// Load templates from a directory.
89    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    /// Set the merge strategy.
95    pub fn with_merge_strategy(mut self, strategy: MergeStrategy) -> Self {
96        self.merge_strategy = strategy;
97        self
98    }
99
100    /// Get embedded German first names (sample).
101    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        // Try file templates first
283        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        // Fall back to embedded templates
299        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        // Try file templates first
323        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        // Fall back to embedded templates
334        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        // Try file templates first
344        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        // Fall back to embedded templates
355        let embedded = match category {
356            "manufacturing" => Self::embedded_vendor_names_manufacturing(),
357            "services" => Self::embedded_vendor_names_services(),
358            _ => Self::embedded_vendor_names_manufacturing(),
359        };
360
361        embedded
362            .choose(rng)
363            .unwrap_or(&"Unknown Vendor")
364            .to_string()
365    }
366
367    fn get_customer_name(&self, industry: &str, rng: &mut dyn RngCore) -> String {
368        // Try file templates first
369        if let Some(ref data) = self.template_data {
370            if let Some(names) = data.customer_names.industries.get(industry) {
371                if !names.is_empty() {
372                    if let Some(name) = names.choose(rng) {
373                        return name.clone();
374                    }
375                }
376            }
377        }
378
379        // Fall back to embedded templates
380        let embedded = match industry {
381            "automotive" => Self::embedded_customer_names_automotive(),
382            "retail" => Self::embedded_customer_names_retail(),
383            _ => Self::embedded_customer_names_retail(),
384        };
385
386        embedded
387            .choose(rng)
388            .unwrap_or(&"Unknown Customer")
389            .to_string()
390    }
391
392    fn get_material_description(&self, material_type: &str, rng: &mut dyn RngCore) -> String {
393        // Try file templates first
394        if let Some(ref data) = self.template_data {
395            if let Some(descs) = data.material_descriptions.by_type.get(material_type) {
396                if !descs.is_empty() {
397                    if let Some(desc) = descs.choose(rng) {
398                        return desc.clone();
399                    }
400                }
401            }
402        }
403
404        // Fall back to generic
405        format!("{} material", material_type)
406    }
407
408    fn get_asset_description(&self, category: &str, rng: &mut dyn RngCore) -> String {
409        // Try file templates first
410        if let Some(ref data) = self.template_data {
411            if let Some(descs) = data.asset_descriptions.by_category.get(category) {
412                if !descs.is_empty() {
413                    if let Some(desc) = descs.choose(rng) {
414                        return desc.clone();
415                    }
416                }
417            }
418        }
419
420        // Fall back to generic
421        format!("{} asset", category)
422    }
423
424    fn get_line_text(
425        &self,
426        process: BusinessProcess,
427        account_type: &str,
428        rng: &mut dyn RngCore,
429    ) -> String {
430        let key = Self::process_to_key(process);
431
432        // Try file templates first
433        if let Some(ref data) = self.template_data {
434            let descs_map = match process {
435                BusinessProcess::P2P => &data.line_item_descriptions.p2p,
436                BusinessProcess::O2C => &data.line_item_descriptions.o2c,
437                BusinessProcess::H2R => &data.line_item_descriptions.h2r,
438                BusinessProcess::R2R => &data.line_item_descriptions.r2r,
439                _ => &data.line_item_descriptions.p2p,
440            };
441
442            if let Some(descs) = descs_map.get(account_type) {
443                if !descs.is_empty() {
444                    if let Some(desc) = descs.choose(rng) {
445                        return desc.clone();
446                    }
447                }
448            }
449        }
450
451        // Fall back to generic
452        format!("{} posting", key.to_uppercase())
453    }
454
455    fn get_header_template(&self, process: BusinessProcess, rng: &mut dyn RngCore) -> String {
456        let key = Self::process_to_key(process);
457
458        // Try file templates first
459        if let Some(ref data) = self.template_data {
460            if let Some(templates) = data.header_text_templates.by_process.get(key) {
461                if !templates.is_empty() {
462                    if let Some(template) = templates.choose(rng) {
463                        return template.clone();
464                    }
465                }
466            }
467        }
468
469        // Fall back to generic
470        format!("{} Transaction", key.to_uppercase())
471    }
472}
473
474/// A thread-safe wrapper around a template provider.
475pub type SharedTemplateProvider = Arc<dyn TemplateProvider>;
476
477/// Create a default shared template provider.
478pub fn default_provider() -> SharedTemplateProvider {
479    Arc::new(DefaultTemplateProvider::new())
480}
481
482/// Create a shared template provider from a file.
483pub fn provider_from_file(
484    path: &std::path::Path,
485) -> Result<SharedTemplateProvider, super::loader::TemplateError> {
486    Ok(Arc::new(DefaultTemplateProvider::from_file(path)?))
487}
488
489#[cfg(test)]
490mod tests {
491    use super::*;
492    use rand::SeedableRng;
493    use rand_chacha::ChaCha8Rng;
494
495    #[test]
496    fn test_default_provider() {
497        let provider = DefaultTemplateProvider::new();
498        let mut rng = ChaCha8Rng::seed_from_u64(12345);
499
500        let name = provider.get_person_first_name(NameCulture::German, true, &mut rng);
501        assert!(!name.is_empty());
502
503        let last_name = provider.get_person_last_name(NameCulture::German, &mut rng);
504        assert!(!last_name.is_empty());
505    }
506
507    #[test]
508    fn test_vendor_names() {
509        let provider = DefaultTemplateProvider::new();
510        let mut rng = ChaCha8Rng::seed_from_u64(12345);
511
512        let name = provider.get_vendor_name("manufacturing", &mut rng);
513        assert!(!name.is_empty());
514        assert!(!name.contains("Unknown"));
515    }
516
517    #[test]
518    fn test_shared_provider() {
519        let provider = default_provider();
520        let mut rng = ChaCha8Rng::seed_from_u64(12345);
521
522        let name = provider.get_customer_name("retail", &mut rng);
523        assert!(!name.is_empty());
524    }
525}