use rand::seq::IndexedRandom;
use rand::Rng;
use std::sync::Arc;
use super::loader::{MergeStrategy, TemplateData, TemplateLoader};
use super::names::NameCulture;
use crate::models::BusinessProcess;
pub trait TemplateProvider: Send + Sync {
fn get_person_first_name(
&self,
culture: NameCulture,
is_male: bool,
rng: &mut dyn Rng,
) -> String;
fn get_person_last_name(&self, culture: NameCulture, rng: &mut dyn Rng) -> String;
fn get_vendor_name(&self, category: &str, rng: &mut dyn Rng) -> String;
fn get_customer_name(&self, industry: &str, rng: &mut dyn Rng) -> String;
fn get_material_description(&self, material_type: &str, rng: &mut dyn Rng) -> String;
fn get_asset_description(&self, category: &str, rng: &mut dyn Rng) -> String;
fn get_line_text(
&self,
process: BusinessProcess,
account_type: &str,
rng: &mut dyn Rng,
) -> String;
fn get_header_template(&self, process: BusinessProcess, rng: &mut dyn Rng) -> String;
fn get_bank_name(&self, _rng: &mut dyn Rng) -> Option<String> {
None
}
fn get_finding_title(
&self,
_finding_type_key: &str,
_rng: &mut dyn Rng,
) -> Option<(String, String)> {
None
}
fn get_finding_narrative(
&self,
_finding_type_key: &str,
_section: &str,
_rng: &mut dyn Rng,
) -> Option<String> {
None
}
fn get_department_name(&self, _department_code: &str, _rng: &mut dyn Rng) -> Option<String> {
None
}
}
pub struct DefaultTemplateProvider {
template_data: Option<TemplateData>,
merge_strategy: MergeStrategy,
}
pub const BUNDLED_DEFAULTS_YAML: &str = include_str!("../../templates/defaults.yaml");
impl DefaultTemplateProvider {
pub fn new() -> Self {
Self {
template_data: None,
merge_strategy: MergeStrategy::Extend,
}
}
pub fn bundled() -> Result<Self, super::loader::TemplateError> {
let data = TemplateLoader::load_from_yaml_str(BUNDLED_DEFAULTS_YAML)?;
Ok(Self::with_templates(data, MergeStrategy::Extend))
}
pub fn with_templates(template_data: TemplateData, strategy: MergeStrategy) -> Self {
Self {
template_data: Some(template_data),
merge_strategy: strategy,
}
}
pub fn from_file(path: &std::path::Path) -> Result<Self, super::loader::TemplateError> {
let data = TemplateLoader::load_from_file(path)?;
Ok(Self::with_templates(data, MergeStrategy::Extend))
}
pub fn from_directory(path: &std::path::Path) -> Result<Self, super::loader::TemplateError> {
let data = TemplateLoader::load_from_directory(path)?;
Ok(Self::with_templates(data, MergeStrategy::Extend))
}
pub fn with_merge_strategy(mut self, strategy: MergeStrategy) -> Self {
self.merge_strategy = strategy;
self
}
fn embedded_german_first_names_male() -> Vec<&'static str> {
vec![
"Hans", "Klaus", "Wolfgang", "Dieter", "Michael", "Stefan", "Thomas", "Andreas",
"Peter", "Jürgen", "Matthias", "Frank", "Martin", "Bernd",
]
}
fn embedded_german_first_names_female() -> Vec<&'static str> {
vec![
"Anna",
"Maria",
"Elisabeth",
"Ursula",
"Monika",
"Petra",
"Karin",
"Sabine",
"Andrea",
"Christine",
"Gabriele",
"Heike",
"Birgit",
]
}
fn embedded_german_last_names() -> Vec<&'static str> {
vec![
"Müller",
"Schmidt",
"Schneider",
"Fischer",
"Weber",
"Meyer",
"Wagner",
"Becker",
"Schulz",
"Hoffmann",
"Schäfer",
"Koch",
"Bauer",
"Richter",
]
}
fn embedded_us_first_names_male() -> Vec<&'static str> {
vec![
"James",
"John",
"Robert",
"Michael",
"William",
"David",
"Richard",
"Joseph",
"Thomas",
"Charles",
"Christopher",
"Daniel",
"Matthew",
]
}
fn embedded_us_first_names_female() -> Vec<&'static str> {
vec![
"Mary",
"Patricia",
"Jennifer",
"Linda",
"Barbara",
"Elizabeth",
"Susan",
"Jessica",
"Sarah",
"Karen",
"Lisa",
"Nancy",
"Betty",
"Margaret",
]
}
fn embedded_us_last_names() -> Vec<&'static str> {
vec![
"Smith",
"Johnson",
"Williams",
"Brown",
"Jones",
"Garcia",
"Miller",
"Davis",
"Rodriguez",
"Martinez",
"Hernandez",
"Lopez",
"Gonzalez",
]
}
fn embedded_vendor_names_manufacturing() -> Vec<&'static str> {
vec![
"Precision Parts Inc.",
"Industrial Components LLC",
"Advanced Materials Corp.",
"Steel Solutions GmbH",
"Quality Fasteners Ltd.",
"Machining Excellence Inc.",
]
}
fn embedded_vendor_names_services() -> Vec<&'static str> {
vec![
"Consulting Partners LLP",
"Technical Services Inc.",
"Professional Solutions LLC",
"Business Advisory Group",
"Strategic Consulting Co.",
"Expert Services Ltd.",
]
}
fn embedded_customer_names_automotive() -> Vec<&'static str> {
vec![
"AutoWerke Industries",
"Vehicle Tech Solutions",
"Motor Parts Direct",
"Automotive Excellence Corp.",
"Drive Systems Inc.",
"Engine Components Ltd.",
]
}
fn embedded_customer_names_retail() -> Vec<&'static str> {
vec![
"Retail Solutions Corp.",
"Consumer Goods Direct",
"Shop Smart Inc.",
"Merchandise Holdings LLC",
"Retail Distribution Co.",
"Store Systems Ltd.",
]
}
fn culture_to_key(culture: NameCulture) -> &'static str {
match culture {
NameCulture::WesternUs => "us",
NameCulture::German => "german",
NameCulture::Hispanic => "hispanic",
NameCulture::French => "french",
NameCulture::Chinese => "chinese",
NameCulture::Japanese => "japanese",
NameCulture::Indian => "indian",
}
}
fn process_to_key(process: BusinessProcess) -> &'static str {
match process {
BusinessProcess::P2P => "p2p",
BusinessProcess::O2C => "o2c",
BusinessProcess::H2R => "h2r",
BusinessProcess::R2R => "r2r",
_ => "other",
}
}
}
impl Default for DefaultTemplateProvider {
fn default() -> Self {
Self::new()
}
}
impl TemplateProvider for DefaultTemplateProvider {
fn get_person_first_name(
&self,
culture: NameCulture,
is_male: bool,
rng: &mut dyn Rng,
) -> String {
let key = Self::culture_to_key(culture);
if let Some(ref data) = self.template_data {
if let Some(culture_names) = data.person_names.cultures.get(key) {
let names = if is_male {
&culture_names.male_first_names
} else {
&culture_names.female_first_names
};
if !names.is_empty() {
if let Some(name) = names.choose(rng) {
return name.clone();
}
}
}
}
let embedded = match culture {
NameCulture::German => {
if is_male {
Self::embedded_german_first_names_male()
} else {
Self::embedded_german_first_names_female()
}
}
_ => {
if is_male {
Self::embedded_us_first_names_male()
} else {
Self::embedded_us_first_names_female()
}
}
};
embedded.choose(rng).unwrap_or(&"Unknown").to_string()
}
fn get_person_last_name(&self, culture: NameCulture, rng: &mut dyn Rng) -> String {
let key = Self::culture_to_key(culture);
if let Some(ref data) = self.template_data {
if let Some(culture_names) = data.person_names.cultures.get(key) {
if !culture_names.last_names.is_empty() {
if let Some(name) = culture_names.last_names.choose(rng) {
return name.clone();
}
}
}
}
let embedded = match culture {
NameCulture::German => Self::embedded_german_last_names(),
_ => Self::embedded_us_last_names(),
};
embedded.choose(rng).unwrap_or(&"Unknown").to_string()
}
fn get_vendor_name(&self, category: &str, rng: &mut dyn Rng) -> String {
if let Some(ref data) = self.template_data {
if let Some(names) = data.vendor_names.categories.get(category) {
if !names.is_empty() {
if let Some(name) = names.choose(rng) {
return name.clone();
}
}
}
}
let embedded = match category {
"manufacturing" => Self::embedded_vendor_names_manufacturing(),
"services" => Self::embedded_vendor_names_services(),
_ => {
tracing::debug!(
"Unknown vendor name category '{}', falling back to manufacturing",
category
);
Self::embedded_vendor_names_manufacturing()
}
};
embedded
.choose(rng)
.unwrap_or(&"Unknown Vendor")
.to_string()
}
fn get_customer_name(&self, industry: &str, rng: &mut dyn Rng) -> String {
if let Some(ref data) = self.template_data {
if let Some(names) = data.customer_names.industries.get(industry) {
if !names.is_empty() {
if let Some(name) = names.choose(rng) {
return name.clone();
}
}
}
}
let embedded = match industry {
"automotive" => Self::embedded_customer_names_automotive(),
"retail" => Self::embedded_customer_names_retail(),
_ => {
tracing::debug!(
"Unknown customer name industry '{}', falling back to retail",
industry
);
Self::embedded_customer_names_retail()
}
};
embedded
.choose(rng)
.unwrap_or(&"Unknown Customer")
.to_string()
}
fn get_material_description(&self, material_type: &str, rng: &mut dyn Rng) -> String {
if let Some(ref data) = self.template_data {
if let Some(descs) = data.material_descriptions.by_type.get(material_type) {
if !descs.is_empty() {
if let Some(desc) = descs.choose(rng) {
return desc.clone();
}
}
}
}
format!("{material_type} material")
}
fn get_asset_description(&self, category: &str, rng: &mut dyn Rng) -> String {
if let Some(ref data) = self.template_data {
if let Some(descs) = data.asset_descriptions.by_category.get(category) {
if !descs.is_empty() {
if let Some(desc) = descs.choose(rng) {
return desc.clone();
}
}
}
}
format!("{category} asset")
}
fn get_line_text(
&self,
process: BusinessProcess,
account_type: &str,
rng: &mut dyn Rng,
) -> String {
let key = Self::process_to_key(process);
if let Some(ref data) = self.template_data {
let descs_map = match process {
BusinessProcess::P2P => &data.line_item_descriptions.p2p,
BusinessProcess::O2C => &data.line_item_descriptions.o2c,
BusinessProcess::H2R => &data.line_item_descriptions.h2r,
BusinessProcess::R2R => &data.line_item_descriptions.r2r,
_ => &data.line_item_descriptions.p2p,
};
if let Some(descs) = descs_map.get(account_type) {
if !descs.is_empty() {
if let Some(desc) = descs.choose(rng) {
return desc.clone();
}
}
}
}
format!("{} posting", key.to_uppercase())
}
fn get_header_template(&self, process: BusinessProcess, rng: &mut dyn Rng) -> String {
let key = Self::process_to_key(process);
if let Some(ref data) = self.template_data {
if let Some(templates) = data.header_text_templates.by_process.get(key) {
if !templates.is_empty() {
if let Some(template) = templates.choose(rng) {
return template.clone();
}
}
}
}
format!("{} Transaction", key.to_uppercase())
}
fn get_bank_name(&self, rng: &mut dyn Rng) -> Option<String> {
if let Some(ref data) = self.template_data {
if !data.bank_names.names.is_empty() {
if let Some(name) = data.bank_names.names.choose(rng) {
return Some(name.clone());
}
}
}
None
}
fn get_finding_title(
&self,
finding_type_key: &str,
rng: &mut dyn Rng,
) -> Option<(String, String)> {
if let Some(ref data) = self.template_data {
if let Some(entries) = data.finding_titles.by_type.get(finding_type_key) {
if !entries.is_empty() {
if let Some(entry) = entries.choose(rng) {
return Some((entry.title.clone(), entry.account.clone()));
}
}
}
}
None
}
fn get_finding_narrative(
&self,
finding_type_key: &str,
section: &str,
rng: &mut dyn Rng,
) -> Option<String> {
if let Some(ref data) = self.template_data {
if let Some(sections) = data.finding_narratives.by_type.get(finding_type_key) {
if let Some(templates) = sections.get(section) {
if !templates.is_empty() {
if let Some(tpl) = templates.choose(rng) {
return Some(tpl.clone());
}
}
}
}
}
None
}
fn get_department_name(&self, department_code: &str, _rng: &mut dyn Rng) -> Option<String> {
if let Some(ref data) = self.template_data {
if let Some(name) = data.department_names.by_code.get(department_code) {
if !name.is_empty() {
return Some(name.clone());
}
}
}
None
}
}
pub type SharedTemplateProvider = Arc<dyn TemplateProvider>;
pub fn default_provider() -> SharedTemplateProvider {
Arc::new(DefaultTemplateProvider::new())
}
pub fn provider_from_file(
path: &std::path::Path,
) -> Result<SharedTemplateProvider, super::loader::TemplateError> {
Ok(Arc::new(DefaultTemplateProvider::from_file(path)?))
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use rand::SeedableRng;
use rand_chacha::ChaCha8Rng;
#[test]
fn test_default_provider() {
let provider = DefaultTemplateProvider::new();
let mut rng = ChaCha8Rng::seed_from_u64(12345);
let name = provider.get_person_first_name(NameCulture::German, true, &mut rng);
assert!(!name.is_empty());
let last_name = provider.get_person_last_name(NameCulture::German, &mut rng);
assert!(!last_name.is_empty());
}
#[test]
fn bundled_defaults_loads() {
let provider = DefaultTemplateProvider::bundled().expect("bundled YAML parses");
let mut rng = ChaCha8Rng::seed_from_u64(42);
let vendor = provider.get_vendor_name("office_supplies", &mut rng);
assert!(!vendor.is_empty());
let customer = provider.get_customer_name("retail", &mut rng);
assert!(!customer.is_empty());
}
#[test]
fn bundled_matches_embedded_for_person_names() {
for culture in [NameCulture::German, NameCulture::WesternUs] {
for is_male in [true, false] {
let p_embedded = DefaultTemplateProvider::new();
let p_bundled = DefaultTemplateProvider::bundled().unwrap();
let mut rng_e = ChaCha8Rng::seed_from_u64(12345);
let mut rng_b = ChaCha8Rng::seed_from_u64(12345);
for _ in 0..500 {
let ne = p_embedded.get_person_first_name(culture, is_male, &mut rng_e);
let nb = p_bundled.get_person_first_name(culture, is_male, &mut rng_b);
assert_eq!(
ne, nb,
"first name mismatch for culture={culture:?} male={is_male}"
);
}
}
let p_embedded = DefaultTemplateProvider::new();
let p_bundled = DefaultTemplateProvider::bundled().unwrap();
let mut rng_e = ChaCha8Rng::seed_from_u64(54321);
let mut rng_b = ChaCha8Rng::seed_from_u64(54321);
for _ in 0..500 {
let ne = p_embedded.get_person_last_name(culture, &mut rng_e);
let nb = p_bundled.get_person_last_name(culture, &mut rng_b);
assert_eq!(ne, nb, "last name mismatch for culture={culture:?}");
}
}
}
#[test]
fn bundled_matches_embedded_for_vendor_customer_names() {
for category in ["manufacturing", "services"] {
let p_embedded = DefaultTemplateProvider::new();
let p_bundled = DefaultTemplateProvider::bundled().unwrap();
let mut rng_e = ChaCha8Rng::seed_from_u64(99);
let mut rng_b = ChaCha8Rng::seed_from_u64(99);
for _ in 0..200 {
let ne = p_embedded.get_vendor_name(category, &mut rng_e);
let nb = p_bundled.get_vendor_name(category, &mut rng_b);
assert_eq!(ne, nb, "vendor mismatch for category={category}");
}
}
for industry in ["automotive", "retail"] {
let p_embedded = DefaultTemplateProvider::new();
let p_bundled = DefaultTemplateProvider::bundled().unwrap();
let mut rng_e = ChaCha8Rng::seed_from_u64(77);
let mut rng_b = ChaCha8Rng::seed_from_u64(77);
for _ in 0..200 {
let ne = p_embedded.get_customer_name(industry, &mut rng_e);
let nb = p_bundled.get_customer_name(industry, &mut rng_b);
assert_eq!(ne, nb, "customer mismatch for industry={industry}");
}
}
}
#[test]
fn bundled_defaults_include_embedded_mirrored_entries() {
let provider = DefaultTemplateProvider::bundled().expect("bundled YAML parses");
let mut rng = ChaCha8Rng::seed_from_u64(1);
let expected = [
"Retail Solutions Corp.",
"Consumer Goods Direct",
"Shop Smart Inc.",
"Merchandise Holdings LLC",
"Retail Distribution Co.",
"Store Systems Ltd.",
];
let mut saw_expected = false;
for _ in 0..500 {
let name = provider.get_customer_name("retail", &mut rng);
if expected.contains(&name.as_str()) {
saw_expected = true;
break;
}
}
assert!(
saw_expected,
"bundled retail customer names (mirrored from embedded) should appear in the draw stream"
);
}
#[test]
fn test_vendor_names() {
let provider = DefaultTemplateProvider::new();
let mut rng = ChaCha8Rng::seed_from_u64(12345);
let name = provider.get_vendor_name("manufacturing", &mut rng);
assert!(!name.is_empty());
assert!(!name.contains("Unknown"));
}
#[test]
fn test_shared_provider() {
let provider = default_provider();
let mut rng = ChaCha8Rng::seed_from_u64(12345);
let name = provider.get_customer_name("retail", &mut rng);
assert!(!name.is_empty());
}
}