Class: Synthra::Registry

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

Overview

Registry for storing and managing multiple schemas

The Registry provides a central store for schema definitions, enabling:

  • Loading multiple DSL files
  • Cross-schema references (Ref(User.id))
  • Schema lookup by name
  • Enumeration of all schemas

When generating data with cross-schema references, pass the registry to the generate method to enable reference resolution.

Examples:

Basic usage

registry = Synthra::Registry.new
registry.load_file("schemas/user.dsl")

user_schema = registry.schema("User")
user = user_schema.generate

Multiple schemas with references

registry = Synthra::Registry.new
registry.load_file("schemas/user.dsl")      # User: id, name, ...
registry.load_file("schemas/order.dsl")     # Order: user_id: Ref(User.id)

order = registry.schema("Order").generate(registry: registry)
# order["user_id"] will be a generated User.id

Iterating over schemas

registry.each do |name, schema|
  puts "Schema: #{name} with #{schema.fields.length} fields"
end

Instance Method Summary collapse

Constructor Details

#initializeRegistry

Create a new empty registry

Examples:

registry = Synthra::Registry.new
registry.size  # => 0


68
69
70
71
72
73
74
# File 'lib/synthra/registry.rb', line 68

def initialize
  @schemas = {}
  @mutex = Mutex.new
  @ast_cache = {}
  @cache_mutex = Mutex.new
  @max_cache_size = Synthra.configuration.registry_cache_size
end

Instance Method Details

#clearvoid

This method returns an undefined value.

Clear all registered schemas

Removes all schemas from the registry.

Examples:

registry.clear
registry.size  # => 0


418
419
420
421
422
423
424
425
# File 'lib/synthra/registry.rb', line 418

def clear
  @mutex.synchronize do
    @schemas.clear
    @cache_mutex.synchronize do
      @ast_cache.clear
    end
  end
end

#each {|name, schema| ... } ⇒ Enumerator, void

Iterate over all schemas

Yields each schema name and schema object to the block.

Examples:

registry.each do |name, schema|
  puts "#{name}: #{schema.fields.length} fields"
end

Get as array

pairs = registry.each.to_a
# => [["User", #<Schema>], ["Order", #<Schema>]]

Yields:

  • (name, schema)

    block to execute for each schema

Yield Parameters:

  • name (String)

    the schema name

  • schema (Schema)

    the schema object

Returns:

  • (Enumerator, void)

    returns Enumerator if no block given



473
474
475
476
477
# File 'lib/synthra/registry.rb', line 473

def each(&block)
  @mutex.synchronize do
    @schemas.each(&block)
  end
end

#extract_schema_nodes(parse_result) ⇒ Array<SchemaNode> (private)

Extract schema nodes from parse result Handles both legacy (Array) and Simulyra (ProgramNode) syntax

Parameters:

  • parse_result (Array, ProgramNode)

    parsed result

Returns:

  • (Array<SchemaNode>)

    array of schema nodes



291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
# File 'lib/synthra/registry.rb', line 291

def extract_schema_nodes(parse_result)
  if parse_result.is_a?(Parser::AST::ProgramNode)
    # Simulyra DSL syntax - combine all schemas
    all_schemas = []
    all_schemas.concat(parse_result.schemas) if parse_result.schemas
    all_schemas.concat(parse_result.legacy_schemas) if parse_result.legacy_schemas
    all_schemas
  elsif parse_result.is_a?(Array)
    # Legacy syntax - already an array of schema nodes
    parse_result
  else
    # Single node - wrap in array
    [parse_result].compact
  end
end

#load_dir(dir, validate: Synthra.configuration.validate_paths_on_load) ⇒ Array<Schema>

Load schemas from all DSL files in a directory

Loads and parses all .dsl files in the specified directory, adding all schemas defined in them to the registry. Existing schemas with the same name will be overwritten.

Examples:

registry.load_dir("schemas/")
# => [#<Schema User>, #<Schema Order>, #<Schema Product>]

Handle errors

begin
  registry.load_dir("nonexistent/")
rescue Errno::ENOENT => e
  puts "Directory not found: #{e.message}"
end

Skip validation

registry.load_dir("schemas/", validate: false)

Parameters:

  • dir (String)

    directory path containing .dsl files

  • validate (Boolean) (defaults to: Synthra.configuration.validate_paths_on_load)

    whether to validate copy() paths (default: config setting)

Returns:

  • (Array<Schema>)

    all schemas that were loaded

Raises:



207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
# File 'lib/synthra/registry.rb', line 207

