Class: Synthra::Generator::Engine

Inherits:
Object
  • Object
show all
Defined in:
lib/synthra/generator/engine.rb

Overview

Core generation engine

The Engine orchestrates the data generation process, handling:

  • Field iteration and value generation
  • Type lookup and invocation
  • Uniqueness constraint enforcement
  • Optional and conditional field handling
  • Field-level behavior application
  • Deterministic seeding via RNG

Examples:

Generate a single record

engine = Engine.new(schema)
user = engine.generate(seed: 42, mode: :random)

Generate multiple records

users = engine.generate_many(100, seed: 42)

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(schema, registry: nil, resolver: nil) ⇒ Engine

Create a new Engine instance

Examples:

engine = Engine.new(user_schema, registry: schema_registry)

Parameters:

  • schema (Schema)

    the schema to generate from

  • registry (Registry, nil) (defaults to: nil)

    optional registry for Ref() lookups

  • resolver (Resolver, nil) (defaults to: nil)

    optional resolver instance to reuse (for cycle detection)



79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
# File 'lib/synthra/generator/engine.rb', line 79

def initialize(schema, registry: nil, resolver: nil)
  @schema = schema
  @registry = registry
  @rng = nil
  @faker_adapter = nil
  @uniqueness = Uniqueness.new(schema_name: schema.name)

  # Use provided resolver or create new one if registry is available
  # Reusing resolver ensures @generating set is shared for cycle detection
  @resolver = resolver || (registry ? Resolver.new(registry) : nil)

  # Pre-compute observability flag at initialization to avoid repeated
  # config lookups in the hot path (called once per Engine, not per record)
  @observability_enabled = Synthra.configuration.observability_enabled?
end

Instance Attribute Details

#registryObject (readonly)

Returns the value of attribute registry.



66
67
68
# File 'lib/synthra/generator/engine.rb', line 66

def registry
  @registry
end

#rngObject (readonly)

Returns the value of attribute rng.



59
60
61
# File 'lib/synthra/generator/engine.rb', line 59

def rng
  @rng
end

#schemaObject (readonly)

Returns the value of attribute schema.



52
53
54
# File 'lib/synthra/generator/engine.rb', line 52

def schema
  @schema
end

Instance Method Details

#emit_observability(config, field, value, duration_ms) ⇒ Object (private)



275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
# File 'lib/synthra/generator/engine.rb', line 275

def emit_observability(config, field, value, duration_ms)
  if config.metrics_collector
    config.metrics_collector.timing(
      "synthra.field_generation",
      duration_ms,
      schema: @schema.name,
      field: field.name,
      type: field.type_name
    )
  end

  config.logger&.debug(
    "Generated field #{@schema.name}.#{field.name} (#{field.type_name}) in #{duration_ms}ms"
  )

  config.on_field_generated&.call(@schema.name, field.name, value, duration_ms)
end

#generate(seed: nil, mode: :random, overrides: {}, depth: 0, parent_context: nil, shared_context: nil) ⇒ Hash

Generate a single record

Creates one fake data record based on the schema definition. Use the seed parameter for deterministic/reproducible output.

Examples:

Basic generation

record = engine.generate

Deterministic generation

record = engine.generate(seed: 12345)

With overrides

record = engine.generate(overrides: { "name" => "Test User" })

Parameters:

  • seed (Integer, nil) (defaults to: nil)

    random seed for deterministic output

  • mode (Symbol) (defaults to: :random)

    generation mode (:random, :edge, :invalid, :mixed)

  • overrides (Hash) (defaults to: {})

    field values to use instead of generating

Returns:

  • (Hash)

    the generated record



116
117
118
119
120
121
# File 'lib/synthra/generator/engine.rb', line 116

def generate(seed: nil, mode: :random, overrides: {}, depth: 0, parent_context: nil, shared_context: nil)

  # Reset RNG if seed is provided, or create one if none exists
  setup_rng(seed) if seed || @rng.nil?
  generate_single(mode: mode, overrides: overrides, depth: depth, parent_context: parent_context, shared_context: shared_context)
end

#generate_field(field, context, mode) ⇒ Object (private)

Generate a single field value

Parameters:

  • field (Field)

    the field definition

  • context (Context)

    current generation context

  • mode (Symbol)

    generation mode

Returns:

  • (Object)

    generated value



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
333
# File 'lib/synthra/generator/engine.rb', line 302

def generate_field(field, context, mode)
  type_name = field.type_name
  type_args = field.type_args


  # Look up the type generator - first try Types::Registry
  type_gen = begin
    Types::Registry.lookup(type_name)
  rescue Errors::UnknownTypeError
    # If not a registered type, check if it's a schema reference
    if @registry && @registry.schema?(type_name)
      # Create an ObjectType for the schema reference
      Types::Core::ObjectType.new(type_name)
    else
      raise  # Re-raise the UnknownTypeError if not a schema either
    end
  end


  # Pass faker_adapter in context for type generators to use
  context.faker_adapter = @faker_adapter if @faker_adapter


  # Handle uniqueness constraint
  if field.unique?
    @uniqueness.generate_unique(field.name) do
      type_gen.generate(@rng, context, type_args, mode)
    end
  else
    type_gen.generate(@rng, context, type_args, mode)
  end
end

#generate_many(count, seed: nil, mode: :random, overrides: {}, shared_context: nil) ⇒ Array<Hash>

