Class: Tina4::FakeData

Inherits:
Object
  • Object
show all
Defined in:
lib/tina4/seeder.rb

Overview

Zero-dependency fake data generator with deterministic seeding. Uses Ruby's built-in Random for reproducible data generation.

Determinism is PER-LANGUAGE, not cross-language (SEED-DETERMINISM-PERLANG): FakeData.new(seed: 42) reproduces the identical sequence on every run within Ruby, but the same seed on Python/PHP/Node's FakeData will NOT produce the same values -- each language uses its own PRNG (Ruby's Random, Python's Mersenne Twister, PHP's per-instance Mt19937, Node's mulberry32). There is no shared cross-language PRNG, and hand-rolling one would add cost for no real benefit -- use a seed to make ONE language's run reproducible, never to compare output across languages.

NOT FOR SECRETS (SEED-SECRETS-DOC): this is a non-cryptographic PRNG meant for realistic-looking fixtures and test data. Never use it to generate API keys, passwords, tokens, or anything else that must be unguessable -- use SecureRandom (or Tina4::Auth for password hashing) instead.

Examples:

fake = Tina4::FakeData.new(seed: 42)
fake.name        # => "Sarah Johnson"
fake.email       # => "sarah.johnson123@example.com"
fake.integer(1, 100)

Constant Summary collapse

FIRST_NAMES =
%w[
  James Mary Robert Patricia John Jennifer Michael Linda David Elizabeth
  William Barbara Richard Susan Joseph Jessica Thomas Sarah Charles Karen
  Christopher Lisa Daniel Nancy Matthew Betty Anthony Margaret Mark Sandra
  Donald Ashley Steven Dorothy Paul Kimberly Andrew Emily Joshua Donna
  Kenneth Michelle Kevin Carol Brian Amanda George Melissa Timothy Deborah
  Ronald Stephanie Edward Rebecca Jason Sharon Jeffrey Laura Ryan Cynthia
  Jacob Kathleen Gary Amy Nicholas Angela Eric Shirley Jonathan Anna
  Stephen Brenda Larry Pamela Justin Emma Scott Nicole Brandon Helen
  Benjamin Samantha Samuel Katherine Raymond Christine Gregory Debra
  Frank Rachel Alexander Carolyn Patrick Janet Jack Catherine Andre Aisha
  Wei Yuki Carlos Fatima Raj Priya Mohammed Sophia Liam Olivia Noah Ava
  Ethan Mia Lucas Isabella Mason Charlotte Logan Amelia Aiden Harper
].freeze
LAST_NAMES =
%w[
  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 Lewis Robinson Walker
  Young Allen King Wright Scott Torres Nguyen Hill Flores Green Adams Nelson
  Baker Hall Rivera Campbell Mitchell Carter Roberts Gomez Phillips Evans
  Turner Diaz Parker Cruz Edwards Collins Reyes Stewart Morris Morales
  Murphy Cook Rogers Gutierrez Ortiz Morgan Cooper Peterson Bailey Reed
  Kelly Howard Ramos Kim Cox Ward Richardson Watson Brooks Chavez Wood
  James Bennett Gray Mendoza Ruiz Hughes Price Alvarez Castillo Sanders
  Patel Müller Nakamura Singh Chen Silva Ali Okafor
].freeze
WORDS =
%w[
  the be to of and a in that have it for not on with he as you do at
  this but his by from they we say her she or an will my one all would
  there their what so up out if about who get which go me when make can
  like time no just him know take people into year your good some could
  them see other than then now look only come its over think also back
  after use two how our work first well way even new want because any
  these give day most us great small large every found still between name
  should home big end along each much both help line turn move thing right
  same old better point long real system data report order product service
  customer account payment record total status market world company project
  team value process business group result information development management
  quality performance technology support research design program network
].freeze
CITIES =
[
  "New York", "London", "Tokyo", "Paris", "Berlin", "Sydney", "Toronto",
  "Mumbai", "São Paulo", "Cairo", "Lagos", "Dubai", "Singapore",
  "Hong Kong", "Seoul", "Mexico City", "Bangkok", "Istanbul", "Moscow",
  "Rome", "Barcelona", "Amsterdam", "Nairobi", "Cape Town", "Johannesburg",
  "Buenos Aires", "Lima", "Santiago", "Jakarta", "Manila", "Kuala Lumpur",
  "Auckland", "Vancouver", "Chicago", "San Francisco", "Los Angeles",
  "Miami", "Boston", "Seattle", "Denver"
].freeze
COUNTRIES =
[
  "United States", "United Kingdom", "Canada", "Australia", "Germany",
  "France", "Japan", "Brazil", "India", "South Africa", "Nigeria",
  "Egypt", "Kenya", "Mexico", "Argentina", "Chile", "Colombia", "Spain",
  "Italy", "Netherlands", "Sweden", "Norway", "Denmark", "Finland",
  "Switzerland", "Belgium", "Austria", "New Zealand", "Singapore",
  "South Korea", "Thailand", "Indonesia", "Philippines", "Vietnam",
  "Malaysia", "United Arab Emirates", "Saudi Arabia", "Turkey", "Poland"
].freeze
DOMAINS =
%w[
  example.com test.org sample.net demo.io mail.com
  inbox.org webmail.net company.com corp.io biz.net
].freeze
STREETS =
%w[Main Oak Pine Maple Cedar Elm Park Lake Hill River Church Market King Queen High].freeze
STREET_TYPES =
%w[Street Avenue Road Drive Lane Boulevard Way Place].freeze
COMPANY_WORDS =
%w[Tech Global Apex Nova Core Prime Next Blue Bright Smart Swift Peak Fusion Pulse Vertex].freeze
COMPANY_SUFFIXES =
%w[Inc Corp Ltd LLC Group Solutions Systems Labs].freeze
JOB_TITLES =
[
  "Software Engineer", "Product Manager", "Designer", "Data Analyst",
  "DevOps Engineer", "CEO", "CTO", "Sales Manager", "Marketing Lead",
  "Accountant", "Operations Manager", "QA Engineer", "UX Researcher",
  "Support Specialist", "HR Manager", "Technical Writer"
].freeze
CURRENCIES =
%w[USD EUR GBP JPY CAD AUD CHF ZAR INR CNY].freeze
CREDIT_CARD_PREFIXES =
%w[4111 4242 5500 5105].freeze
PRODUCT_ADJECTIVES =