def load_dir(dir, validate: Synthra.configuration.validate_paths_on_load)
  schemas = @mutex.synchronize do

    # Validate and expand directory path
    dir = File.expand_path(dir)
    raise Errno::ENOENT, dir unless Dir.exist?(dir)


    # Find all .dsl files
    pattern = File.join(dir, "*.dsl")
    files = Dir.glob(pattern)


    # Load each file
    all_schemas = []
    files.each do |file|
      # Call load_file_unsafe to avoid double synchronization (we're already in @mutex)
      all_schemas.concat(load_file_unsafe(file))
    end

    all_schemas
  end

  # Validate paths outside mutex to avoid deadlock
  validate_paths! if validate

  schemas
end

#load_file(path, validate: Synthra.configuration.validate_paths_on_load) ⇒ Array<Schema>

Load schemas from a DSL file

Reads and parses the DSL file, adding all schemas defined in it to the registry. Existing schemas with the same name will be overwritten.

Examples:

registry.load_file("schemas/user.dsl")
# => [#<Schema User fields=5 behaviors=0>]

Handle errors

begin
  registry.load_file("invalid.dsl")
rescue Synthra::ParseError => e
  puts "Error at line #{e.line}: #{e.message}"
end

Skip validation

registry.load_file("schema.dsl", validate: false)

Parameters:

  • path (String)

    path to the DSL file (relative or absolute)

  • validate (Boolean) (defaults to: Synthra.configuration.validate_paths_on_load)

    whether to validate copy() paths (default: config setting)

Returns:

  • (Array<Schema>)

    the schemas that were loaded

Raises:



106
107
108
109
110
111
112
113
114
115
# File 'lib/synthra/registry.rb', line 106

def load_file(path, validate: Synthra.configuration.validate_paths_on_load)
  schemas = @mutex.synchronize do
    load_file_unsafe(path)
  end

  # Validate paths outside mutex to avoid deadlock
  validate_paths! if validate

  schemas
end

#load_file_unsafe(path) ⇒ Object (private)



119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
# File 'lib/synthra/registry.rb', line 119

def load_file_unsafe(path)

  # Check cache first
  cache_key = File.expand_path(path)
  mtime = File.mtime(path) if File.exist?(path)
  
  cached = @cache_mutex.synchronize do
    cache_entry = @ast_cache[cache_key]
    if cache_entry && cache_entry[:mtime] == mtime

      # Update access time for LRU tracking
      cache_entry[:accessed_at] = Time.now
      cache_entry[:ast_nodes]
    else
      nil
    end
  end

  if cached

    # Use cached AST. Normalize through extract_schema_nodes so the new Simulyra
    # (ProgramNode) syntax works here too — load_string already does this; load_file
    # used to blindly #map the raw parse result and broke on every v2 .dsl file.
    extract_schema_nodes(cached).map do |node|
      schema = node.to_schema
      @schemas[schema.name] = schema
      schema
    end
  else

    # Parse and cache
    content = File.read(path)
    parser = Parser::Parser.new
    ast_nodes = parser.parse(content)


    # Cache the AST with LRU eviction
    @cache_mutex.synchronize do

      # Limit cache size using LRU (Least Recently Used) eviction
      if @ast_cache.size >= @max_cache_size

        # Remove least recently used entry
        oldest_key = @ast_cache.min_by { |_, v| v[:accessed_at] || Time.at(0) }[0]
        @ast_cache.delete(oldest_key)
      end
      @ast_cache[cache_key] = { ast_nodes: ast_nodes, mtime: mtime, accessed_at: Time.now }
    end

    extract_schema_nodes(ast_nodes).map do |node|
      schema = node.to_schema
      @schemas[schema.name] = schema
      schema
    end
  end
end

#load_string(source, validate: Synthra.configuration.validate_paths_on_load) ⇒ Array<Schema>

Load schemas from a DSL string

Parses the DSL source code and adds all schemas to the registry. Useful for testing or when DSL content is dynamically generated.

Examples:

dsl = <<~DSL
  User:
    id: uuid
    name: name
DSL

schemas = registry.load_string(dsl)
schemas.first.name  # => "User"

Skip validation

schemas = registry.load_string(dsl, validate: false)

Parameters:

  • source (String)

    DSL source code

  • validate (Boolean) (defaults to: Synthra.configuration.validate_paths_on_load)

    whether to validate copy() paths (default: config setting)

Returns:

  • (Array<Schema>)

    the schemas that were parsed

Raises:



262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
# File 'lib/synthra/registry.rb', line 262

def load_string(source, validate: Synthra.configuration.validate_paths_on_load)
  schemas = @mutex.synchronize do
    parser = Parser::Parser.new
    parse_result = parser.parse(source)

    # Handle both legacy (array of SchemaNode) and Simulyra (ProgramNode) syntax
    ast_nodes = extract_schema_nodes(parse_result)

    ast_nodes.map do |node|
      schema = node.to_schema
      @schemas[schema.name] = schema
      schema
    end
  end

  # Validate paths outside mutex to avoid deadlock
  validate_paths! if validate

  schemas
