use crate::{
schemasync::TableConfig,
schemasync::mockmake::Mockmaker,
schemasync::mockmake::format::Format,
types::{
EnumRepresentation, FieldType, ForeignTypeRegistry, StructConfig, StructField, TaggedUnion,
VariantData,
},
};
use bon::Builder;
#[cfg(feature = "mockmake")]
use chrono_tz::TZ_VARIANTS;
use convert_case::{Case, Casing};
use rand::{RngExt, rngs::ThreadRng, seq::IndexedRandom};
use std::collections::HashMap;
#[derive(Debug, Builder)]
pub struct FieldValueGenerator<'a> {
mockmaker: &'a Mockmaker<'a>,
table_config: &'a TableConfig,
field: &'a StructField,
id_index: &'a usize,
coordinated_values: &'a HashMap<String, String>,
registry: &'a ForeignTypeRegistry,
}
impl<'a> FieldValueGenerator<'a> {
pub fn run(&self) -> String {
if let Some(format) = &self.field.format {
return self.handle_format(format);
}
self.generate_field_value(&self.field.field_type)
}
pub fn generate_field_value(&self, field_type: &FieldType) -> String {
tracing::trace!(
field_name = %self.field.field_name,
field_type = ?self.field.field_type,
"Generating field value"
);
let mut rng = rand::rng();
match field_type {
FieldType::String => format!("'{}'", Mockmaker::random_string(8)),
FieldType::Char => {
let c = rng.random_range(32u8..=126u8) as char;
format!("'{}'", c)
}
FieldType::Bool => format!("{}", rng.random_bool(0.5)),
FieldType::Unit => "NONE".to_string(),
FieldType::F32 | FieldType::F64 => {
format!("{:.2}f", rng.random_range(0.0..100.0))
}
FieldType::I8
| FieldType::I16
| FieldType::I32
| FieldType::I64
| FieldType::I128
| FieldType::Isize => {
format!("{}", rng.random_range(0..100))
}
FieldType::U8
| FieldType::U16
| FieldType::U32
| FieldType::U64
| FieldType::U128
| FieldType::Usize => format!("{}", rng.random_range(0..100)),
FieldType::Option(inner_type) => self.handle_option(inner_type, &mut rng),
FieldType::Vec(inner_type) => self.handle_vec(inner_type),
FieldType::Tuple(types) => self.handle_tuple(types),
FieldType::Struct(fields) => self.handle_struct(fields),
FieldType::HashMap(key, value) => self.handle_hash_map(key, value),
FieldType::BTreeMap(key, value) => self.handle_b_tree_map(key, value),
FieldType::RecordLink(inner_type) => self.generate_field_value(inner_type),
FieldType::Other(type_name) => self.handle_other(type_name, &mut rng),
}
}
pub fn handle_format(&self, format: &Format) -> String {
let generated = format.generate_formatted_value();
match format {
Format::Percentage
| Format::Latitude
| Format::Longitude
| Format::CurrencyAmount
| Format::AppointmentDurationNs => generated,
Format::DateTime | Format::AppointmentDateTime | Format::DateWithinDays(_) => {
format!("d'{}'", generated)
}
_ => format!("'{}'", generated),
}
}
fn handle_record_id(
&self,
field_name: &String,
table_name: &String,
rng: &mut ThreadRng,
) -> String {
{
if self.table_config.relation.is_none() && field_name == "id" {
match self.mockmaker.id_map.get(table_name) {
Some(ids) => return format!("r'{}'", ids[*self.id_index].clone()),
None => panic!(
"{}",
format!(
"There were no ids for the table {}, field {}\ncurrent id_map: {:#?}",
table_name, field_name, self.mockmaker.id_map
)
),
};
} else if field_name == "in" {
let relation = self.table_config.relation.as_ref().unwrap();
for candidate in &relation.from {
if let Some(ids) = self.mockmaker.id_map.get(candidate) {
if ids.is_empty() {
panic!(
"{}",
format!(
"There were no id's for the table {}, field {}",
candidate, field_name
)
)
}
return format!("r'{}'", ids[rng.random_range(0..ids.len())].clone());
}
}
panic!(
"{}",
format!(
"There were no id's for any of the tables {:?}, field {}",
relation.from, field_name
)
);
} else if field_name == "out" {
let relation = self.table_config.relation.as_ref().unwrap();
for candidate in &relation.to {
if let Some(ids) = self.mockmaker.id_map.get(candidate) {
if ids.is_empty() {
panic!(
"{}",
format!(
"There were no id's for the table {}, field {}",
candidate, field_name
)
)
}
return format!("r'{}'", ids[rng.random_range(0..ids.len())].clone());
}
}
panic!(
"{}",
format!(
"There were no id's for any of the tables {:?}, field {}",
relation.to, field_name
)
);
}
panic!(
"EvenframeRecordId used for field other than in, out, or id. Should use RecordLink type"
)
}
}
fn handle_option(&self, inner_type: &FieldType, rng: &mut ThreadRng) -> String {
if rng.random_bool(0.5) {
"null".to_string()
} else {
self.generate_field_value(inner_type)
}
}
fn handle_vec(&self, inner_type: &FieldType) -> String {
let count = rand::rng().random_range(2..10);
let items: Vec<String> = (0..count)
.map(|_| self.generate_field_value(inner_type))
.collect();
format!("[{}]", items.join(", "))
}
fn handle_tuple(&self, types: &[FieldType]) -> String {
let values: Vec<String> = types
.iter()
.map(|inner_type| self.generate_field_value(inner_type))
.collect();
format!("({})", values.join(", "))
}
fn handle_struct(&self, fields: &[(String, FieldType)]) -> String {
let mut nested_coordinated_values = HashMap::new();
let field_prefix = format!("{}.", self.field.field_name);
for (coord_field, coord_value) in self.coordinated_values {
if coord_field.starts_with(&field_prefix) {
let nested_field = &coord_field[field_prefix.len()..];
nested_coordinated_values.insert(nested_field.to_string(), coord_value.clone());
}
}
let field_values: Vec<String> = fields
.iter()
.map(|(fname, ftype)| {
let value = if let Some(coord_value) = nested_coordinated_values.get(fname) {
let is_datetime = if let FieldType::Other(name) = ftype {
self.registry
.lookup(name)
.is_some_and(|ftc| ftc.mock_strategy == "datetime")
} else {
false
};
if is_datetime {
format!("d'{}'", coord_value)
} else if matches!(ftype, FieldType::String) {
format!("'{}'", coord_value)
} else {
coord_value.clone()
}
} else {
self.generate_field_value(ftype)
};
format!("{}: {}", fname, value)
})
.collect();
format!("{{ {} }}", field_values.join(", "))
}
fn handle_hash_map(&self, key_ft: &FieldType, value_ft: &FieldType) -> String {
let count = rand::rng().random_range(0..3);
let entries: Vec<String> = (0..count)
.map(|_| {
let key_string = self.generate_field_value(key_ft);
let value_string = self.generate_field_value(value_ft);
format!("{}: {}", key_string, value_string)
})
.collect();
format!("{{ {} }}", entries.join(", "))
}
fn handle_b_tree_map(&self, key_ft: &FieldType, value_ft: &FieldType) -> String {
let count = rand::rng().random_range(0..3);
let entries: Vec<String> = (0..count)
.map(|_| {
let key_string = self.generate_field_value(key_ft);
let value_string = self.generate_field_value(value_ft);
format!("{}: {}", key_string, value_string)
})
.collect();
format!("{{ {} }}", entries.join(", "))
}
fn handle_other(&self, type_name: &String, rng: &mut ThreadRng) -> String {
if let Some(ftc) = self.registry.lookup(type_name) {
match ftc.mock_strategy.as_str() {
"datetime" => return format!("d'{}'", chrono::Utc::now().to_rfc3339()),
"duration" => {
return format!(
"duration::from_nanos({})",
rng.random_range(0..86_400_000_000_000i64)
);
}
"timezone" => {
#[cfg(feature = "mockmake")]
{
let tz = &TZ_VARIANTS[rng.random_range(0..TZ_VARIANTS.len())];
return format!("'{}'", tz.name());
}
#[cfg(not(feature = "mockmake"))]
{
let timezones = ["UTC", "America/New_York", "Europe/London", "Asia/Tokyo"];
return format!("'{}'", timezones[rng.random_range(0..timezones.len())]);
}
}
"decimal" => return format!("{:.3}dec", rng.random_range(0.0..100.0)),
"float" => return format!("{:.2}f", rng.random_range(0.0..100.0)),
"record_id" => {
return self.handle_record_id(
&self.field.field_name,
&self.table_config.table_name,
rng,
);
}
_ => {
}
}
}
let snake_case_name = type_name.to_case(Case::Snake);
if let Some((table_name, _)) = self
.mockmaker
.tables
.iter()
.find(|(_, table_config)| &table_config.table_name == type_name)
{
self.handle_table(table_name, rng)
} else if let Some(struct_config) = self
.mockmaker
.objects
.get(type_name)
.or_else(|| self.mockmaker.objects.get(&snake_case_name))
{
self.handle_object(struct_config)
} else if let Some(tagged_union) = self.mockmaker.enums.get(type_name) {
self.handle_enum(tagged_union, rng)
} else {
panic!(
"{}",
format!(
"This type could not be parsed table {}, field {}",
self.table_config.table_name, self.field.field_name
)
)
}
}
fn handle_table(&self, table_name: &String, rng: &mut ThreadRng) -> String {
if let Some(possible_ids) = self.mockmaker.id_map.get(table_name) {
let idx = rng.random_range(0..possible_ids.len());
format!("r'{}'", possible_ids[idx])
} else {
panic!(
"{}",
format!(
"There were no id's for the table {}, field {}",
table_name, self.field.field_name
)
)
}
}
fn handle_enum(&self, tagged_union: &TaggedUnion, rng: &mut ThreadRng) -> String {
let variant = tagged_union
.variants
.choose(rng)
.expect("Something went wrong selecting a random enum variant, returned None");
if let Some(ref variant_data) = variant.data {
let inner = match variant_data {
VariantData::InlineStruct(enum_struct) => {
self.generate_field_value(&FieldType::Other(enum_struct.struct_name.clone()))
}
VariantData::DataStructureRef(field_type) => self.generate_field_value(field_type),
};
match &tagged_union.representation {
EnumRepresentation::ExternallyTagged => {
format!("{{ {}: {} }}", variant.name, inner)
}
EnumRepresentation::InternallyTagged { tag } => {
if let VariantData::InlineStruct(_) = variant_data {
let trimmed = inner.trim();
if trimmed.starts_with('{') && trimmed.ends_with('}') {
let body = &trimmed[1..trimmed.len() - 1];
format!("{{ {}: '{}', {} }}", tag, variant.name, body.trim())
} else {
format!("{{ {}: '{}' }}", tag, variant.name)
}
} else {
format!("{{ {}: {} }}", variant.name, inner)
}
}
EnumRepresentation::AdjacentlyTagged { tag, content } => {
format!("{{ {}: '{}', {}: {} }}", tag, variant.name, content, inner)
}
EnumRepresentation::Untagged => inner,
}
} else {
match &tagged_union.representation {
EnumRepresentation::InternallyTagged { tag }
| EnumRepresentation::AdjacentlyTagged { tag, .. } => {
format!("{{ {}: '{}' }}", tag, variant.name)
}
_ => format!("'{}'", variant.name),
}
}
}
fn handle_object(&self, struct_config: &StructConfig) -> String {
let mut assignments = Vec::new();
for struct_field in &struct_config.fields {
let val = Self::builder()
.coordinated_values(self.coordinated_values)
.field(struct_field)
.id_index(self.id_index)
.mockmaker(self.mockmaker)
.table_config(self.table_config)
.registry(self.registry)
.build()
.run();
assignments.push(format!("{}: {val}", struct_field.field_name));
}
format!("{{ {} }}", assignments.join(", "))
}
}