Note:

For large batches (10K+ records), consider using generate_many_stream for memory-efficient lazy generation.

Generate multiple records

Creates multiple fake data records efficiently. Uniqueness constraints are enforced across the entire batch.

Examples:

Generate 100 users

users = engine.generate_many(100)

Deterministic batch

users = engine.generate_many(100, seed: 42)

Parameters:

  • count (Integer)

    number of records to generate

  • seed (Integer, nil) (defaults to: nil)

    random seed for deterministic output

  • mode (Symbol) (defaults to: :random)

    generation mode

  • overrides (Hash) (defaults to: {})

    field values to override in all records

Returns:

  • (Array<Hash>)

    array of generated records



145
146
147
148
149
150
151
152
153
154
155
# File 'lib/synthra/generator/engine.rb', line 145

def generate_many(count, seed: nil, mode: :random, overrides: {}, shared_context: nil)
  setup_rng(seed)
  @uniqueness.clear  # Reset uniqueness tracking for new batch

  # Create shared context for batch - values persist across all records
  shared_context ||= {}

  count.times.map do
    generate_single(mode: mode, overrides: overrides, depth: 0, shared_context: shared_context)
  end
end

#generate_many_stream(count, seed: nil, mode: :random, overrides: {}, shared_context: nil) ⇒ Enumerator

Generate multiple records as a lazy stream (Enumerator)

Returns an Enumerator that generates records on-demand. This is memory-efficient for large batches as records are not all held in memory at once.

Examples:

Generate 10K users efficiently

engine.generate_many_stream(10000, seed: 42).each do |user|
  process(user)  # Records generated one at a time
end

Parameters:

  • count (Integer)

    number of records to generate

  • seed (Integer, nil) (defaults to: nil)

    random seed for deterministic output

  • mode (Symbol) (defaults to: :random)

    generation mode

  • overrides (Hash) (defaults to: {})

    field values to override in all records

Returns:

  • (Enumerator)

    lazy enumerator of records



176
177
178
179
180
181
182
183
184
185
186
187
188
# File 'lib/synthra/generator/engine.rb', line 176

def generate_many_stream(count, seed: nil, mode: :random, overrides: {}, shared_context: nil)
  setup_rng(seed)
  @uniqueness.clear  # Reset uniqueness tracking for new batch

  # Create shared context for batch - values persist across all records
  shared_context ||= {}

  Enumerator.new(count) do |yielder|
    count.times do
      yielder << generate_single(mode: mode, overrides: overrides, depth: 0, shared_context: shared_context)
    end
  end
end

#generate_single(mode:, overrides:, depth: 0, parent_context: nil, shared_context: nil) ⇒ Hash (private)

Generate a single record (internal)

Parameters:

  • mode (Symbol)

    generation mode

  • overrides (Hash)

    field overrides

  • depth (Integer) (defaults to: 0)

    current recursion depth

  • shared_context (Hash) (defaults to: nil)

    shared values that persist across batch

Returns:

  • (Hash)

    generated record



214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
# File 'lib/synthra/generator/engine.rb', line 214

def generate_single(mode:, overrides:, depth: 0, parent_context: nil, shared_context: nil)
  # Cache config locally to avoid repeated method calls
  config = Synthra.configuration
  presence_rate = config.optional_field_presence_rate || Synthra::OPTIONAL_FIELD_PRESENCE_RATE
  null_rate = config.nullable_field_null_rate || Synthra::NULLABLE_FIELD_NULL_RATE

  # Create context with proper initialization
  context = Context.new(
    {},
    registry: @registry,
    depth: depth,
    parent_context: parent_context,
    shared_context: shared_context || {},
    resolver: @resolver,
    faker_adapter: @faker_adapter
  )

  result = {}
  applicator = Behaviors::Applicator.new(@schema, @rng)

  # Iterate fields in definition order (important for copy() to work)
  @schema.fields.each do |field|
    # Skip if override provided
    if overrides.key?(field.name) || overrides.key?(field.name.to_sym)
      result[field.name] = overrides[field.name] || overrides[field.name.to_sym]
      context[field.name] = result[field.name]
      next
    end

    # Handle conditional fields
    if field.conditional?
      next unless context[field.condition]
    end

    # Handle optional fields
    next if field.optional? && !@rng.boolean(presence_rate)

    # Generate the field value with optional timing
    start_time = @observability_enabled ? Process.clock_gettime(Process::CLOCK_MONOTONIC) : nil
    value = generate_field(field, context, mode)

    # Apply field-level behaviors
    value = applicator.apply_field_behaviors(field, value)
    next if value == :omit

    # Handle nullable fields
    value = nil if field.nullable? && @rng.boolean(null_rate)

    # Observability hooks (only if enabled - checked once at init)
    if @observability_enabled
      duration_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time) * 1000).round
      emit_observability(config, field, value, duration_ms)
    end

    result[field.name] = value
    context[field.name] = value
  end

  result
end

#setup_rng(seed) ⇒ void (private)

This method returns an undefined value.

Setup RNG and FakerAdapter for deterministic output

Parameters:

  • seed (Integer, nil)

    seed value (nil generates random seed)



199
200
201
202
# File 'lib/synthra/generator/engine.rb', line 199

def setup_rng(seed)
  @rng = RNG.new(seed)
  @faker_adapter = FakerAdapter.new(@rng)
end