Product-name vocabulary (adjective + noun). Seeds a generic +name+/+full_name+ column on a product-ish table with "Wireless Keyboard" instead of a person name. Mirrors the Python master's word banks.

%w[
  Wireless Organic Premium Classic Eco Smart Portable
  Deluxe Compact Rustic Handcrafted Vintage Modern
  Ergonomic Stainless Bamboo Recycled Artisan Professional
  Ultra Insulated Lightweight Adjustable Foldable
].freeze
PRODUCT_NOUNS =
[
  "Keyboard", "Coffee Beans", "Backpack", "Water Bottle", "Desk Lamp",
  "Headphones", "Notebook", "Sneakers", "Sunglasses", "Wallet", "Mug",
  "Chair", "Blender", "Speaker", "Charger", "Umbrella", "Toothbrush",
  "Jacket", "Watch", "Kettle", "Picture Frame", "Planter", "Cutlery Set",
  "Yoga Mat", "Phone Case"
].freeze
PRODUCT_TABLE_HINTS =

A table/model whose name contains any of these gets product names on its generic +name+/+full_name+ column instead of a person name.

%w[
  product item catalog inventory goods merchandise sku listing stock ware
].freeze

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(seed: nil) ⇒ FakeData

Returns a new instance of FakeData.



135
136
137
# File 'lib/tina4/seeder.rb', line 135

def initialize(seed: nil)
  @rng = seed ? Random.new(seed) : Random.new
end

