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"

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(document) ⇒ IndexBuilder

Returns a new instance of IndexBuilder.



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

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

Instance Attribute Details

#associationsObject (readonly)

Returns the value of attribute associations.



157
158
159
# File 'lib/lutaml/uml_repository/index_builder.rb', line 157

def associations
  @associations
end

#class_to_qnameObject (readonly)

Returns the value of attribute class_to_qname.



157
158
159
# File 'lib/lutaml/uml_repository/index_builder.rb', line 157

def class_to_qname
  @class_to_qname
end

#classesObject (readonly)

Returns the value of attribute classes.



157
158
159
# File 'lib/lutaml/uml_repository/index_builder.rb', line 157

def classes
  @classes
end

#diagram_indexObject (readonly)

Returns the value of attribute diagram_index.



157
158
159
# File 'lib/lutaml/uml_repository/index_builder.rb', line 157

def diagram_index
  @diagram_index
end

#inheritance_graphObject (readonly)

Returns the value of attribute inheritance_graph.



157
158
159
# File 'lib/lutaml/uml_repository/index_builder.rb', line 157

def inheritance_graph
  @inheritance_graph
end

#package_pathsObject

Returns the value of attribute package_paths.



156
157
158
# File 'lib/lutaml/uml_repository/index_builder.rb', line 156

def package_paths
  @package_paths
end

#package_to_pathObject (readonly)

Returns the value of attribute package_to_path.



157
158
159
# File 'lib/lutaml/uml_repository/index_builder.rb', line 157

def package_to_path
  @package_to_path
end

#qualified_namesObject

Returns the value of attribute qualified_names.



156
157
158
# File 'lib/lutaml/uml_repository/index_builder.rb', line 156

def qualified_names
  @qualified_names
end

#stereotypesObject (readonly)

Returns the value of attribute stereotypes.



157
158
159
# File 'lib/lutaml/uml_repository/index_builder.rb', line 157

def stereotypes
  @stereotypes
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.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.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.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



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

def self.build_diagram_index(document, indexes)
  builder = new(document)
  if indexes && indexes[:package_paths]
    builder.package_paths = indexes[:package_paths]
  else
    builder.build_package_path_index
  end
  builder.build_diagram_index
  builder.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
# File 'lib/lutaml/uml_repository/index_builder.rb', line 114

def self.build_inheritance_graph(document, indexes)
  builder = new(document)
  if indexes && indexes[:qualified_names]
    builder.qualified_names = indexes[:qualified_names]
  else
    builder.build_qualified_name_index
  end
  builder.build_inheritance_graph_index
  builder.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.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.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.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.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



6
7
8
9
# File 'lib/lutaml/uml_repository/index_builders/association_index.rb', line 6

def build_association_index
  index_document_associations
  index_class_level_associations
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"]


41
42
43
44
45
46
47
48
49
50
51
52
53
# File 'lib/lutaml/uml_repository/index_builders/association_index.rb', line 41

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
# File 'lib/lutaml/uml_repository/index_builders/class_index.rb', line 11

def build_qualified_name_index
  index_document_classifiers
  index_package_classifiers
end

#build_stereotype_indexObject

Build the stereotype index

Creates a hash grouping classes by their stereotype:

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


43
44
45
46
# File 'lib/lutaml/uml_repository/index_builders/class_index.rb', line 43

def build_stereotype_index
  index_document_stereotypes
  index_package_stereotypes
end

#extract_parent_name(generalization) ⇒ String?

Extract parent name from generalization object

Generalization object

Parameters:

Returns:

  • (String, nil)

    Parent class name



117
118
119
120
121
# File 'lib/lutaml/uml_repository/index_builders/association_index.rb', line 117

def extract_parent_name(generalization)
  return nil unless generalization

  name_from_general(generalization) || generalization.name
end

#has_stereotype?(classifier) ⇒ Boolean

Returns:

  • (Boolean)


100
101
102
# File 'lib/lutaml/uml_repository/index_builders/class_index.rb', line 100

def has_stereotype?(classifier)
  classifier.stereotype && !classifier.stereotype.empty?
end

#index_by_stereotype(classifiers) ⇒ Object

Index classifiers by their stereotypes

Parameters:

  • classifiers (Array)

    Array of classifier objects



88
89
90
91
92
93
94
95
96
97
98
# File 'lib/lutaml/uml_repository/index_builders/class_index.rb', line 88

def index_by_stereotype(classifiers)
  return unless classifiers

  classifiers.each do |classifier|
    next unless has_stereotype?(classifier)

    Array(classifier.stereotype).each do |stereotype|
      (@stereotypes[stereotype] ||= []) << classifier
    end
  end
end

#index_class_level_associationsObject



19
20
21
22
23
24
25
26
27
28
29
# File 'lib/lutaml/uml_repository/index_builders/association_index.rb', line 19

def index_class_level_associations
  @qualified_names.each_value do |klass|
    next unless klassifiable?(klass) && klass.associations

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

      @associations[assoc.xmi_id] ||= assoc
    end
  end
end

