Class: Lutaml::Model::TransformationRegistry

Inherits:
Object
  • Object
show all
Defined in:
lib/lutaml/model/transformation_registry.rb

Overview

Registry for managing transformation lifecycle and caching.

This registry is the single source of truth for all transformation instances and resolved mappings. It provides:

  1. Centralized caching - both transformations and mappings

  2. Thread safety - Mutex-protected access

  3. Cycle detection - handles self-referential models (e.g., Address.address: Address)

  4. Single responsibility - manages transformation lifecycle only

  5. Open/Closed - extensible via builder registration

Architecture layers:

  • Configuration (Model classes): Define mappings (stateless DSL)

  • Compilation (TransformationBuilder): Build transformation (stateless) <- NEW

  • Registry (TransformationRegistry): Cache and lifecycle (stateful service) <- THIS

  • Execution (Transformation): Serialize data (stateful instances)

Examples:

Adding a custom format builder

class ProtobufBuilder < TransformationBuilder
  def self.build(model_class, mapping, format, register)
    Protobuf::Transformation.new(model_class, mapping, format, register)
  end
end

TransformationRegistry.register_builder(:protobuf, ProtobufBuilder)

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeTransformationRegistry

Returns a new instance of TransformationRegistry.



91
92
93
94
95
96
97
98
99
100
101
# File 'lib/lutaml/model/transformation_registry.rb', line 91

def initialize
  @transformations = {} # Cache for transformation instances (mutex-protected for cycle detection)
  @mappings = if RuntimeCompatibility.opal?
                {}
              else
                Concurrent::Map.new
              end
  @mutex = Mutex.new
  @building_threads = {} # Track which thread is building each transformation
  @condition = ConditionVariable.new
end

Class Method Details

.builder_for(format) ⇒ Class?

Get the registered builder for a format.

Parameters:

  • format (Symbol)

    The format

Returns:

  • (Class, nil)

    The builder class or nil



75
76
77
78
# File 'lib/lutaml/model/transformation_registry.rb', line 75

def builder_for(format)
  @builders ||= {}
  @builders[format]
end

.instanceObject

Get singleton instance



37
38
39
# File 'lib/lutaml/model/transformation_registry.rb', line 37

def instance
  @instance ||= new
end

.register_builder(format, builder) ⇒ void

This method returns an undefined value.

Register a builder for a format.

This allows extending TransformationRegistry with new formats without modifying its code (Open/Closed Principle).

Builders are registered at load time by each format module:

  • XML: registered by lib/lutaml/xml.rb

  • KeyValue formats: registered by lib/lutaml/key_value.rb

Examples:

TransformationRegistry.register_builder(:protobuf, ProtobufBuilder)

Parameters:

  • format (Symbol)

    The format to register (e.g., :protobuf)

  • builder (Class)

    A class that inherits from TransformationBuilder



56
57
58
59
60
61
62
63
64
65
66
67
68
69
# File 'lib/lutaml/model/transformation_registry.rb', line 56

def register_builder(format, builder)
  unless builder < TransformationBuilder
    raise ArgumentError,
          "Builder must inherit from TransformationBuilder"
  end

  @builders ||= {}
  @builders[format] = builder

  # Track load-time registrations so reset can restore them
  # Only record the first registration per format as the default
  @default_builders ||= {}
  @default_builders[format] ||= builder
end

.reset_builders!void

This method returns an undefined value.

Reset builders to load-time defaults (useful for testing)

Restores the builders to the set registered at load time, removing any test-specific overrides.



86
87
88
# File 'lib/lutaml/model/transformation_registry.rb', line 86

def reset_builders!
  @builders = (@default_builders || {}).dup
end

Instance Method Details

#clearObject

Clear all cached data (transformations and mappings).

This is useful for testing or when configuration changes dynamically.



199
200
201
202
203
204
# File 'lib/lutaml/model/transformation_registry.rb', line 199

def clear
  @mutex.synchronize do
    @transformations.clear
  end
  @mappings.clear
end

#get_or_build_mapping(model_class, format, register) ⇒ Mapping

Get or build resolved mapping for a model class and format.

This method caches the resolved mapping (either from mappings or from default_mappings(format)).

CRITICAL: Ensures mappings are imported before returning, which handles deferred symbol-based imports (e.g., import_model :SomeModel).

NOTE: Building is done OUTSIDE the mutex to avoid deadlock when ensure_mappings_imported! recursively calls mappings_for.

Parameters:

  • model_class (Class)

    The model class

  • format (Symbol)

    The format

  • register (Symbol, Register, nil)

    The register

Returns:

  • (Mapping)

    The resolved mapping



176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
# File 'lib/lutaml/model/transformation_registry.rb', line 176

def get_or_build_mapping(model_class, format, register)
  # Performance: Use fast array key instead of symbol construction
  register_id = extract_register_id(register)
  key = [model_class.object_id, format, register_id]

  # Fast path: native Ruby uses Concurrent::Map, Opal uses Hash.
  cached = @mappings[key]
  return cached if cached

  # Build mapping OUTSIDE any lock to avoid deadlock
  # (ensure_mappings_imported! may recursively call get_or_build_mapping)
  mapping = model_class.mappings[format]
  mapping = mapping || model_class.send(:default_mappings, format)

  mapping.ensure_mappings_imported!(register_id)

  @mappings[key] = mapping
  mapping
end

#get_or_build_transformation(model_class, format, register) ⇒ Transformation?

Get or build transformation for a model class and format.

This method provides cycle detection for self-referential models. When a transformation is being built, it’s marked with :building to prevent infinite recursion.

THREAD SAFETY: Uses ConditionVariable to wait for in-progress builds from other threads, while still detecting cycles within the same thread.

Parameters:

  • model_class (Class)

    The model class (e.g., Person, Address)

  • format (Symbol)

    The format (:xml, :json, :yaml, :hash, :toml)

  • register (Symbol, Register, nil)

    The register for type resolution

Returns:

  • (Transformation, nil)

    The transformation, or nil if cycle detected



116
117
118
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
# File 'lib/lutaml/model/transformation_registry.rb', line 116

def get_or_build_transformation(model_class, format, register)
  key = transformation_key(model_class, format, register)
  current_thread = Thread.current

  @mutex.synchronize do
    loop do
      cached = @transformations[key]

      # Return if already built
      return cached if cached && cached != :building

      # Check for cycles (same thread trying to build same transformation)
      if cached == :building && @building_threads[key] == current_thread
        return nil
      end

      # If another thread is building, wait for it
      if cached == :building
        @condition.wait(@mutex)
        next # Re-check after waking
      end

      # Mark as building and record which thread
      @transformations[key] = :building
      @building_threads[key] = current_thread
      break
    end
  end

  # Build transformation OUTSIDE the lock to avoid deadlock
  # (building may trigger recursive calls to get_or_build_transformation)
  mapping = get_or_build_mapping(model_class, format, register)
  transformation = build_transformation(model_class, mapping, format,
                                        register)

  # Cache and return the result
  @mutex.synchronize do
    @transformations[key] = transformation
    @building_threads.delete(key)
    @condition.broadcast
  end

  transformation
end