Skip to content

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 →