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.



152
153
154
155
156
157
158
159
160
161
162
163
# File 'lib/lutaml/uml_repository/index_builder.rb', line 152

def initialize(document)
  @document = document
  @package_paths = {}
  @qualified_names = {}
  @stereotypes = {}
  @inheritance_graph = {}
  @diagram_index = {}
  @package_to_path = {}
  @class_to_qname = {}
  @classes = {}
  @associations = {}
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
48
49
50
51
52
53
54
55
56
57
# File 'lib/lutaml/uml_repository/index_builder.rb', line 45

def self.build_all(document) # rubocop:disable Metrics/MethodLength
  {
    package_paths: build_package_paths(document),
    qualified_names: build_qualified_names(document),
    stereotypes: build_stereotypes(document),
    inheritance_graph: build_inheritance_graph(document, nil),
    diagram_index: build_diagram_index(document, nil),
    package_to_path: build_package_to_path(document),
    class_to_qname: build_class_to_qname(document),
    classes: build_classes(document),
    associations: build_associations(document),
  }.freeze
end

.build_associations(document) ⇒ Object



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

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



85
86
87
88
89
# File 'lib/lutaml/uml_repository/index_builder.rb', line 85

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



91
92
93
94
95
# File 'lib/lutaml/uml_repository/index_builder.rb', line 91

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



139
140
141
142
143
144
145
146
147
148
149
150
# File 'lib/lutaml/uml_repository/index_builder.rb', line 139

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



121
122
123
124
125
126
127
128
129
130
131
132
# File 'lib/lutaml/uml_repository/index_builder.rb', line 121

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



63
64
65
66
67
# File 'lib/lutaml/uml_repository/index_builder.rb', line 63

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



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

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



79
80
81
82
83
# File 'lib/lutaml/uml_repository/index_builder.rb', line 79

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



110
111
112
113
114
# File 'lib/lutaml/uml_repository/index_builder.rb', line 110

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



168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
# File 'lib/lutaml/uml_repository/index_builder.rb', line 168

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,
  }.freeze
end

#build_association_indexObject

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



234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
# File 'lib/lutaml/uml_repository/index_builder.rb', line 234

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{}]


301
302
303
304
305
306
307
308
309
310
311
# File 'lib/lutaml/uml_repository/index_builder.rb', line 301

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"]


282
283
284
285
286
287
288
289
290
291
292
293
294
# File 'lib/lutaml/uml_repository/index_builder.rb', line 282

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



338
339
340
341
342
# File 'lib/lutaml/uml_repository/index_builder.rb', line 338

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{}


196
197
198
199
200
201
202
203
204
205
206
# File 'lib/lutaml/uml_repository/index_builder.rb', line 196

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{}


213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
# File 'lib/lutaml/uml_repository/index_builder.rb', line 213

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{}]


262
263
264
265
266
267
268
269
270
271
272
273
274
# File 'lib/lutaml/uml_repository/index_builder.rb', line 262

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



416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
# File 'lib/lutaml/uml_repository/index_builder.rb', line 416

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



367
368
369
370
371
372
373
374
375
376
377
378
379
380
# File 'lib/lutaml/uml_repository/index_builder.rb', line 367

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



348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
# File 'lib/lutaml/uml_repository/index_builder.rb', line 348

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
  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



386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
# File 'lib/lutaml/uml_repository/index_builder.rb', line 386

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
    next unless klass.generalization

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

    # Handle generalization - it could have a general attribute
    parent_name = extract_parent_name(klass.generalization)
    next unless parent_name

    # Try to resolve parent qualified name
    parent_qname = resolve_qualified_name(parent_name, package_path)
    next unless parent_qname

    # Avoid self-references
    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



443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
# File 'lib/lutaml/uml_repository/index_builder.rb', line 443

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)

  # Try to find in all qualified names (simple name match)
  @qualified_names.each_key do |qname|
    return qname if qname.end_with?("::#{name}")
  end

  nil
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



318
319
320
321
322
323
324
325
326
327
328
329
330
331
# File 'lib/lutaml/uml_repository/index_builder.rb', line 318

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