Module: Lutaml::Model::Serialize::ModelImport

Included in:
ClassMethods
Defined in:
lib/lutaml/model/serialize/model_import.rb

Overview

Handles model import/export methods for Serialize::ClassMethods

Extracted from serialize.rb to improve code organization. Provides methods for importing attributes and mappings from other models.

Instance Method Summary collapse

Instance Method Details

#check_sort_configs!Object

Check and validate sort configurations

Raises:



277
278
279
280
281
# File 'lib/lutaml/model/serialize/model_import.rb', line 277

def check_sort_configs!
  return unless collection_with_conflicting_sort?

  raise Lutaml::Model::SortingConfigurationConflictError.new
end

#collection_with_conflicting_sort?Boolean

Check for conflicting sort configurations (XML-specific)

Returns false by default. XML overrides to check @mappings.ordered?

Returns:

  • (Boolean)

    True if there’s a conflict



270
271
272
# File 'lib/lutaml/model/serialize/model_import.rb', line 270

def collection_with_conflicting_sort?
  false
end

#ensure_child_imports_resolved!(register, visited = nil) ⇒ void

This method returns an undefined value.

Recursively ensure all child model imports are resolved

Walks through all attributes and ensures any Serializable types have BOTH model-level imports (attributes) AND mapping-level imports (XML) resolved before serialization/deserialization begins.

CRITICAL PERFORMANCE FIX (Session 122): Uses a visited set to prevent redundant processing in large schemas. Without this, OOXML-scale schemas (hundreds of classes) create exponential cascades.

Parameters:

  • register (Symbol)

    the register ID

  • visited (Set<Class>, nil) (defaults to: nil)

    classes already processed (internal use only)



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
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
# File 'lib/lutaml/model/serialize/model_import.rb', line 296

def ensure_child_imports_resolved!(register, visited = nil)
  return unless finalized?

  # Normalize register to Symbol for use in instance variable name
  reg_id = case register
           when Symbol then register
           when String then register.to_sym
           when Lutaml::Model::Register then register.id
           else register
           end

  # Guard: skip if already resolved for this register.
  # After the first call, all imports ARE resolved — subsequent calls are pure waste.
  resolved_key = :"@_imports_resolved_#{reg_id}"
  if instance_variable_get(resolved_key)
    return
  end

  # Create visited set ONLY at top level (first call)
  # All recursive calls share the SAME set to prevent redundant processing
  visited ||= Set.new

  return if visited.include?(self) # Already processed this class

  visited.add(self) # Mark as visited BEFORE processing to prevent cycles

  # Mark as resolved BEFORE processing to prevent redundant top-level calls
  instance_variable_set(resolved_key, true)

  # CRITICAL: Use direct instance variable access to avoid triggering ensure_imports!
  # Calling attributes(register) would trigger ensure_imports! which creates circular chains
  attrs = @attributes || {}
  attrs.each_value do |attr|
    type_class = attr.type(register)
    next unless type_class
    # Check if type_class is a Serializable class that needs import resolution
    next unless type_class.is_a?(Class) && type_class < Lutaml::Model::Serialize
    next if visited.include?(type_class) # Skip if already visited

    # Mark child as visited BEFORE processing to prevent cycles
    visited.add(type_class)

    # Use child's own register if it has one, otherwise use parent's register
    # This ensures versioned schemas (e.g., MML v2 with lutaml_default_register = :mml_v2)
    # have their imports resolved in the correct context
    type_class_register = Lutaml::Model::Register.resolve_for_child(
      type_class, register
    )

    # Ensure model-level imports (attributes, choices, and format-specific mappings)
    # ensure_imports! calls ensure_format_mapping_imports! which is overridden by XML
    type_class.ensure_imports!(type_class_register)

    # Recursively process child's children, passing THE SAME visited set
    type_class.ensure_child_imports_resolved!(type_class_register,
                                              visited)
  end
end

#ensure_choice_imports!(register_id = nil) ⇒ Object

Ensure all choice imports are resolved for a specific register

Parameters:

  • register_id (Symbol, nil) (defaults to: nil)

    The register context



191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
# File 'lib/lutaml/model/serialize/model_import.rb', line 191

