Class: Lutaml::UmlRepository::IndexBuilder

Inherits:
Object
  • Object
show all
Defined in:
lib/lutaml/uml_repository/index_builder.rb,
lib/lutaml/uml_repository/index_builders/class_index.rb,
lib/lutaml/uml_repository/index_builders/package_index.rb,
lib/lutaml/uml_repository/index_builders/association_index.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.



145
146
147
148
149
150
151
152
153
154
155
156
157
158
# File 'lib/lutaml/uml_repository/index_builder.rb', line 145

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



48
49
50
# File 'lib/lutaml/uml_repository/index_builder.rb', line 48

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

.build_associations(document) ⇒ Object



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

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



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

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



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

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



132
133
134
135
136
137
138
139
140
141
142
143
# File 'lib/lutaml/uml_repository/index_builder.rb', line 132

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



114
115
116
117
118
119
120
121
122
123
124
125
# File 'lib/lutaml/uml_repository/index_builder.rb', line 114

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



56
57
58
59
60
# File 'lib/lutaml/uml_repository/index_builder.rb', line 56

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



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

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



72
73
74
75
76
# File 'lib/lutaml/uml_repository/index_builder.rb', line 72

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



103
104
105
106
107
# File 'lib/lutaml/uml_repository/index_builder.rb', line 103

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



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

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



6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# File 'lib/lutaml/uml_repository/index_builders/association_index.rb', line 6

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


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

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


34
35
36
37
38
39
40
41
42
43
44
45
46
# File 'lib/lutaml/uml_repository/index_builders/association_index.rb', line 34

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



50
51
52
53
54
# File 'lib/lutaml/uml_repository/index_builders/package_index.rb', line 50

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


13
14
15
16
17
18
19
20
21
22
23
# File 'lib/lutaml/uml_repository/index_builders/package_index.rb', line 13

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


11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# File 'lib/lutaml/uml_repository/index_builders/class_index.rb', line 11

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


38
39
40
41
42
43
44
45
46
47
48
49
50
# File 'lib/lutaml/uml_repository/index_builders/class_index.rb', line 38

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



100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
# File 'lib/lutaml/uml_repository/index_builders/association_index.rb', line 100

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



78
79
80
81
82
83
84
85
86
87
88
89
90
91
# File 'lib/lutaml/uml_repository/index_builders/class_index.rb', line 78

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



56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
# File 'lib/lutaml/uml_repository/index_builders/class_index.rb', line 56

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



52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
# File 'lib/lutaml/uml_repository/index_builders/association_index.rb', line 52

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



127
128
129
130
131
132
133
134
135
136
137
138
# File 'lib/lutaml/uml_repository/index_builders/association_index.rb', line 127

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



30
31
32
33
34
35
36
37
38
39
40
41
42
43
# File 'lib/lutaml/uml_repository/index_builders/package_index.rb', line 30

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