Class Method Details

.product_table?(table) ⇒ Boolean

True when the table/model name looks like a product catalogue, so a generic +name+/+full_name+ column seeds a product name, not a person name. With no table context (nil/"") this is false, so the person-name default is kept (back-compat). Mirrors the Python master's _is_product_table.

Returns:

  • (Boolean)


286
287
288
289
# File 'lib/tina4/seeder.rb', line 286

def self.product_table?(table)
  down = table.to_s.downcase
  PRODUCT_TABLE_HINTS.any? { |hint| down.include?(hint) }
end

.seed(seed) ⇒ Object

Static factory — create a seeded FakeData instance. fake = FakeData.seed(42) fake.name # deterministic



142
143
144
# File 'lib/tina4/seeder.rb', line 142

def self.seed(seed)
  new(seed: seed)
end

Instance Method Details

#addressObject



255
256
257
# File 'lib/tina4/seeder.rb', line 255

def address
  "#{@rng.rand(1..9999)} #{STREETS[@rng.rand(STREETS.length)]} #{STREET_TYPES[@rng.rand(STREET_TYPES.length)]}"
end

#blob(size: 64) ⇒ Object



230
231
232
# File 'lib/tina4/seeder.rb', line 230

def blob(size: 64)
  SecureRandom.random_bytes(size)
end

#booleanObject



211
212
213
# File 'lib/tina4/seeder.rb', line 211

def boolean
  @rng.rand(2) == 1
end

#choice(items) ⇒ Object



243
244
245
# File 'lib/tina4/seeder.rb', line 243

def choice(items)
  items[@rng.rand(items.length)]
end

#cityObject



247
248
249
# File 'lib/tina4/seeder.rb', line 247

def city
  CITIES[@rng.rand(CITIES.length)]
end

#color_hexObject



306
307
308
# File 'lib/tina4/seeder.rb', line 306

def color_hex
  "#%06x" % @rng.rand(0..0xFFFFFF)
end

#companyObject



263
264
265
266
267
268
# File 'lib/tina4/seeder.rb', line 263

def company
  w1 = COMPANY_WORDS[@rng.rand(COMPANY_WORDS.length)]
  w2 = COMPANY_WORDS[@rng.rand(COMPANY_WORDS.length)]
  suffix = COMPANY_SUFFIXES[@rng.rand(COMPANY_SUFFIXES.length)]
  "#{w1}#{w2} #{suffix}"
end

#countryObject



251
252
253
# File 'lib/tina4/seeder.rb', line 251

def country
  COUNTRIES[@rng.rand(COUNTRIES.length)]
end

#credit_cardObject

Generate a fake credit card number (test numbers only, e.g. 4111...).



300
301
302
303
304
# File 'lib/tina4/seeder.rb', line 300

def credit_card
  prefix = CREDIT_CARD_PREFIXES[@rng.rand(CREDIT_CARD_PREFIXES.length)]
  rest = Array.new(12) { @rng.rand(0..9) }.join
  prefix + rest
end

#currencyObject



291
292
293
# File 'lib/tina4/seeder.rb', line 291

def currency
  CURRENCIES[@rng.rand(CURRENCIES.length)]
end

#date(start_year: 2020, end_year: 2026) ⇒ Object



222
223
224
# File 'lib/tina4/seeder.rb', line 222

def date(start_year: 2020, end_year: 2026)
  datetime(start_year: start_year, end_year: end_year).strftime("%Y-%m-%d")
end

#datetime(start_year: 2020, end_year: 2026) ⇒ Object



215
216
217
218
219
220
# File 'lib/tina4/seeder.rb', line 215

def datetime(start_year: 2020, end_year: 2026)
  start_time = Time.new(start_year, 1, 1)
  end_time = Time.new(end_year, 12, 31, 23, 59, 59)
  delta = (end_time - start_time).to_i
  Time.at(start_time.to_i + @rng.rand(0..delta))
end

#email(from_name: nil) ⇒ Object



