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

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(seed: nil) ⇒ FakeData

Returns a new instance of FakeData.



111
112
113
# File 'lib/tina4/seeder.rb', line 111

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

Class Method Details

.seed(seed) ⇒ Object

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



118
119
120
# File 'lib/tina4/seeder.rb', line 118

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

Instance Method Details

#addressObject



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

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

#blob(size: 64) ⇒ Object



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

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

#booleanObject



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

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

#choice(items) ⇒ Object



219
220
221
# File 'lib/tina4/seeder.rb', line 219

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

#cityObject



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

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

#color_hexObject



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

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

#companyObject



239
240
241
242
243
244
# File 'lib/tina4/seeder.rb', line 239

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



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

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

#credit_cardObject

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



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

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



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

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

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



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

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



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

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



134
135
136
137
138
139
140
141
142
# File 'lib/tina4/seeder.rb', line 134

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



122
123
124
# File 'lib/tina4/seeder.rb', line 122

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

#for_field(field_def, column_name = nil) ⇒ Object

Generate appropriate data based on field definition and column name.



285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
# File 'lib/tina4/seeder.rb', line 285

def for_field(field_def, column_name = 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)
    val.length > max_len ? val[0...max_len] : val

  else
    word
  end
end

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



178
179
180
# File 'lib/tina4/seeder.rb', line 178

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

#ip_addressObject



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

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

#job_titleObject



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

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

#json_data(keys: nil) ⇒ Object



210
211
212
213
214
215
216
217
# File 'lib/tina4/seeder.rb', line 210

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



126
127
128
# File 'lib/tina4/seeder.rb', line 126

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

#nameObject



130
131
132
# File 'lib/tina4/seeder.rb', line 130

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

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



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

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

#paragraph(sentences: 3) ⇒ Object



157
158
159
# File 'lib/tina4/seeder.rb', line 157

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

#password(length: 16) ⇒ Object



274
275
276
277
# File 'lib/tina4/seeder.rb', line 274

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

#phoneObject



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

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

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

Run a generator block count times and return the results.



280
281
282
# File 'lib/tina4/seeder.rb', line 280

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

#sentence(words: 6) ⇒ Object



151
152
153
154
155
# File 'lib/tina4/seeder.rb', line 151

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



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

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

#text(max_length: 200) ⇒ Object



161
162
163
164
# File 'lib/tina4/seeder.rb', line 161

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



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

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



174
175
176
# File 'lib/tina4/seeder.rb', line 174

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

#uuidObject



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

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



166
167
168
# File 'lib/tina4/seeder.rb', line 166

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

#zip_codeObject



235
236
237
# File 'lib/tina4/seeder.rb', line 235

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