def ensure_choice_imports!(register_id = nil)
  register_id ||= Lutaml::Model::Config.default_register
  @choices_imported = {} if @choices_imported.nil? || @choices_imported == false
  return if @choices_imported[register_id] || Utils.present?(@register_records[register_id][:choice_attributes])

  @choices_imported[register_id] = true
  all_resolved = true

  importable_choices.each do |choice, choice_imports|
    choice_imports.each do |method, models|
      models.uniq.each do |model|
        model_class = begin
          Lutaml::Model::GlobalContext.resolve_type(model, register_id)
        rescue Lutaml::Model::UnknownTypeError
          nil
        end

        if model_class.nil?
          all_resolved = false
          next
        end

        if model_class.is_a?(Class) && model_class.include?(Lutaml::Model::Serialize)
          model_class.ensure_imports!(register_id)
        end

        choice.public_send(method, model_class, register_id)
      end
    end
  end

  @choices_imported[register_id] = true if all_resolved
end

#ensure_model_imports!(register_id = nil) ⇒ Object

Ensure all model imports are resolved for a specific register

Parameters:

  • register_id (Symbol, nil) (defaults to: nil)

    The register context



155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
# File 'lib/lutaml/model/serialize/model_import.rb', line 155

def ensure_model_imports!(register_id = nil)
  register_id ||= Lutaml::Model::Config.default_register
  @models_imported = {} if @models_imported.nil? || @models_imported == false
  return if @models_imported[register_id] || Utils.present?(@register_records[register_id][:attributes])

  @models_imported[register_id] = true
  all_resolved = true

  importable_models.each do |method, models|
    models.uniq.each do |model|
      model_class = begin
        Lutaml::Model::GlobalContext.resolve_type(model, register_id)
      rescue Lutaml::Model::UnknownTypeError
        nil
      end

      if model_class.nil?
        all_resolved = false
        next
      end

      if model_class.is_a?(Class) && model_class.include?(Lutaml::Model::Serialize)
        model_class.ensure_imports!(register_id)
      end

      import_model_with_root_error(model_class, register_id)
      @model.public_send(method, model_class, register_id)
    end
  end

  @models_imported[register_id] = all_resolved
end

#ensure_restrict_attributes!(register_id = nil) ⇒ Object

Ensure all restrict attributes are applied

Parameters:

  • register_id (Symbol, nil) (defaults to: nil)

    The register context



228
229
230
231
232
233
234
235
236
237
238
# File 'lib/lutaml/model/serialize/model_import.rb', line 228

def ensure_restrict_attributes!(register_id = nil)
  return if restrict_attributes.empty?

  attrs = restrict_attributes.dup
  restrict_attributes.clear
  register_id ||= Lutaml::Model::Config.default_register
  attrs.each do |name, options_list|
    options_list[:register] = register_id
    restrict(name, options_list)
  end
end

#finalized?Boolean

Check if the class has been finalized

Returns:

  • (Boolean)

    True if the class definition is complete



261
262
263
# File 'lib/lutaml/model/serialize/model_import.rb', line 261

def finalized?
  @finalized
end

#import_model(model, register_id = nil) ⇒ Object

Import both attributes and mappings from another model

Parameters:

  • model (Class, Symbol, String)

    The model to import from

  • register_id (Symbol, nil) (defaults to: nil)

    The register context



93
94
95
96
97
98
99
100
101
102
103
104
105
# File 'lib/lutaml/model/serialize/model_import.rb', line 93

def import_model(model, register_id = nil)
  if model.is_a?(Symbol) || model.is_a?(String)
    reg = extract_register_id(register_id)
    importable_models[:import_model] << model.to_sym
    @models_imported = (@models_imported || {}).merge(reg => false)
    @choices_imported = (@choices_imported || {}).merge(reg => false)
    setup_trace_point
    return
  end

  import_model_attributes(model, register_id)
  import_model_mappings(model, register_id)
end

#import_model_attributes(model, register_id = nil) ⇒ Object

Import attributes from another model

Deferred path (model is a Symbol/String): stores model reference per-register and resolves lazily on first access via ensure_imports!.

Immediate path (model is a Class): merges into class-level @attributes for default register, or register-specific storage for non-default registers.

Parameters:

  • model (Class, Symbol, String)

    The model to import from

  • register_id (Symbol, nil) (defaults to: nil)

    The register context



43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
# File 'lib/lutaml/model/serialize/model_import.rb', line 43