end

#namesArray<String>

Get all schema names

Examples:

registry.names  # => ["User", "Order", "Product"]

Returns:

  • (Array<String>)

    list of schema names



488
489
490
491
492
# File 'lib/synthra/registry.rb', line 488

def names
  @mutex.synchronize do
    @schemas.keys
  end
end

#register_schema(name, schema) ⇒ Schema

Register an already-built Schema object under a name.

Counterpart to load_string/load_file for callers that construct a Schema directly (OpenAPI import, ActiveRecord inference, the CLI/API server). Was referenced in those paths but never defined, so every one of them raised NoMethodError.

Parameters:

  • name (String, Symbol)

    the schema name

  • schema (Schema)

    the schema to register

Returns:

  • (Schema)

    the registered schema



348
349
350
351
352
# File 'lib/synthra/registry.rb', line 348

def register_schema(name, schema)
  @mutex.synchronize do
    @schemas[name.to_s] = schema
  end
end

#schema(name) ⇒ Schema

Get a schema by name

Retrieves a schema from the registry. Raises KeyError if not found.

Examples:

user_schema = registry.schema("User")
user_schema.fields.length  # => 5

Handle missing schema

begin
  registry.schema("NonExistent")
rescue KeyError => e
  puts e.message  # => "Schema not found: NonExistent"
end

Parameters:

  • name (String, Symbol)

    schema name

Returns:

Raises:

  • (KeyError)

    if schema is not found



330
331
332
333
334
335
336
# File 'lib/synthra/registry.rb', line 330

def schema(name)
  @mutex.synchronize do
    @schemas.fetch(name.to_s) do
      raise KeyError, "Schema not found: #{name}"
    end
  end
end

#schema?(name) ⇒ Boolean

Check if a schema exists

Examples:

registry.schema?("User")    # => true
registry.schema?("Unknown") # => false

Parameters:

  • name (String, Symbol)

    schema name to check

Returns:

  • (Boolean)

    true if the schema exists



383
384
385
386
387
# File 'lib/synthra/registry.rb', line 383

def schema?(name)
  @mutex.synchronize do
    @schemas.key?(name.to_s)
  end
end

#schemasHash<String, Schema>

Get all registered schemas as a hash

Returns a copy of the internal schemas hash to prevent modification.

Examples:

all_schemas = registry.schemas
all_schemas.keys  # => ["User", "Order"]

Returns:

  • (Hash<String, Schema>)

    schema name => Schema mapping



366
367
368
369
370
# File 'lib/synthra/registry.rb', line 366

def schemas
  @mutex.synchronize do
    @schemas.dup
  end
end

#sizeInteger Also known as: count, length

Get the number of registered schemas

Examples:

registry.load_file("user.dsl")
registry.size  # => 1

Returns:

  • (Integer)

    number of schemas in the registry



437
438
439
440
441
# File 'lib/synthra/registry.rb', line 437

def size
  @mutex.synchronize do
    @schemas.size
  end
end

#unregister(name) ⇒ Schema?

Unregister a schema by name

Removes a single schema from the registry.

Examples:

registry.unregister("User")

Parameters:

  • name (String)

    schema name to remove

Returns:

  • (Schema, nil)

    the removed schema, or nil if not found



400
401
402
403
404
# File 'lib/synthra/registry.rb', line 400

def unregister(name)
  @mutex.synchronize do
    @schemas.delete(name.to_s)
  end
end

#validate_pathsArray<Errors::PathError>

Validate all copy() paths in the registry

Checks that all copy() type references point to valid paths in the schema structure. This catches typos and invalid references at load time rather than at generation time.

Examples:

errors = registry.validate_paths
errors.each { |e| puts e.message }

Returns:



508
509
510
511
512
513
# File 'lib/synthra/registry.rb', line 508

def validate_paths
  # Don't hold mutex during validation - the validator caches schemas
  # to avoid deadlock
  validator = Validator::PathValidator.new(self)
  validator.validate
end

#validate_paths!true

Validate all copy() paths, raising on first error

Examples:

registry.load_file("schema.dsl")
registry.validate_paths!  # Raises if copy() paths are invalid

Returns:

  • (true)

    if all paths are valid

Raises:



526
527
528
529
530
531
# File 'lib/synthra/registry.rb', line 526

def validate_paths!
  # Don't hold mutex during validation - the validator caches schemas
  # to avoid deadlock
  validator = Validator::PathValidator.new(self)
  validator.validate!
end