158
159
160
161
162
163
164
165
166
# File 'lib/tina4/seeder.rb', line 158

def email(from_name: nil)
  if from_name
    local = from_name.downcase.split.join(".")
  else
    local = "#{first_name.downcase}.#{last_name.downcase}"
  end
  local += @rng.rand(1..999).to_s
  "#{local}@#{DOMAINS[@rng.rand(DOMAINS.length)]}"
end

#first_nameObject



146
147
148
# File 'lib/tina4/seeder.rb', line 146

def first_name
  FIRST_NAMES[@rng.rand(FIRST_NAMES.length)]
end

#for_field(field_def, column_name = nil, table = nil) ⇒ Object

Generate appropriate data based on field definition and column name.

table (optional) is the table/model name. When it names a product-ish catalogue (see FakeData.product_table?) a generic +name+/+full_name+/ fullname column yields a product name ("Wireless Keyboard") instead of a person name. With no table context the person-name default is kept (back-compat). first_name/last_name/user_name stay person either way.



332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
# File 'lib/tina4/seeder.rb', line 332

def for_field(field_def, column_name = nil, table = nil)
  col = (column_name || "").to_s.downcase
  type = field_def[:type]

  # Skip auto-increment primary keys
  return nil if field_def[:primary_key] && field_def[:auto_increment]

  case type
  when :integer
    return integer(min: 18, max: 85) if col.include?("age")
    return integer(min: 1950, max: 2026) if col.include?("year")
    return integer(min: 1, max: 100) if col =~ /quantity|qty|count/
    return boolean if col =~ /active|enabled|visible|^is_/
    return integer(min: 1, max: 10) if col =~ /rating|score/
    integer(min: 1, max: 10_000)

  when :float, :decimal
    decimals = field_def[:scale] || 2
    return numeric(min: 0.01, max: 9999.99, decimals: decimals) if col =~ /price|cost|amount|total|fee/
    return numeric(min: 0.0, max: 100.0, decimals: decimals) if col =~ /rate|percent|ratio/
    return numeric(min: -90.0, max: 90.0, decimals: 6) if col.include?("lat")
    return numeric(min: -180.0, max: 180.0, decimals: 6) if col =~ /lon|lng/
    numeric(min: 0.0, max: 10_000.0, decimals: decimals)

  when :date
    date

  when :datetime, :timestamp
    timestamp

  when :boolean
    boolean

  when :blob
    blob

  when :json
    json_data

  when :string, :text
    max_len = field_def[:length] || 255
    val = generate_string_for(col, max_len, table)
    val.length > max_len ? val[0...max_len] : val

  else
    word
  end
end

#integer(min: 0, max: 10_000) ⇒ Object



202
203
204
# File 'lib/tina4/seeder.rb', line 202

def integer(min: 0, max: 10_000)
  @rng.rand(min..max)
end

#ip_addressObject



295
296
297
# File 'lib/tina4/seeder.rb', line 295

def ip_address
  "#{@rng.rand(1..255)}.#{@rng.rand(0..255)}.#{@rng.rand(0..255)}.#{@rng.rand(1..254)}"
end

#job_titleObject



270
271
272
# File 'lib/tina4/seeder.rb', line 270

def job_title
  JOB_TITLES[@rng.rand(JOB_TITLES.length)]
end

#json_data(keys: nil) ⇒ Object



234
235
236
237
238
239
240
241
# File 'lib/tina4/seeder.rb', line 234

def json_data(keys: nil)
  if keys
    keys.each_with_object({}) { |k, h| h[k] = word }
  else
    n = @rng.rand(2..5)
    n.times.each_with_object({}) { |_, h| h[word] = word }
  end
end

#last_nameObject



150
151
152
# File 'lib/tina4/seeder.rb', line 150

def last_name
  LAST_NAMES[@rng.rand(LAST_NAMES.length)]
end

#nameObject



154
155
156
# File 'lib/tina4/seeder.rb', line 154