def import_model_attributes(model, register_id = nil)
  if model.is_a?(Symbol) || model.is_a?(String)
    reg = extract_register_id(register_id)
    importable_models[:import_model_attributes] << model.to_sym
    @models_imported = (@models_imported || {}).merge(reg => false)
    @choices_imported = (@choices_imported || {}).merge(reg => false)
    setup_trace_point
    return
  end

  reg = extract_register_id(register_id)
  if reg != :default
    register_only_import_model_attributes(model, register_id)
    return
  end

  # Default register: merge into class-level storage
  model.attributes.each_value { |attr| define_attribute_methods(attr) }
  @attributes.merge!(Utils.deep_dup(model.attributes))
  @choice_attributes.concat(deep_duplicate_choice_attributes(model))
  # Ensure @models_imported is a hash; migrate from legacy nil/false state
  @models_imported ||= {}
  @models_imported[reg] = true
end

#import_model_mappings(model, register_id = nil) ⇒ Object

Import mappings from another model

Delegates to format-specific import_model_mappings which stores in register-specific storage when register_id is non-default.

Parameters:

  • model (Class)

    The model to import from

  • register_id (Symbol, nil) (defaults to: nil)

    The register context



75
76
77
78
79
80
81
82
83
84
85
86
87
# File 'lib/lutaml/model/serialize/model_import.rb', line 75

def import_model_mappings(model, register_id = nil)
  Lutaml::Model::FormatRegistry.formats.each do |format|
    next unless model.mappings.key?(format)

    klass = ::Lutaml::Model::Config.mappings_class_for(format)
    @mappings[format] ||= klass.new
    mapping = @mappings[format]
    if mapping.respond_to?(:import_model_mappings)
      mapping.import_model_mappings(model,
                                    register_id)
    end
  end
end

#import_model_with_root_error(model, register = nil) ⇒ Object

Raise error if importing a model with a root element

Parameters:

  • model (Class)

    The model to check

  • register (Symbol, nil) (defaults to: nil)

    The register context

Raises:



27
28
29
30
31
# File 'lib/lutaml/model/serialize/model_import.rb', line 27

def import_model_with_root_error(model, register = nil)
  return unless model.root?(register)

  raise Lutaml::Model::ImportModelWithRootError.new(model)
end

#importable_choicesMappingHash

Get hash of importable choices (deferred choice imports)

Returns:



144
145
146
147
148
149
150
# File 'lib/lutaml/model/serialize/model_import.rb', line 144

def importable_choices
  @importable_choices ||= MappingHash.new do |h, k|
    h[k] = MappingHash.new do |h1, k1|
      h1[k1] = []
    end
  end
end

#importable_modelsMappingHash

Get hash of importable models (deferred imports)

Returns:

  • (MappingHash)

    Hash of method names to model arrays



130
131
132
# File 'lib/lutaml/model/serialize/model_import.rb', line 130

def importable_models
  @importable_models ||= MappingHash.new { |h, k| h[k] = [] }
end

#restrict_attributesMappingHash

Get hash of restrict attributes (deferred restrictions)

Returns:



137
138
139
# File 'lib/lutaml/model/serialize/model_import.rb', line 137

def restrict_attributes
  @restrict_attributes ||= MappingHash.new
end

#root?(_register = nil) ⇒ Boolean

Check if register has a root mapping (XML-specific)

Returns false by default. When Lutaml::Xml is loaded, this is overridden to check XML mappings via prepend.

Parameters:

  • register (Symbol, nil)

    The register to check

Returns:

  • (Boolean)

    True if the model has a root mapping



18
19
20
# File 'lib/lutaml/model/serialize/model_import.rb', line 18

def root?(_register = nil)
  false
end

#setup_trace_pointObject

Setup trace point to detect class finalization

Uses TracePoint to detect when a class definition is complete, allowing deferred import resolution.



244
245
246
247
248
249
250
251
252
253
254
255
256
# File 'lib/lutaml/model/serialize/model_import.rb', line 244

def setup_trace_point
  @trace ||= TracePoint.new(:end) do |_tp|
    if include?(Lutaml::Model::Serialize)
      @finalized = true
      # NOTE: Do NOT resolve imports here - it's too early in the load process
      # Classes being imported may not be defined yet, causing circular errors
      # Imports will be resolved lazily on first use via ensure_imports!
      # BUT they will be resolved RECURSIVELY, preventing redundant resolution
      @trace.disable
    end
  end
  @trace.enable unless @trace.enabled?
end