Skip to content

Mock Data Generator — Go 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 Go implementation — the same logic the interactive tool runs, in a shareable, citable form.

// Package mockdata is the Go twin of CosmoDev's src/lib/mockData.ts (dual
// source: the web lib is TypeScript, the CLI lib is Go — kept in lock-step).
// Pure + deterministic, never panics. The table-driven tests in
// mock-data-generator_test.go share vectors with src/lib/mockData.test.ts so
// the two implementations are held to the same contract.
//
// Generation mirrors the TS lib exactly: a seeded mulberry32 PRNG drives every
// field, consumed in schema order, so identical (schema, count, seed) produces
// identical output. No math/rand, no time.Now. Invalid options fall back to
// defaults/zero-values mirroring the TS lib rather than panicking.
package mockdata

import (
	"fmt"
	"math"
	"strings"
	"time"
)

// FieldType enumerates the kinds of value a field can generate. It is the Go
// equivalent of the TS FieldType union; the underlying strings must match.
type FieldType string

const (
	FieldTypeFirstName FieldType = "firstName"
	FieldTypeLastName  FieldType = "lastName"
	FieldTypeFullName  FieldType = "fullName"
	FieldTypeEmail     FieldType = "email"
	FieldTypeUsername  FieldType = "username"
	FieldTypeInteger   FieldType = "integer"
	FieldTypeNumber    FieldType = "number"
	FieldTypeBoolean   FieldType = "boolean"
	FieldTypeUUID      FieldType = "uuid"
	FieldTypeDate      FieldType = "date"
	FieldTypePick      FieldType = "pick"
	FieldTypeString    FieldType = "string"
	FieldTypeIndex     FieldType = "index"
)

// FieldSpec describes one column of generated data. Optional fields use
// pointers/empties so an omitted value is distinguishable from a specified
// zero, mirroring the TS lib's ?? / || / isFinite defaults:
//   - Min/Max: nil → per-type default (integer 0/100, number 0/1, username 1/99)
//   - From/To: empty string → default date range ("2000-01-01"/"2025-12-31")
//   - Options: nil/empty → pick returns null
//   - Length: nil → default 8
type FieldSpec struct {
	Name    string
	Type    FieldType
	Min     *float64
	Max     *float64
	From    string
	To      string
	Options []string
	Length  *int
}

// firstNames / lastNames mirror the FIRST_NAMES / LAST_NAMES pools in the TS lib.
var (
	firstNames = []string{
		"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",
	}
	lastNames = []string{
		"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",
	}
)

const lowercaseLetters = "abcdefghijklmnopqrstuvwxyz"

// Mulberry32 returns a seeded PRNG yielding floats in [0, 1). It is the Go
// twin of mulberry32() in src/lib/mockData.ts: the arithmetic is carried out
// in uint32 so every bit matches the JS >>> 0 / Math.imul reference.
func Mulberry32(seed int) func() float64 {
	a := uint32(seed) // == seed >>> 0
	return func() float64 {
		a += 0x6d2b79f5                     // == (a + 0x6d2b79f5) | 0
		t := (a ^ (a >> 15)) * (1 | a)      // == Math.imul(a ^ (a>>>15), 1|a)
		t = (t + (t^(t>>7))*(61|t)) ^ t     // == (t + Math.imul(t ^ (t>>>7), 61|t)) ^ t
		return float64(t^(t>>14)) / 4294967296.0
	}
}

// isFinite mirrors JS Number.isFinite: false for NaN and ±Inf.
func isFinite(x float64) bool { return !math.IsNaN(x) && !math.IsInf(x, 0) }

// clampInt mirrors clampInt() in the TS lib: floor finite bounds (defaulting
// non-finite min→0, max→100) and return them ordered low/high so an inverted
// range is silently swapped.
func clampInt(minVal, maxVal float64) (lo, hi int) {
	lo = 0
	if isFinite(minVal) {
		lo = int(math.Floor(minVal))
	}
	hi = 100
	if isFinite(maxVal) {
		hi = int(math.Floor(maxVal))
	}
	if lo > hi {
		lo, hi = hi, lo
	}
	return lo, hi
}

// uuidFromRng generates a v4-style UUID from a PRNG, matching uuidFromRng():
// 32 hex digits then one extra draw for the variant nibble, version pinned.
func uuidFromRng(rng func() float64) string {
	const hexDigits = "0123456789abcdef"
	out := make([]byte, 32)
	for i := 0; i < 32; i++ {
		out[i] = hexDigits[int(rng()*16)]
	}
	out[12] = '4'
	out[16] = "89ab"[int(rng()*4)]
	return fmt.Sprintf("%s-%s-%s-%s-%s", out[0:8], out[8:12], out[12:16], out[16:20], out[20:32])
}

// sanitizeFieldName strips every rune that is not [A-Za-z0-9_$], falling back
// to "field<index>" when nothing remains — mirroring sanitizeFieldName().
func sanitizeFieldName(name string, idx int) string {
	var b strings.Builder
	for _, r := range name {
		switch {
		case r >= 'A' && r <= 'Z',
			r >= 'a' && r <= 'z',
			r >= '0' && r <= '9',
			r == '_', r == '$':
			b.WriteRune(r)
		}
	}
	if b.Len() == 0 {
		return fmt.Sprintf("field%d", idx)
	}
	return b.String()
}