def name
  "#{first_name} #{last_name}"
end

#numeric(min: 0.0, max: 1000.0, decimals: 2) ⇒ Object



206
207
208
209
# File 'lib/tina4/seeder.rb', line 206

def numeric(min: 0.0, max: 1000.0, decimals: 2)
  val = min + @rng.rand * (max - min)
  val.round(decimals)
end

#paragraph(sentences: 3) ⇒ Object



181
182
183
# File 'lib/tina4/seeder.rb', line 181

def paragraph(sentences: 3)
  Array.new(sentences) { sentence(words: @rng.rand(5..12)) }.join(" ")
end

#password(length: 16) ⇒ Object



315
316
317
318
# File 'lib/tina4/seeder.rb', line 315

def password(length: 16)
  chars = [*"a".."z", *"A".."Z", *"0".."9"]
  Array.new(length) { chars[@rng.rand(chars.length)] }.join
end

#phoneObject



168
169
170
171
172
173
# File 'lib/tina4/seeder.rb', line 168

def phone
  area = @rng.rand(200..999)
  mid = @rng.rand(100..999)
  tail = @rng.rand(1000..9999)
  "+1 (#{area}) #{mid}-#{tail}"
end

#productObject

A plausible product name, e.g. "Wireless Keyboard" or "Organic Coffee Beans". Deterministic under a seed like every other generator (draws from the same @rng), so a seeded run reproduces it. Mirrors the Python master's FakeData.product().



278
279
280
# File 'lib/tina4/seeder.rb', line 278

def product
  "#{PRODUCT_ADJECTIVES[@rng.rand(PRODUCT_ADJECTIVES.length)]} #{PRODUCT_NOUNS[@rng.rand(PRODUCT_NOUNS.length)]}"
end

#run(count = 1, &block) ⇒ Object

Run a generator block count times and return the results.



321
322
323
# File 'lib/tina4/seeder.rb', line 321

def run(count = 1, &block)
  Array.new(count) { block.call }
end

#sentence(words: 6) ⇒ Object



175
176
177
178
179
# File 'lib/tina4/seeder.rb', line 175

def sentence(words: 6)
  w = Array.new(words) { WORDS[@rng.rand(WORDS.length)] }
  w[0] = w[0].capitalize
  "#{w.join(' ')}."
end

#slug(words: 3) ⇒ Object



194
195
196
# File 'lib/tina4/seeder.rb', line 194

def slug(words: 3)
  Array.new(words) { WORDS[@rng.rand(WORDS.length)] }.join("-")
end

#text(max_length: 200) ⇒ Object



185
186
187
188
# File 'lib/tina4/seeder.rb', line 185

def text(max_length: 200)
  t = paragraph(sentences: 2)
  t.length > max_length ? t[0...max_length] : t
end

#timestamp(start_year: 2020, end_year: 2026) ⇒ Object



226
227
228
# File 'lib/tina4/seeder.rb', line 226

def timestamp(start_year: 2020, end_year: 2026)
  datetime(start_year: start_year, end_year: end_year).strftime("%Y-%m-%d %H:%M:%S")
end

#urlObject



198
199
200
# File 'lib/tina4/seeder.rb', line 198

def url
  "https://#{DOMAINS[@rng.rand(DOMAINS.length)]}/#{slug}"
end

#uuidObject



310
311
312
313
# File 'lib/tina4/seeder.rb', line 310

def uuid
  h = Array.new(32) { "0123456789abcdef"[@rng.rand(16)] }.join
  "#{h[0..7]}-#{h[8..11]}-#{h[12..15]}-#{h[16..19]}-#{h[20..31]}"
end

#wordObject



190
191
192
# File 'lib/tina4/seeder.rb', line 190

def word
  WORDS[@rng.rand(WORDS.length)]
end

#zip_codeObject



259
260
261
# File 'lib/tina4/seeder.rb', line 259

def zip_code
  @rng.rand(10_000..99_999).to_s
end