#index_classifier(classifier, package_path) ⇒ Object



76
77
78
79
80
81
82
83
# File 'lib/lutaml/uml_repository/index_builders/class_index.rb', line 76

def index_classifier(classifier, package_path)
  qualified_name = "#{package_path}::#{classifier.name}"
  @qualified_names[qualified_name] = classifier
  @class_to_qname[classifier.xmi_id] = qualified_name if classifier.xmi_id
  @classes[classifier.xmi_id] = classifier if classifier.xmi_id
  (@simple_name_to_qnames[classifier.name] ||= []) << qualified_name
  (@package_to_classes[package_path] ||= []) << classifier
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



66
67
68
69
70
71
72
73
74
# File 'lib/lutaml/uml_repository/index_builders/class_index.rb', line 66

def index_classifiers(classifiers, package_path)
  return unless classifiers

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

    index_classifier(classifier, package_path)
  end
end

#index_document_associationsObject



11
12
13
14
15
16
17
# File 'lib/lutaml/uml_repository/index_builders/association_index.rb', line 11

def index_document_associations
  @document.associations&.each do |assoc|
    next unless assoc.xmi_id

    @associations[assoc.xmi_id] = assoc
  end
end

#index_document_classifiersObject



16
17
18
19
20
21
22
23
24
25
26
# File 'lib/lutaml/uml_repository/index_builders/class_index.rb', line 16

def index_document_classifiers
  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
end

#index_document_stereotypesObject



48
49
50
51
52
# File 'lib/lutaml/uml_repository/index_builders/class_index.rb', line 48

def index_document_stereotypes
  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
end

#index_generalization_edge(child_qname, klass, package_path) ⇒ Object



71
72
73
74
75
76
77
78
79
80
81
# File 'lib/lutaml/uml_repository/index_builders/association_index.rb', line 71

def index_generalization_edge(child_qname, klass, package_path)
  return unless klass.generalization

  parent_name = extract_parent_name(klass.generalization)
  return unless parent_name

  parent_qname = resolve_qualified_name(parent_name, package_path)
  return unless parent_qname && child_qname != parent_qname

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

#index_inheritance_assoc_edges(child_qname, klass, package_path) ⇒ Object



83
84
85
86
87
88
89
90
91
92
# File 'lib/lutaml/uml_repository/index_builders/association_index.rb', line 83

def index_inheritance_assoc_edges(child_qname, klass, package_path)
  return unless klass.associations

  klass.associations
    .select { |assoc| assoc.member_end_type == "inheritance" }
    .each do |assoc|
    index_inheritance_edge(child_qname, assoc,
                           package_path)
  end
end

#index_inheritance_edge(child_qname, assoc, package_path) ⇒ Object



94
95
96
97
98
99
100
101
102
# File 'lib/lutaml/uml_repository/index_builders/association_index.rb', line 94

def index_inheritance_edge(child_qname, assoc, package_path)
  parent_name = resolve_parent_name_from_assoc(assoc)
  return unless parent_name

  parent_qname = resolve_qualified_name(parent_name, package_path)
  return unless parent_qname && child_qname != parent_qname

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

#index_package_classifiersObject



28
29
30
31
32
33
34
35
# File 'lib/lutaml/uml_repository/index_builders/class_index.rb', line 28

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

#index_package_stereotypesObject



54
55
56
57
58
59
60
# File 'lib/lutaml/uml_repository/index_builders/class_index.rb', line 54

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

#klassifiable?(klass) ⇒ Boolean

Returns:

  • (Boolean)


31
32
33
# File 'lib/lutaml/uml_repository/index_builders/association_index.rb', line 31

def klassifiable?(klass)
  klass.is_a?(Lutaml::Uml::Class) || klass.is_a?(Lutaml::Uml::DataType)
end

#name_from_general(generalization) ⇒ 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



132
133
134
135
136
137
# File 'lib/lutaml/uml_repository/index_builders/association_index.rb', line 132

def name_from_general(generalization)
  parent = generalization.general
  return nil unless parent

  parent.respond_to?(:name) ? parent.name : parent.to_s
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



59
60
61
62
63
64
65
66
67
68
69
# File 'lib/lutaml/uml_repository/index_builders/association_index.rb', line 59

def process_generalizations(classes, package_path)
  return unless classes

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

    child_qname = "#{package_path}::#{klass.name}"
    index_generalization_edge(child_qname, klass, package_path)
    index_inheritance_assoc_edges(child_qname, klass, package_path)
  end
end

#resolve_parent_name_from_assoc(assoc) ⇒ Object



104
105
106
107
108
109
110
# File 'lib/lutaml/uml_repository/index_builders/association_index.rb', line 104

def resolve_parent_name_from_assoc(assoc)
  parent_name = assoc.member_end
  return nil unless parent_name

  parent_name = parent_name.name if parent_name.is_a?(Lutaml::Uml::Generalization)
  parent_name.is_a?(String) && !parent_name.empty? ? parent_name : nil
end

#resolve_qualified_name(name, current_package_path) ⇒ Object



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

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