// parseDateMS parses an ISO date ("YYYY-MM-DD") to UTC milliseconds, falling
// back to the given default string on failure so the generator never panics.
// time.Parse of a date-only layout yields UTC midnight, matching Date.parse.
func parseDateMS(s, fallback string) int64 {
	t, err := time.Parse("2006-01-02", s)
	if err != nil {
		t, err = time.Parse("2006-01-02", fallback)
		if err != nil {
			return 0
		}
	}
	return t.UnixMilli()
}

// GenerateMock produces count rows of deterministic mock data from a schema.
// It is the Go twin of generateMock() in src/lib/mockData.ts and must agree
// with it on every shared test vector.
func GenerateMock(schema []FieldSpec, count, seed int) []map[string]interface{} {
	rng := Mulberry32(seed)
	n := count
	if n < 0 {
		n = 0
	}
	rows := make([]map[string]interface{}, 0, n)
	for i := 0; i < n; i++ {
		row := map[string]interface{}{}
		for f, spec := range schema {
			key := sanitizeFieldName(spec.Name, f)
			row[key] = generateField(spec, i, rng)
		}
		rows = append(rows, row)
	}
	return rows
}

// generateField produces one value for a field, mirroring generateField(). The
// default case returns nil so an unknown type yields JSON null, never a panic.
func generateField(spec FieldSpec, index int, rng func() float64) interface{} {
	switch spec.Type {
	case FieldTypeIndex:
		return index
	case FieldTypeFirstName:
		return firstNames[int(rng()*float64(len(firstNames)))]
	case FieldTypeLastName:
		return lastNames[int(rng()*float64(len(lastNames)))]
	case FieldTypeFullName:
		first := firstNames[int(rng()*float64(len(firstNames)))]
		last := lastNames[int(rng()*float64(len(lastNames)))]
		return first + " " + last
	case FieldTypeUsername:
		first := strings.ToLower(firstNames[int(rng()*float64(len(firstNames)))])
		lo, hi := usernameBounds(spec)
		return first + fmt.Sprintf("%d", lo+int(rng()*float64(hi-lo+1)))
	case FieldTypeEmail:
		first := strings.ToLower(firstNames[int(rng()*float64(len(firstNames)))])
		last := strings.ToLower(lastNames[int(rng()*float64(len(lastNames)))])
		return first + "." + last + "@example.com"
	case FieldTypeInteger:
		lo, hi := integerBounds(spec)
		return lo + int(rng()*float64(hi-lo+1))
	case FieldTypeNumber:
		lo, hi := numberBounds(spec)
		if lo > hi {
			lo, hi = hi, lo
		}
		return math.Round((lo+rng()*(hi-lo))*10000) / 10000
	case FieldTypeBoolean:
		return rng() < 0.5
	case FieldTypeUUID:
		return uuidFromRng(rng)
	case FieldTypeDate:
		from, to := spec.From, spec.To
		if from == "" {
			from = "2000-01-01"
		}
		if to == "" {
			to = "2025-12-31"
		}
		lo := parseDateMS(from, "2000-01-01")
		hi := parseDateMS(to, "2025-12-31")
		if lo > hi {
			lo, hi = hi, lo
		}
		ms := lo + int64(rng()*float64(hi-lo))
		return time.UnixMilli(ms).UTC().Format("2006-01-02")
	case FieldTypePick:
		if len(spec.Options) == 0 {
			return nil
		}
		return spec.Options[int(rng()*float64(len(spec.Options)))]
	case FieldTypeString:
		length := 8
		if spec.Length != nil {
			length = *spec.Length
		}
		if length < 1 {
			length = 1
		}
		var b strings.Builder
		for i := 0; i < length; i++ {
			b.WriteByte(lowercaseLetters[int(rng()*float64(len(lowercaseLetters)))])
		}
		return b.String()
	default:
		return nil
	}
}

// usernameBounds resolves spec.Min/Max with the TS defaults (1/99), then clamps
// via clampInt so non-finite values fall back to 0/100 — matching the TS lib's
// ?? (which does not catch NaN) feeding into clampInt (which does).
func usernameBounds(spec FieldSpec) (int, int) {
	minVal, maxVal := 1.0, 99.0
	if spec.Min != nil {
		minVal = *spec.Min
	}
	if spec.Max != nil {
		maxVal = *spec.Max
	}
	return clampInt(minVal, maxVal)
}

// integerBounds resolves spec.Min/Max with the TS defaults (0/100), then clamps.
func integerBounds(spec FieldSpec) (int, int) {
	minVal, maxVal := 0.0, 100.0
	if spec.Min != nil {
		minVal = *spec.Min
	}
	if spec.Max != nil {
		maxVal = *spec.Max
	}
	return clampInt(minVal, maxVal)
}

// numberBounds resolves spec.Min/Max with the TS defaults (0/1), keeping them
// as floats and treating non-finite values as absent — mirroring the number
// branch's Number.isFinite gate.
func numberBounds(spec FieldSpec) (float64, float64) {
	minVal, maxVal := 0.0, 1.0
	if spec.Min != nil && isFinite(*spec.Min) {
		minVal = *spec.Min
	}
	if spec.Max != nil && isFinite(*spec.Max) {
		maxVal = *spec.Max
	}
	return minVal, maxVal
}

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 →