Mock Data Generator — Rust source
Generate deterministic fake data (names, emails, numbers, dates, booleans, UUIDs, pick-from-list) from a schema and a seed. Reproducible output.
This is the Rust implementation — the same logic the interactive tool runs, in a shareable, citable form.
//! mock-data-generator — Rust polyglot showcase port.
//!
//! CosmoDev polyglot showcase port of mock-data-generator.
//! Ported from src/lib/mockData.ts (the canonical TypeScript reference).
//!
//! Pure mock-data generation — deterministic and seed-stable. Identical
//! (schema, count, seed) always yields byte-identical output, so a seed
//! reproduces a fixture exactly. There is no time/rand side-effect: the only
//! source of "randomness" is a seeded mulberry32 PRNG, which is fast but NOT
//! cryptographically secure — mock data is not a secret, so speed wins.
//!
//! The reference JS does its arithmetic in 32-bit integer semantics (`|0`,
//! `>>>`, `Math.imul`). Rust models that directly with `u32`: wrapping add and
//! multiply on `u32` reproduce the exact bit stream the other ports emulate
//! with explicit masking. Dates are handled with a tiny proleptic-Gregorian
//! converter so the whole file stays stdlib-only (no `chrono`).
//!
//! Display source — part of CosmoDev's polyglot tool pages.
//!
//! Usage:
//!
//! ```ignore
//! let rows = mockdatagenerator::generate_mock(&schema, 5, 1234);
//! ```
use std::collections::BTreeMap;
use std::mem;
/// The kinds of values a field can synthesize.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum FieldType {
FirstName,
LastName,
FullName,
Email,
Username,
Integer,
Number,
Boolean,
Uuid,
Date,
Pick,
/// A random lowercase string. (Named to avoid the `String` type.)
RandomString,
Index,
}
/// A single column declaration. Numeric bounds are `Option` so "absent" is
/// distinct from a real zero — this matches the TS `min ?? default` semantics,
/// where a missing bound and an explicit 0 differ.
#[derive(Clone, Debug)]
pub struct FieldSpec {
pub name: String,
pub field_type: FieldType,
pub min: Option<f64>,
pub max: Option<f64>,
pub from: Option<String>,
pub to: Option<String>,
pub options: Option<Vec<String>>,
pub length: Option<f64>,
}
impl FieldSpec {
/// Convenience constructor: a column with only a name and a type.
pub fn new(name: &str, field_type: FieldType) -> Self {
Self {
name: name.to_string(),
field_type,
min: None,
max: None,
from: None,
to: None,
options: None,
length: None,
}
}
}
/// A synthesized value. Distinguishing integers from floats mirrors the JS
/// output where `integer`/`index` serialize as integers and `number` as a
/// float; the JSON rendering below respects that distinction.
#[derive(Clone, Debug)]
pub enum Value {
Null,
Bool(bool),
Int(i64),
Float(f64),
Str(String),
}
const FIRST_NAMES: &[&str] = &[
"Ava", "Liam", "Noah", "Emma", "Olivia", "Aiden", "Sophia", "Mason", "Isabella",
"Lucas", "Mia", "Ethan", "Amelia", "Leo", "Harper", "Ezra", "Ella", "Owen",
"Luna", "Finn", "Zoe", "Jude", "Nora", "Kai", "Ruby", "Theo", "Ivy", "Max",
];
const LAST_NAMES: &[&str] = &[
"Smith", "Johnson", "Williams", "Brown", "Jones", "Garcia", "Miller", "Davis",
"Rodriguez", "Martinez", "Hernandez", "Lopez", "Gonzalez", "Wilson", "Anderson",
"Thomas", "Taylor", "Moore", "Jackson", "Martin", "Lee", "Perez", "Thompson",
"White", "Harris", "Sanchez", "Clark", "Ramirez",
];
/// A deterministic mulberry32 PRNG (fast, NOT crypto-secure).
///
/// All arithmetic is `u32` with wrapping ops, so `wrapping_add` and
/// `wrapping_mul` reproduce the exact bit stream of the reference (which uses
/// JS `|0` wraparound and `Math.imul` low-32 multiply).
pub struct Mulberry32 {
a: u32,
}
impl Mulberry32 {
pub fn new(seed: u32) -> Self {
Self { a: seed }
}
/// Advance the state and return the next float in [0, 1).
pub fn next(&mut self) -> f64 {
self.a = self.a.wrapping_add(0x6D2B79F5);
let mut t = (self.a ^ (self.a >> 15)).wrapping_mul(1 | self.a);
t = (t.wrapping_add((t ^ (t >> 7)).wrapping_mul(61 | t))) ^ t;
(t ^ (t >> 14)) as f64 / 4_294_967_296.0
}
}
/// Pick an index in `[0, len)` using one PRNG draw. Truncation toward zero
/// equals `Math.floor` for the non-negative draws produced here.
fn pick_index(rng: &mut Mulberry32, len: usize) -> usize {
(rng.next() * len as f64) as usize
}
/// Resolve `[min, max]` into an ordered integer range with given defaults.
///
/// Missing or non-finite bounds collapse to `def_lo`/`def_hi` (0/100 for
/// integers, 1/99 for usernames). The returned pair is always lo <= hi so the
/// caller's span math can never invert.
fn clamp_int(min: Option<f64>, max: Option<f64>, def_lo: i64, def_hi: i64) -> (i64, i64) {
let resolve = |v: Option<f64>, def: i64| match v {
Some(x) if x.is_finite() => x.floor() as i64,
_ => def,
};
let (mut lo, mut hi) = (resolve(min, def_lo), resolve(max, def_hi));
if lo > hi {
mem::swap(&mut lo, &mut hi);
}
(lo, hi)
}
/// Build a v4-shaped UUID from the PRNG stream.
///
/// Sets the version nibble (position 12 -> '4') and a variant nibble
/// (position 16 -> 8/9/a/b) so the result parses as a legal RFC 4122 v4 UUID,
/// even though the bytes are deterministic, not secret.
fn uuid_from_rng(rng: &mut Mulberry32) -> String {
const HEX: &[u8] = b"0123456789abcdef";
let mut bytes = [0u8; 32];
for slot in bytes.iter_mut() {
*slot = HEX[(rng.next() * 16.0) as usize];
}
bytes[12] = b'4';
bytes[16] = b"89ab"[(rng.next() * 4.0) as usize];
// The five RFC groups: 8-4-4-4-12 hex digits.
let s = |range: &[u8]| std::str::from_utf8(range).unwrap();
format!(
"{}-{}-{}-{}-{}",
s(&bytes[0..8]),
s(&bytes[8..12]),
s(&bytes[12..16]),
s(&bytes[16..20]),
s(&bytes[20..32]),
)
}
/// Coerce a column name into an identifier-safe key.
///
/// Strips everything outside `[A-Za-z0-9_$]`; an all-stripped name becomes
/// `fieldN` so no row ever loses a key.
fn sanitize_field_name(name: &str, idx: usize) -> String {
let cleaned: String = name
.chars()
.filter(|c| c.is_ascii_alphanumeric() || *c == '_' || *c == '$')
.collect();
if cleaned.is_empty() {
format!("field{}", idx)
} else {
cleaned
}
}
/// Parse a strict "YYYY-MM-DD" string into (year, month, day).
fn parse_iso_date(s: &str) -> Option<(i32, u32, u32)> {
let bytes = s.as_bytes();
if bytes.len() < 10 || bytes[4] != b'-' || bytes[7] != b'-' {
return None;
}
let y: i32 = s.get(0..4)?.parse().ok()?;
let m: u32 = s.get(5..7)?.parse().ok()?;
let d: u32 = s.get(8..10)?.parse().ok()?;
Some((y, m, d))
}
/// Days from 1970-01-01 to a proleptic Gregorian date (Hinnant's algorithm).
/// Matches the UTC-midnight epoch that JS `Date.parse` uses for date-only ISO.
fn civil_to_days(y: i32, m: u32, d: u32) -> i64 {
let y = if m <= 2 { y - 1 } else { y };
let era = if y >= 0 { y } else { y - 399 } / 400;
let yoe = (y - era * 400) as i64; // [0, 399]
let m_adj = if m > 2 { m - 3 } else { m + 9 } as i64;
let doy = (153 * m_adj + 2) / 5 + d as i64 - 1;
let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
era as i64 * 146_097 + doe - 719_468
}
/// Inverse of `civil_to_days`: days-since-epoch -> (year, month, day).
fn days_to_civil(z: i64) -> (i32, u32, u32) {
let z = z + 719_468;
let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
let doe = z - era * 146_097; // [0, 146096]
let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146_096) / 365; // [0, 399]
let y = yoe + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); // [0, 365]
let mp = (5 * doy + 2) / 153; // [0, 4]
let d = (doy - (153 * mp + 2) / 5 + 1) as u32; // [1, 31]
let m = if mp < 10 { mp + 3 } else { mp - 9 } as u32; // [1, 12]
let y = (if m <= 2 { y + 1 } else { y }) as i32;
(y, m, d)
}
/// Parse an ISO date to UTC milliseconds, defaulting on empty/invalid input
/// so a bad date never corrupts the PRNG stream alignment.
fn parse_date_ms(s: Option<&str>, fallback: &str) -> i64 {
let text = s.filter(|t| !t.is_empty()).unwrap_or(fallback);
let (y, m, d) = parse_iso_date(text).unwrap_or_else(|| {
let (fy, fm, fd) = parse_iso_date(fallback).unwrap_or((2000, 1, 1));
(fy, fm, fd)
});
civil_to_days(y, m, d) * 86_400_000
}
/// Generate `count` rows of mock data from a schema.
///
/// The PRNG is seeded once and consumed left-to-right, row by row and field by
/// field — that consumption order is what makes output reproducible: inserting
/// or reordering a field shifts every later value. A `BTreeMap` keeps keys
/// sorted for stable display (JS preserves insertion order; the values are
/// identical either way). Negative counts yield zero rows.
pub fn generate_mock(schema: &[FieldSpec], count: i64, seed: u32) -> Vec<BTreeMap<String, Value>> {
let mut rng = Mulberry32::new(seed);
let n = count.max(0) as usize;
let mut rows = Vec::with_capacity(n);
for i in 0..n {
let mut row = BTreeMap::new();
for (f, spec) in schema.iter().enumerate() {
let key = sanitize_field_name(&spec.name, f);
row.insert(key, generate_field(spec, i as i64, &mut rng));
}
rows.push(row);
}
rows
}
/// Produce one value for one field. Each branch consumes a fixed number of PRNG
/// draws, keeping the stream aligned across rows and identical to the reference.
fn generate_field(spec: &FieldSpec, index: i64, rng: &mut Mulberry32) -> Value {
match spec.field_type {
FieldType::Index => Value::Int(index),
FieldType::FirstName => Value::Str(FIRST_NAMES[pick_index(rng, FIRST_NAMES.len())].into()),
FieldType::LastName => Value::Str(LAST_NAMES[pick_index(rng, LAST_NAMES.len())].into()),
FieldType::FullName => {
let first = FIRST_NAMES[pick_index(rng, FIRST_NAMES.len())];
let last = LAST_NAMES[pick_index(rng, LAST_NAMES.len())];
Value::Str(format!("{} {}", first, last))
}
FieldType::Username => {
let first = FIRST_NAMES[pick_index(rng, FIRST_NAMES.len())].to_lowercase();
let (lo, hi) = clamp_int(spec.min, spec.max, 1, 99);
let num = lo + (rng.next() * (hi - lo + 1) as f64).floor() as i64;
Value::Str(format!("{}{}", first, num))
}
FieldType::Email => {
let first = FIRST_NAMES[pick_index(rng, FIRST_NAMES.len())].to_lowercase();
let last = LAST_NAMES[pick_index(rng, LAST_NAMES.len())].to_lowercase();
Value::Str(format!("{}.{}@example.com", first, last))
}
FieldType::Integer => {
let (lo, hi) = clamp_int(spec.min, spec.max, 0, 100);
Value::Int(lo + (rng.next() * (hi - lo + 1) as f64).floor() as i64)
}
FieldType::Number => {
let min_v = spec.min.filter(|x| x.is_finite()).unwrap_or(0.0);
let max_v = spec.max.filter(|x| x.is_finite()).unwrap_or(1.0);
let lo = min_v.min(max_v);
let hi = min_v.max(max_v);
let value = lo + rng.next() * (hi - lo);
// Round to 4 decimals; floor(x + 0.5) reproduces JS Math.round
// (round-half-up toward +inf) across the whole real line.
Value::Float((value * 10_000.0 + 0.5).floor() / 10_000.0)
}
FieldType::Boolean => Value::Bool(rng.next() < 0.5),
FieldType::Uuid => Value::Str(uuid_from_rng(rng)),
FieldType::Date => {
let from_ms = parse_date_ms(spec.from.as_deref(), "2000-01-01");
let to_ms = parse_date_ms(spec.to.as_deref(), "2025-12-31");
let (lo, hi) = if from_ms < to_ms {
(from_ms, to_ms)
} else {
(to_ms, from_ms)
};
let ms = lo + (rng.next() * (hi - lo) as f64).floor() as i64;
// Midnights are exact multiples of 86_400_000 ms; floor-divide back
// to a day count and rebuild the civil (UTC) date.
let days = ms.div_euclid(86_400_000);
let (y, m, d) = days_to_civil(days);
Value::Str(format!("{:04}-{:02}-{:02}", y, m, d))
}
FieldType::Pick => match spec.options.as_deref() {
Some(opts) if !opts.is_empty() => {
Value::Str(opts[pick_index(rng, opts.len())].clone())
}
_ => Value::Null,
},
FieldType::RandomString => {
let mut length = spec.length.filter(|x| x.is_finite()).unwrap_or(8.0).floor() as i64;
if length < 1 {
length = 1;
}
const CHARS: &[u8] = b"abcdefghijklmnopqrstuvwxyz";
let mut out = String::with_capacity(length as usize);
for _ in 0..length {
out.push(CHARS[pick_index(rng, CHARS.len())] as char);
}
Value::Str(out)
}
}
}
// ---------------------------------------------------------------------------
// JSON convenience rendering. The TS source returns plain objects; this keeps
// the Rust port demonstrable end-to-end without pulling in serde.
// ---------------------------------------------------------------------------
impl Value {
/// Render the value as a compact JSON fragment.
pub fn to_json(&self) -> String {
match self {
Value::Null => "null".into(),
Value::Bool(b) => b.to_string(),
Value::Int(i) => i.to_string(),
// Render floats with enough precision; integers-valued floats keep
// a trailing ".0" to match JS number serialization.
Value::Float(f) => format!("{}", f),
Value::Str(s) => {
let escaped = s.replace('\\', r"\\").replace('"', "\\\"");
format!("\"{}\"", escaped)
}
}
}
}
/// Render a single row as a compact JSON object.
pub fn row_to_json(row: &BTreeMap<String, Value>) -> String {
let parts: Vec<String> = row
.iter()
.map(|(k, v)| format!("\"{}\": {}", k.replace('"', "\\\""), v.to_json()))
.collect();
format!("{{{}}}", parts.join(", "))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn seed_is_stable() {
// Same seed -> same first row, and the stream is reproducible.
let mut rng = Mulberry32::new(1234);
let a = rng.next();
let mut rng2 = Mulberry32::new(1234);
let b = rng2.next();
assert_eq!(a, b);
}
#[test]
fn epoch_anchor_matches_js() {
// civil_to_days must agree with JS Date.parse for the default range.
assert_eq!(civil_to_days(2000, 1, 1) * 86_400_000, 946_684_800_000);
assert_eq!(civil_to_days(2025, 12, 31) * 86_400_000, 1_767_139_200_000);
}
}
Also available in 13 other languages
Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →