Class: Lutaml::UmlRepository::IndexBuilder

Inherits:
Object
  • Object
show all
Defined in:
lib/lutaml/uml_repository/index_builder.rb

Overview

IndexBuilder builds fast lookup indexes from a Lutaml::Uml::Document

This class creates immutable hash indexes that enable O(1) lookups for:

  • Package paths (e.g., “ModelRoot::i-UR::urf”)

  • Qualified names (e.g., “ModelRoot::i-UR::urf::Building”)

  • Stereotypes (e.g., “featureType” => [Class, Class, …])

  • Inheritance graph (parent_qname => [child_qname, …])

  • Diagram index (package_id => [Diagram, …])

  • Package to path mapping (package_id => path)

  • Class to qualified name mapping (class_id => qualified_name)

  • Classes (class_id => Class)

  • Associations (association_id => Association)

All indexes are frozen to ensure immutability.

Examples:

Building all indexes from a document

indexes = IndexBuilder.build_all(document)
package = indexes[:package_paths]["ModelRoot::i-UR"]
klass = indexes[:qualified_names]["ModelRoot::i-UR::Building"]

Constant Summary collapse

ROOT_PACKAGE_NAME =
"ModelRoot"

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(document) ⇒ IndexBuilder

Returns a new instance of IndexBuilder.



142
143
144
145
146
147
148
149
150
151
152
153
154
155
# File 'lib/lutaml/uml_repository/index_builder.rb', line 142

def initialize(document)
  @document = document
  @package_paths = {}
  @qualified_names = {}
  @stereotypes = {}
  @inheritance_graph = {}
  @diagram_index = {}
  @package_to_path = {}
  @class_to_qname = {}
  @classes = {}
  @associations = {}
  @simple_name_to_qnames = {}
  @package_to_classes = {}
end

Class Method Details

.build_all(document) ⇒ Hash

Build all indexes from a UML document

Parameters:

Returns:

  • (Hash)

    A frozen hash containing all indexes with keys:

    • :package_paths - Maps package paths to Package objects

    • :qualified_names - Maps qualified names to Class/DataType/Enum objects

    • :stereotypes - Groups classes by stereotype

    • :inheritance_graph - Maps parent qualified names to child qualified names

    • :diagram_index - Maps package IDs/paths to Diagram objects

    • :package_to_path - Maps package XMI IDs to paths

    • :class_to_qname - Maps class XMI IDs to qualified names

    • :classes - Maps class XMI IDs to Class objects

    • :associations - Maps association XMI IDs to Association objects



45
46
47
# File 'lib/lutaml/uml_repository/index_builder.rb', line 45

def self.build_all(document)
  new(document).build_all
end

.build_associations(document) ⇒ Object



87
88
89
90
91
92
93
94
# File 'lib/lutaml/uml_repository/index_builder.rb', line 87

def self.build_associations(document)
  builder = new(document)
  # build_association_index needs @qualified_names to collect
  # class-level associations
  builder.build_qualified_name_index
  builder.build_association_index
  builder.instance_variable_get(:@associations).freeze
end

.build_class_to_qname(document) ⇒ Object



75
76
77
78
79
# File 'lib/lutaml/uml_repository/index_builder.rb', line 75

def self.build_class_to_qname(document)
  builder = new(document)
  builder.build_qualified_name_index
  builder.instance_variable_get(:@class_to_qname).freeze
end

.build_classes(document) ⇒ Object



81
82
83
84
85
# File 'lib/lutaml/uml_repository/index_builder.rb', line 81

def self.build_classes(document)
  builder = new(document)
  builder.build_qualified_name_index
  builder.instance_variable_get(:@classes).freeze
end

.build_diagram_index(document, indexes) ⇒ Hash

Build diagram index

Parameters:

  • document (Lutaml::Uml::Document)

    The UML document

  • indexes (Hash, nil)

    Existing indexes (requires :package_paths)

Returns:

  • (Hash)

    Frozen hash mapping package IDs to Diagram objects



129
130
131
132
133
134
135
136
137
138
139
140
# File 'lib/lutaml/uml_repository/index_builder.rb', line 129

def self.build_diagram_index(document, indexes)
  builder = new(document)
  # If package_paths index is provided, use it
  if indexes && indexes[:package_paths]
    builder.instance_variable_set(:@package_paths,
                                  indexes[:package_paths])
  else
    builder.build_package_path_index
  end
  builder.build_diagram_index
  builder.instance_variable_get(:@diagram_index).freeze
end

.build_inheritance_graph(document, indexes) ⇒ Hash

Build inheritance graph index

Parameters:

  • document (Lutaml::Uml::Document)

    The UML document

  • indexes (Hash, nil)

    Existing indexes (requires :qualified_names)

Returns:

  • (Hash)

    Frozen hash mapping parent qnames to child qnames



111
112
113
114
115
116
117
118
119
120
121
122
# File 'lib/lutaml/uml_repository/index_builder.rb', line 111

def self.build_inheritance_graph(document, indexes)
  builder = new(document)
  # If qualified_names index is provided, use it
  if indexes && indexes[:qualified_names]
    builder.instance_variable_set(:@qualified_names,
                                  indexes[:qualified_names])
  else
    builder.build_qualified_name_index
  end
  builder.build_inheritance_graph_index
  builder.instance_variable_get(:@inheritance_graph).freeze
end

.build_package_paths(document) ⇒ Hash

Build package paths index

Parameters:

Returns:

  • (Hash)

    Frozen hash mapping package paths to Package objects



53
54
55
56
57
# File 'lib/lutaml/uml_repository/index_builder.rb', line 53

def self.build_package_paths(document)
  builder = new(document)
  builder.build_package_path_index
  builder.instance_variable_get(:@package_paths).freeze
end

.build_package_to_path(document) ⇒ Object



59
60
61
62
63
# File 'lib/lutaml/uml_repository/index_builder.rb', line 59

def self.build_package_to_path(document)
  builder = new(document)
  builder.build_package_path_index
  builder.instance_variable_get(:@package_to_path).freeze
end

.build_qualified_names(document) ⇒ Hash

Build qualified names index

Parameters:

Returns:

  • (Hash)

    Frozen hash mapping qualified names to Class objects



69
70
71
72
73
# File 'lib/lutaml/uml_repository/index_builder.rb', line 69

def self.build_qualified_names(document)
  builder = new(document)
  builder.build_qualified_name_index
  builder.instance_variable_get(:@qualified_names).freeze
end

.build_stereotypes(document) ⇒ Hash

Build stereotypes index

Parameters:

Returns:

  • (Hash)

    Frozen hash grouping classes by stereotype



100
101
102
103
104
# File 'lib/lutaml/uml_repository/index_builder.rb', line 100

def self.build_stereotypes(document)
  builder = new(document)
  builder.build_stereotype_index
  builder.instance_variable_get(:@stereotypes).freeze
end

Instance Method Details

#build_allHash

Build all indexes and return them as a frozen hash

Returns:

  • (Hash)

    Frozen hash containing all indexes



160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
# File 'lib/lutaml/uml_repository/index_builder.rb', line 160

def build_all # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
  build_package_path_index
  build_qualified_name_index
  build_stereotype_index
  build_inheritance_graph_index
  build_diagram_index
  build_association_index

  {
    package_paths: @package_paths.freeze,
    qualified_names: @qualified_names.freeze,
    stereotypes: @stereotypes.freeze,
    inheritance_graph: @inheritance_graph.freeze,
    diagram_index: @diagram_index.freeze,
    package_to_path: @package_to_path.freeze,
    class_to_qname: @class_to_qname.freeze,
    classes: @classes.freeze,
    associations: @associations.freeze,
    package_to_classes: plain_hash(@package_to_classes).freeze,
  }.freeze
end

#build_association_indexObject

rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity



227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
# File 'lib/lutaml/uml_repository/index_builder.rb', line 227

def build_association_index # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
  # Collect document-level associations (XMI format)
  @document.associations&.each do |assoc|
    next unless assoc.xmi_id

    @associations[assoc.xmi_id] = assoc
  end

  # Collect class-level associations (QEA/EA format)
  # Note: This requires qualified_names index to be built first
  @qualified_names.each_value do |klass|
    next unless klass.respond_to?(:associations) && klass.associations

    klass.associations.each do |assoc|
      next unless assoc.xmi_id

      # Avoid duplicates - only add if not already present
      @associations[assoc.xmi_id] ||= assoc
    end
  end
end

#build_diagram_indexObject

Build the diagram index

Creates a hash mapping package IDs/paths to arrays of Diagram objects:

"package_id" => [Diagram{}, Diagram{}]


294
295
296
297
298
299
300
301
302
303
304
# File 'lib/lutaml/uml_repository/index_builder.rb', line 294

def build_diagram_index
  # Traverse packages and collect diagrams
  traverse_packages(@document.packages) do |package, path|
    next unless package.diagrams && !package.diagrams.empty?

    # Index by package ID if available, otherwise by path
    key = package.xmi_id || path
    @diagram_index[key] ||= []
    @diagram_index[key].concat(package.diagrams)
  end
end

#build_inheritance_graph_indexObject

Build the inheritance graph index

Creates a hash mapping parent qualified names to arrays of child qualified names:

"ModelRoot::Parent" => ["ModelRoot::Child1", "ModelRoot::Child2"]


275
276
277
278
279
280
281
282
283
284
285
286
287
# File 'lib/lutaml/uml_repository/index_builder.rb', line 275

def build_inheritance_graph_index
  # Process top-level classes
  if @document.classes
    process_generalizations(@document.classes,
                            ROOT_PACKAGE_NAME)
  end

  # Process classes in packages
  traverse_packages(@document.packages,
                    parent_path: ROOT_PACKAGE_NAME) do |package, path|
    process_generalizations(package.classes, path) if package.classes
  end
end

#build_package_path(name, parent_path) ⇒ String

Build a package path from a package name and parent path

Parameters:

  • name (String)

    Package name

  • parent_path (String, nil)

    Parent package path

Returns:

  • (String)

    Full package path



331
332
333
334
335
# File 'lib/lutaml/uml_repository/index_builder.rb', line 331

def build_package_path(name, parent_path)
  return name unless parent_path

  "#{parent_path}::#{name}"
end

#build_package_path_indexObject

Build the package path index

Creates a hash mapping package paths to Package objects:

"ModelRoot" => Package{},
"ModelRoot::i-UR" => Package{},
"ModelRoot::i-UR::urf" => Package{}


189
190
191
192
193
194
195
196
197
198
199
# File 'lib/lutaml/uml_repository/index_builder.rb', line 189

def build_package_path_index
  # Add root package if it exists
  @package_paths[ROOT_PACKAGE_NAME] = @document if @document

  # Traverse all packages recursively
  traverse_packages(@document.packages,
                    parent_path: ROOT_PACKAGE_NAME) do |package, path|
    @package_paths[path] = package
    @package_to_path[package.xmi_id] = path if package.xmi_id
  end
end

#build_qualified_name_indexObject

Build the qualified name index

Creates a hash mapping qualified names to Class/DataType/Enum objects:

"ModelRoot::i-UR::urf::Building" => Class{}


206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
# File 'lib/lutaml/uml_repository/index_builder.rb', line 206

def build_qualified_name_index # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength
  # Index top-level classes, data_types, and enums from document
  if @document.classes
    index_classifiers(@document.classes,
                      ROOT_PACKAGE_NAME)
  end
  if @document.data_types
    index_classifiers(@document.data_types,
                      ROOT_PACKAGE_NAME)
  end
  index_classifiers(@document.enums, ROOT_PACKAGE_NAME) if @document.enums

  # Traverse packages and index their classifiers
  traverse_packages(@document.packages,
                    parent_path: ROOT_PACKAGE_NAME) do |package, path|
    index_classifiers(package.classes, path) if package.classes
    index_classifiers(package.data_types, path) if package.data_types
    index_classifiers(package.enums, path) if package.enums
  end
end

#build_stereotype_indexObject

Build the stereotype index

Creates a hash grouping classes by their stereotype:

"featureType" => [Class{}, Class{}],
"dataType" => [Class{}]


255
256
257
258
259
260
261
262
263
264
265
266
267
# File 'lib/lutaml/uml_repository/index_builder.rb', line 255

def build_stereotype_index # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity
  # Process top-level classes
  index_by_stereotype(@document.classes) if @document.classes
  index_by_stereotype(@document.data_types) if @document.data_types
  index_by_stereotype(@document.enums) if @document.enums

  # Process classes in packages
  traverse_packages(@document.packages) do |package, _path|
    index_by_stereotype(package.classes) if package.classes
    index_by_stereotype(package.data_types) if package.data_types
    index_by_stereotype(package.enums) if package.enums
  end
end

#extract_parent_name(generalization) ⇒ String?

Extract parent name from generalization object

Generalization object

Parameters:

Returns:

  • (String, nil)

    Parent class name



430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
# File 'lib/lutaml/uml_repository/index_builder.rb', line 430

def extract_parent_name(generalization) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/MethodLength
  return nil unless generalization

  # Check for general attribute (could be a string or object)
  if generalization.respond_to?(:general)
    parent = generalization.general
    return parent.name if parent.respond_to?(:name)
    return parent.to_s if parent
  end

  # Check for name attribute directly
  if generalization.respond_to?(:name) && generalization.name
    return generalization.name
  end

  nil
end

#index_by_stereotype(classifiers) ⇒ Object

Index classifiers by their stereotypes

Parameters:

  • classifiers (Array)

    Array of classifier objects



363
364
365
366
367
368
369
370
371
372
373
374
375
376
# File 'lib/lutaml/uml_repository/index_builder.rb', line 363

def index_by_stereotype(classifiers) # rubocop:disable Metrics/CyclomaticComplexity
  return unless classifiers

  classifiers.each do |classifier|
    next unless classifier.stereotype && !classifier.stereotype.empty?

    # Handle both String and Array stereotypes
    stereotypes = Array(classifier.stereotype)
    stereotypes.each do |stereotype|
      @stereotypes[stereotype] ||= []
      @stereotypes[stereotype] << classifier
    end
  end
end

#index_classifiers(classifiers, package_path) ⇒ Object

Index classifiers (classes, data types, enums) by their qualified names

Parameters:

  • classifiers (Array)

    Array of classifier objects

  • package_path (String)

    Package path for these classifiers



341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
# File 'lib/lutaml/uml_repository/index_builder.rb', line 341

def index_classifiers(classifiers, package_path) # rubocop:disable Metrics/MethodLength
  return unless classifiers

  classifiers.each do |classifier|
    next unless classifier.name

    qualified_name = "#{package_path}::#{classifier.name}"
    @qualified_names[qualified_name] = classifier
    if classifier.xmi_id
      @class_to_qname[classifier.xmi_id] =
        qualified_name
    end
    @classes[classifier.xmi_id] = classifier if classifier.xmi_id
    @simple_name_to_qnames[classifier.name] ||= []
    @simple_name_to_qnames[classifier.name] << qualified_name
    (@package_to_classes[package_path] ||= []) << classifier
  end
end

#process_generalizations(classes, package_path) ⇒ Object

Process generalization relationships to build inheritance graph

Parameters:

  • classes (Array<Lutaml::Uml::Class>)

    Classes to process

  • package_path (String)

    Package path for these classes



382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
# File 'lib/lutaml/uml_repository/index_builder.rb', line 382

def process_generalizations(classes, package_path) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
  return unless classes

  classes.each do |klass|
    next unless klass.name

    child_qname = "#{package_path}::#{klass.name}"

    # Handle generalization attribute
    if klass.generalization
      parent_name = extract_parent_name(klass.generalization)
      if parent_name
        parent_qname = resolve_qualified_name(parent_name, package_path)
        if parent_qname && child_qname != parent_qname
          @inheritance_graph[parent_qname] ||= []
          @inheritance_graph[parent_qname] << child_qname
        end
      end
    end

    # Handle inheritance associations
    next unless klass.associations

    klass.associations.each do |assoc|
      next unless assoc.respond_to?(:member_end_type)
      next unless assoc.member_end_type == "inheritance"

      parent_name = assoc.member_end
      next unless parent_name

      parent_name = parent_name.name if parent_name.respond_to?(:name)
      next unless parent_name.is_a?(String) && !parent_name.empty?

      parent_qname = resolve_qualified_name(parent_name, package_path)
      next unless parent_qname
      next if child_qname == parent_qname

      @inheritance_graph[parent_qname] ||= []
      @inheritance_graph[parent_qname] << child_qname
    end
  end
end

#resolve_qualified_name(name, current_package_path) ⇒ String?

Resolve a class name to its qualified name

This is a simplified resolution that checks:

  1. Same package

  2. Already qualified name in index

Parameters:

  • name (String)

    Class name to resolve

  • current_package_path (String)

    Current package context

Returns:

  • (String, nil)

    Resolved qualified name



457
458
459
460
461
462
463
464
465
466
467
468
# File 'lib/lutaml/uml_repository/index_builder.rb', line 457

def resolve_qualified_name(name, current_package_path)
  # If name contains "::", it might already be qualified
  return name if @qualified_names.key?(name)

  # Try in current package
  local_qname = "#{current_package_path}::#{name}"
  return local_qname if @qualified_names.key?(local_qname)

  # O(1) lookup using reverse index instead of O(n) scan
  candidates = @simple_name_to_qnames[name]
  candidates&.first
end

#traverse_packages(packages, parent_path: nil) {|package, path| ... } ⇒ Object

Traverse packages recursively, yielding each package with its path

Parameters:

  • packages (Array<Lutaml::Uml::Package>)

    Packages to traverse

  • parent_path (String, nil) (defaults to: nil)

    Parent package path

Yields:

  • (package, path)

    Yields each package with its full path



311
312
313
314
315
316
317
318
319
320
321
322
323
324
# File 'lib/lutaml/uml_repository/index_builder.rb', line 311

def traverse_packages(packages, parent_path: nil, &block)
  return unless packages

  packages.each do |package|
    path = build_package_path(package.name, parent_path)
    yield package, path if block

    # Recursively traverse nested packages
    if package.packages
      traverse_packages(package.packages, parent_path: path,
                        &block)
    end
  end
end