lutaml 0.10.13 → 0.10.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (25) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +58 -18
  3. data/lib/lutaml/cli/interactive_shell/export_handler.rb +9 -7
  4. data/lib/lutaml/cli/interactive_shell/help_display.rb +39 -45
  5. data/lib/lutaml/cli/interactive_shell/navigation_commands.rb +36 -21
  6. data/lib/lutaml/cli/interactive_shell/query_commands.rb +63 -47
  7. data/lib/lutaml/cli/interactive_shell.rb +43 -25
  8. data/lib/lutaml/qea/factory/association_builder.rb +114 -125
  9. data/lib/lutaml/qea/factory/class_transformer.rb +67 -43
  10. data/lib/lutaml/qea/factory/enum_transformer.rb +22 -23
  11. data/lib/lutaml/qea/factory/generalization_builder.rb +146 -116
  12. data/lib/lutaml/qea/factory/stereotype_loader.rb +13 -7
  13. data/lib/lutaml/qea/lookup_indexes.rb +8 -1
  14. data/lib/lutaml/uml_repository/exporters/markdown/class_page_builder.rb +33 -25
  15. data/lib/lutaml/uml_repository/index_builders/association_index.rb +49 -47
  16. data/lib/lutaml/uml_repository/index_builders/class_index.rb +30 -23
  17. data/lib/lutaml/uml_repository/queries/class_query.rb +74 -48
  18. data/lib/lutaml/uml_repository/queries/inheritance_query.rb +42 -32
  19. data/lib/lutaml/uml_repository/static_site/data_transformer.rb +55 -35
  20. data/lib/lutaml/uml_repository/static_site/search_index_builder.rb +32 -19
  21. data/lib/lutaml/uml_repository/static_site/serializers/class_serializer.rb +27 -17
  22. data/lib/lutaml/uml_repository/static_site/serializers/inheritance_resolver.rb +92 -68
  23. data/lib/lutaml/uml_repository/static_site/serializers/package_tree_builder.rb +32 -23
  24. data/lib/lutaml/version.rb +1 -1
  25. metadata +1 -1
@@ -8,8 +8,12 @@ module Lutaml
8
8
  # Creates a hash mapping qualified names to Class/DataType/Enum objects:
9
9
  # "ModelRoot::i-UR::urf::Building" => Class{}
10
10
  # @api public
11
- def build_qualified_name_index # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength
12
- # Index top-level classes, data_types, and enums from document
11
+ def build_qualified_name_index
12
+ index_document_classifiers
13
+ index_package_classifiers
14
+ end
15
+
16
+ def index_document_classifiers
13
17
  if @document.classes
14
18
  index_classifiers(@document.classes,
15
19
  ROOT_PACKAGE_NAME)
@@ -19,8 +23,9 @@ module Lutaml
19
23
  ROOT_PACKAGE_NAME)
20
24
  end
21
25
  index_classifiers(@document.enums, ROOT_PACKAGE_NAME) if @document.enums
26
+ end
22
27
 
23
- # Traverse packages and index their classifiers
28
+ def index_package_classifiers
24
29
  traverse_packages(@document.packages,
25
30
  parent_path: ROOT_PACKAGE_NAME) do |package, path|
26
31
  index_classifiers(package.classes, path) if package.classes
@@ -35,13 +40,18 @@ module Lutaml
35
40
  # "featureType" => [Class{}, Class{}],
36
41
  # "dataType" => [Class{}]
37
42
  # @api public
38
- def build_stereotype_index # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity
39
- # Process top-level classes
43
+ def build_stereotype_index
44
+ index_document_stereotypes
45
+ index_package_stereotypes
46
+ end
47
+
48
+ def index_document_stereotypes
40
49
  index_by_stereotype(@document.classes) if @document.classes
41
50
  index_by_stereotype(@document.data_types) if @document.data_types
42
51
  index_by_stereotype(@document.enums) if @document.enums
52
+ end
43
53
 
44
- # Process classes in packages
54
+ def index_package_stereotypes
45
55
  traverse_packages(@document.packages) do |package, _path|
46
56
  index_by_stereotype(package.classes) if package.classes
47
57
  index_by_stereotype(package.data_types) if package.data_types
@@ -53,39 +63,36 @@ module Lutaml
53
63
  #
54
64
  # @param classifiers [Array] Array of classifier objects
55
65
  # @param package_path [String] Package path for these classifiers
56
- def index_classifiers(classifiers, package_path) # rubocop:disable Metrics/MethodLength
66
+ def index_classifiers(classifiers, package_path)
57
67
  return unless classifiers
58
68
 
59
69
  classifiers.each do |classifier|
60
70
  next unless classifier.name
61
71
 
62
- qualified_name = "#{package_path}::#{classifier.name}"
63
- @qualified_names[qualified_name] = classifier
64
- if classifier.xmi_id
65
- @class_to_qname[classifier.xmi_id] =
66
- qualified_name
67
- end
68
- @classes[classifier.xmi_id] = classifier if classifier.xmi_id
69
- @simple_name_to_qnames[classifier.name] ||= []
70
- @simple_name_to_qnames[classifier.name] << qualified_name
71
- (@package_to_classes[package_path] ||= []) << classifier
72
+ index_classifier(classifier, package_path)
72
73
  end
73
74
  end
74
75
 
76
+ def index_classifier(classifier, package_path)
77
+ qualified_name = "#{package_path}::#{classifier.name}"
78
+ @qualified_names[qualified_name] = classifier
79
+ @class_to_qname[classifier.xmi_id] = qualified_name if classifier.xmi_id
80
+ @classes[classifier.xmi_id] = classifier if classifier.xmi_id
81
+ (@simple_name_to_qnames[classifier.name] ||= []) << qualified_name
82
+ (@package_to_classes[package_path] ||= []) << classifier
83
+ end
84
+
75
85
  # Index classifiers by their stereotypes
76
86
  #
77
87
  # @param classifiers [Array] Array of classifier objects
78
- def index_by_stereotype(classifiers) # rubocop:disable Metrics/CyclomaticComplexity
88
+ def index_by_stereotype(classifiers)
79
89
  return unless classifiers
80
90
 
81
91
  classifiers.each do |classifier|
82
92
  next unless classifier.stereotype && !classifier.stereotype.empty?
83
93
 
84
- # Handle both String and Array stereotypes
85
- stereotypes = Array(classifier.stereotype)
86
- stereotypes.each do |stereotype|
87
- @stereotypes[stereotype] ||= []
88
- @stereotypes[stereotype] << classifier
94
+ Array(classifier.stereotype).each do |stereotype|
95
+ (@stereotypes[stereotype] ||= []) << classifier
89
96
  end
90
97
  end
91
98
  end
@@ -66,7 +66,7 @@ module Lutaml
66
66
  # @example Recursive query
67
67
  # classes = query.in_package("ModelRoot::i-UR", recursive: true)
68
68
  # # Returns classes in i-UR and all nested packages
69
- def in_package(package_path_string, recursive: false) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
69
+ def in_package(package_path_string, recursive: false)
70
70
  return [] if package_path_string.nil? || package_path_string.empty?
71
71
 
72
72
  pkg_to_classes = indexes[:package_to_classes]
@@ -81,69 +81,95 @@ module Lutaml
81
81
  private
82
82
 
83
83
  # O(1) indexed lookup for in_package
84
- def in_package_indexed(package_path_string, pkg_to_classes, recursive:) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
84
+ def in_package_indexed(package_path_string, pkg_to_classes, recursive:)
85
85
  is_absolute = package_path_string.start_with?("::")
86
86
  search_segs = package_path_string.split("::").reject(&:empty?)
87
87
 
88
88
  results = []
89
89
  pkg_to_classes.each do |path, classes|
90
- path_segs = path.split("::")
91
- matched = if is_absolute
92
- if recursive
93
- path == package_path_string ||
94
- path.start_with?("#{package_path_string}::")
95
- else
96
- path == package_path_string
97
- end
98
- elsif recursive
99
- # Relative: match when path ends with search segments
100
- (0..(path_segs.size - search_segs.size)).any? do |i|
101
- path_segs[i, search_segs.size] == search_segs
102
- end
103
- else
104
- path_segs.size >= search_segs.size &&
105
- path_segs[-search_segs.size..] == search_segs
106
- end
107
-
108
- results.concat(classes) if matched
90
+ results.concat(classes) if indexed_path_matches?(
91
+ path, package_path_string, is_absolute, search_segs, recursive
92
+ )
109
93
  end
110
94
  results
111
95
  end
112
96
 
97
+ def indexed_path_matches?(path, package_path_string, is_absolute,
98
+ search_segs, recursive)
99
+ if is_absolute
100
+ indexed_absolute_match?(path, package_path_string, recursive)
101
+ else
102
+ indexed_relative_match?(path.split("::"), search_segs, recursive)
103
+ end
104
+ end
105
+
106
+ def indexed_absolute_match?(path, package_path_string, recursive)
107
+ if recursive
108
+ path == package_path_string ||
109
+ path.start_with?("#{package_path_string}::")
110
+ else
111
+ path == package_path_string
112
+ end
113
+ end
114
+
115
+ def indexed_relative_match?(path_segs, search_segs, recursive)
116
+ if recursive
117
+ (0..(path_segs.size - search_segs.size)).any? do |i|
118
+ path_segs[i, search_segs.size] == search_segs
119
+ end
120
+ else
121
+ path_segs.size >= search_segs.size &&
122
+ path_segs[-search_segs.size..] == search_segs
123
+ end
124
+ end
125
+
113
126
  # Fallback: original O(n) scan
114
127
  def in_package_scan(package_path_string, recursive:)
115
128
  package_path = Lutaml::Uml::PackagePath.new(package_path_string)
116
- results = []
117
129
  is_absolute = package_path.absolute?
118
130
 
119
- indexes[:qualified_names].each do |qname_string, klass|
120
- qname = Lutaml::Uml::QualifiedName.new(qname_string)
121
-
122
- matched = if is_absolute
123
- if recursive
124
- qname.package_path.starts_with?(package_path)
125
- else
126
- qname.package_path == package_path
127
- end
128
- else
129
- class_pkg_segs = qname.package_path.segments
130
- search_segs = package_path.segments
131
-
132
- if recursive
133
- (0..(class_pkg_segs.size - search_segs.size))
134
- .any? do |i|
135
- class_pkg_segs[i, search_segs.size] == search_segs
136
- end
137
- else
138
- class_pkg_segs.size >= search_segs.size &&
139
- class_pkg_segs[-search_segs.size..] == search_segs
140
- end
141
- end
142
-
143
- results << klass if matched
131
+ indexes[:qualified_names].each_value.select do |klass|
132
+ scan_matches_package?(klass, package_path, is_absolute, recursive)
144
133
  end
134
+ end
145
135
 
146
- results
136
+ def scan_matches_package?(klass, package_path, is_absolute, recursive)
137
+ qname = resolve_qname_for(klass)
138
+ return false unless qname
139
+
140
+ if is_absolute
141
+ match_absolute_path?(qname, package_path, recursive)
142
+ else
143
+ match_relative_path?(qname, package_path, recursive)
144
+ end
145
+ end
146
+
147
+ def resolve_qname_for(klass)
148
+ indexes[:qualified_names].find { |_, v| v == klass }&.first
149
+ end
150
+
151
+ def match_absolute_path?(qname, package_path, recursive)
152
+ qname = Lutaml::Uml::QualifiedName.new(qname)
153
+ if recursive
154
+ qname.package_path.starts_with?(package_path)
155
+ else
156
+ qname.package_path == package_path
157
+ end
158
+ end
159
+
160
+ def match_relative_path?(qname_string, package_path, recursive)
161
+ qname = Lutaml::Uml::QualifiedName.new(qname_string)
162
+ class_pkg_segs = qname.package_path.segments
163
+ search_segs = package_path.segments
164
+
165
+ if recursive
166
+ (0..(class_pkg_segs.size - search_segs.size)).any? do |i|
167
+ class_pkg_segs[i, search_segs.size] == search_segs
168
+ end
169
+ else
170
+ class_pkg_segs.size >= search_segs.size &&
171
+ class_pkg_segs[-search_segs.size..] == search_segs
172
+ end
147
173
  end
148
174
  end
149
175
  end
@@ -33,27 +33,15 @@ module Lutaml
33
33
  # parent = query.supertype("ModelRoot::Child")
34
34
  # # Or
35
35
  # parent = query.supertype(child_class)
36
- def supertype(class_or_qname) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
36
+ def supertype(class_or_qname)
37
37
  klass = resolve_class(class_or_qname)
38
- return nil unless klass
39
- return nil unless klass.is_a?(Lutaml::Uml::Class)
40
- return nil unless klass.generalization
38
+ return nil unless valid_supertype_target?(klass)
41
39
 
42
40
  parent_name = extract_parent_name(klass.generalization)
43
41
  return nil unless parent_name
44
- # avoid self-references
45
42
  return nil if parent_name == klass.name
46
43
 
47
- # Try to find in qualified_names index
48
- qname_string = resolve_qname(class_or_qname)
49
- return nil unless qname_string
50
-
51
- qname = Lutaml::Uml::QualifiedName.new(qname_string)
52
- package_path = qname.package_path.to_s
53
-
54
- # Try to resolve parent qualified name
55
- parent_qname = resolve_parent_qualified_name(parent_name,
56
- package_path)
44
+ parent_qname = resolve_parent_qname(class_or_qname, parent_name)
57
45
  return nil unless parent_qname
58
46
 
59
47
  indexes[:qualified_names][parent_qname]
@@ -200,14 +188,8 @@ module Lutaml
200
188
  # qualified name, or xmi_id
201
189
  # @return [Boolean] true if circular inheritance detected
202
190
  def has_circular_inheritance?(class_or_id, visited: Set.new)
203
- qname = if class_or_id.is_a?(String) &&
204
- indexes[:qualified_names].key?(class_or_id)
205
- class_or_id
206
- else
207
- resolve_qname(class_or_id)
208
- end
191
+ qname = resolve_to_qname(class_or_id)
209
192
  return false unless qname
210
-
211
193
  return true if visited.include?(qname)
212
194
 
213
195
  visited.add(qname)
@@ -219,6 +201,21 @@ module Lutaml
219
201
 
220
202
  private
221
203
 
204
+ def valid_supertype_target?(klass)
205
+ return false unless klass
206
+ return false unless klass.is_a?(Lutaml::Uml::Class)
207
+
208
+ klass.generalization ? true : false
209
+ end
210
+
211
+ def resolve_parent_qname(class_or_qname, parent_name)
212
+ qname_string = resolve_qname(class_or_qname)
213
+ return nil unless qname_string
214
+
215
+ qname = Lutaml::Uml::QualifiedName.new(qname_string)
216
+ resolve_parent_qualified_name(parent_name, qname.package_path.to_s)
217
+ end
218
+
222
219
  # Resolve a class by xmi_id or qualified name
223
220
  #
224
221
  # @param class_or_id [String] Qualified name or xmi_id
@@ -257,22 +254,24 @@ module Lutaml
257
254
  # @param max_depth [Integer, nil] Maximum depth to traverse
258
255
  # @param current_depth [Integer] Current depth
259
256
  # @return [Array] Array of descendant class objects
260
- def collect_descendants(qname_string, max_depth, current_depth) # rubocop:disable Metrics/MethodLength
257
+ def collect_descendants(qname_string, max_depth, current_depth)
261
258
  return [] if max_depth && current_depth >= max_depth
262
259
 
263
260
  children = direct_subtypes(qname_string)
264
261
  result = children.dup
262
+ collect_child_descendants(children, max_depth, current_depth, result)
263
+ result
264
+ end
265
265
 
266
+ def collect_child_descendants(children, max_depth, current_depth,
267
+ result)
266
268
  children.each do |child|
267
269
  child_qname = resolve_qname(child)
268
270
  next unless child_qname
269
271
 
270
- grandchildren = collect_descendants(child_qname, max_depth,
271
- current_depth + 1)
272
- result.concat(grandchildren)
272
+ result.concat(collect_descendants(child_qname, max_depth,
273
+ current_depth + 1))
273
274
  end
274
-
275
- result
276
275
  end
277
276
 
278
277
  # Extract parent name from generalization object
@@ -285,13 +284,24 @@ module Lutaml
285
284
  return nil unless generalization.is_a?(Lutaml::Uml::Generalization)
286
285
 
287
286
  parent = generalization.general
288
- if parent
289
- return parent.name if parent.is_a?(Lutaml::Uml::Generalization) && parent.name
287
+ return extract_name_from_parent(parent) if parent
288
+
289
+ generalization.name if generalization.name
290
+ end
290
291
 
291
- return parent.to_s
292
+ def resolve_to_qname(class_or_id)
293
+ if class_or_id.is_a?(String) &&
294
+ indexes[:qualified_names].key?(class_or_id)
295
+ class_or_id
296
+ else
297
+ resolve_qname(class_or_id)
292
298
  end
299
+ end
293
300
 
294
- generalization.name if generalization.name
301
+ def extract_name_from_parent(parent)
302
+ return parent.name if parent.is_a?(Lutaml::Uml::Generalization) && parent.name
303
+
304
+ parent.to_s
295
305
  end
296
306
 
297
307
  # Resolve a class name to its qualified name
@@ -33,24 +33,13 @@ module Lutaml
33
33
  def transform
34
34
  Models::SpaDocument.new(
35
35
  metadata: Serializers::MetadataBuilder.new(repository).build,
36
- package_tree: Serializers::PackageTreeBuilder.new(repository,
37
- id_generator).build,
38
- packages: Serializers::PackageSerializer.new(repository,
39
- id_generator, options).build_map,
40
- classes: Serializers::ClassSerializer.new(repository, id_generator,
41
- options, inheritance_resolver).build_map,
42
- attributes: Serializers::AttributeSerializer.new(repository,
43
- id_generator, options).build_map,
36
+ package_tree: build_package_tree,
37
+ packages: build_packages_map,
38
+ classes: build_classes_map,
39
+ attributes: build_attributes_map,
44
40
  associations: build_associations_map,
45
- operations: Serializers::OperationSerializer.new(repository,
46
- id_generator).build_map,
47
- diagrams: if options[:include_diagrams]
48
- Serializers::DiagramSerializer.new(
49
- repository, id_generator, options
50
- ).build_map
51
- else
52
- {}
53
- end,
41
+ operations: build_operations_map,
42
+ diagrams: build_diagrams_map,
54
43
  )
55
44
  end
56
45
 
@@ -74,24 +63,7 @@ module Lutaml
74
63
  map = Hash.new { |h, k| h[k] = [] }
75
64
 
76
65
  repository.classes_index.each do |klass|
77
- next unless klass.association_generalization
78
- unless klass.association_generalization && !klass.association_generalization.empty?
79
- next
80
- end
81
-
82
- klass.association_generalization.each do |assoc_gen|
83
- parent_object_id = assoc_gen.parent_object_id
84
- next unless parent_object_id
85
-
86
- parent_class = class_lookup.by_object_id(parent_object_id)
87
- if parent_class&.xmi_id
88
- next if parent_class.xmi_id == klass.xmi_id
89
-
90
- unless map[klass.xmi_id].include?(parent_class.xmi_id)
91
- map[klass.xmi_id] << parent_class.xmi_id
92
- end
93
- end
94
- end
66
+ add_generalization_entries(map, klass)
95
67
  end
96
68
 
97
69
  map
@@ -101,6 +73,54 @@ module Lutaml
101
73
  @class_lookup ||= ClassLookupIndex.new(repository.classes_index)
102
74
  end
103
75
 
76
+ def build_package_tree
77
+ Serializers::PackageTreeBuilder.new(repository, id_generator).build
78
+ end
79
+
80
+ def build_packages_map
81
+ Serializers::PackageSerializer.new(repository, id_generator,
82
+ options).build_map
83
+ end
84
+
85
+ def build_classes_map
86
+ Serializers::ClassSerializer.new(repository, id_generator,
87
+ options, inheritance_resolver).build_map
88
+ end
89
+
90
+ def build_attributes_map
91
+ Serializers::AttributeSerializer.new(repository, id_generator,
92
+ options).build_map
93
+ end
94
+
95
+ def build_operations_map
96
+ Serializers::OperationSerializer.new(repository,
97
+ id_generator).build_map
98
+ end
99
+
100
+ def build_diagrams_map
101
+ return {} unless options[:include_diagrams]
102
+
103
+ Serializers::DiagramSerializer.new(repository, id_generator,
104
+ options).build_map
105
+ end
106
+
107
+ def add_generalization_entries(map, klass)
108
+ return unless klass.association_generalization
109
+ return if klass.association_generalization.empty?
110
+
111
+ klass.association_generalization.each do |assoc_gen|
112
+ parent_object_id = assoc_gen.parent_object_id
113
+ next unless parent_object_id
114
+
115
+ parent_class = class_lookup.by_object_id(parent_object_id)
116
+ next unless parent_class&.xmi_id
117
+ next if parent_class.xmi_id == klass.xmi_id
118
+ next if map[klass.xmi_id].include?(parent_class.xmi_id)
119
+
120
+ map[klass.xmi_id] << parent_class.xmi_id
121
+ end
122
+ end
123
+
104
124
  def format_definition(definition)
105
125
  return nil if definition.nil? || definition.empty?
106
126
 
@@ -51,23 +51,9 @@ module Lutaml
51
51
 
52
52
  def build_document_store
53
53
  documents = []
54
-
55
- repository.classes_index.each do |klass|
56
- documents << build_class_document(klass)
57
-
58
- klass.attributes&.each do |attr|
59
- documents << build_attribute_document(attr, klass)
60
- end
61
- end
62
-
63
- repository.associations_index.each do |assoc|
64
- documents << build_association_document(assoc)
65
- end
66
-
67
- repository.packages_index.each do |package|
68
- documents << build_package_document(package)
69
- end
70
-
54
+ documents.concat(build_class_documents)
55
+ documents.concat(build_association_documents)
56
+ documents.concat(build_package_documents)
71
57
  documents
72
58
  end
73
59
 
@@ -135,8 +121,8 @@ module Lutaml
135
121
  class_type(klass),
136
122
  Array(klass.stereotype).join(" "),
137
123
  klass.definition,
138
- klass.attributes&.map(&:name)&.join(" "),
139
- klass.operations&.map(&:name)&.join(" "),
124
+ collect_names(klass.attributes),
125
+ collect_names(klass.operations),
140
126
  ].compact
141
127
 
142
128
  normalize_content(parts.join(" "))
@@ -186,6 +172,33 @@ module Lutaml
186
172
  all_content.join(" ").gsub(/\s+/, " ").strip
187
173
  end
188
174
 
175
+ def build_class_documents
176
+ docs = []
177
+ repository.classes_index.each do |klass|
178
+ docs << build_class_document(klass)
179
+ klass.attributes&.each do |attr|
180
+ docs << build_attribute_document(attr, klass)
181
+ end
182
+ end
183
+ docs
184
+ end
185
+
186
+ def build_association_documents
187
+ repository.associations_index.map do |assoc|
188
+ build_association_document(assoc)
189
+ end
190
+ end
191
+
192
+ def build_package_documents
193
+ repository.packages_index.map do |package|
194
+ build_package_document(package)
195
+ end
196
+ end
197
+
198
+ def collect_names(items)
199
+ items&.map(&:name)&.join(" ")
200
+ end
201
+
189
202
  def class_type(klass)
190
203
  klass.class.name.split("::").last
191
204
  end
@@ -30,10 +30,7 @@ inheritance_resolver)
30
30
 
31
31
  private
32
32
 
33
- def serialize(klass, id) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
34
- class_associations = find_class_associations(klass)
35
- sorted_associations = sort_associations(class_associations, klass)
36
-
33
+ def serialize(klass, id)
37
34
  Models::SpaClass.new(
38
35
  id: id,
39
36
  xmi_id: klass.xmi_id,
@@ -43,10 +40,10 @@ inheritance_resolver)
43
40
  package: package_id_for_class(klass),
44
41
  stereotypes: normalize_stereotypes(klass.stereotype),
45
42
  definition: format_definition(klass.definition),
46
- attributes: (klass.attributes || []).sort_by { |a| a.name || "" }
47
- .map { |attr| @id_generator.attribute_id(attr, klass) },
43
+ attributes: serialize_attribute_ids(klass),
48
44
  operations: serialize_class_operations(klass),
49
- associations: sorted_associations,
45
+ associations: sort_associations(find_class_associations(klass),
46
+ klass),
50
47
  generalizations: @inheritance_resolver.find_generalizations(klass),
51
48
  specializations: @inheritance_resolver.find_specializations(klass),
52
49
  is_abstract: klass.is_abstract,
@@ -56,6 +53,11 @@ inheritance_resolver)
56
53
  )
57
54
  end
58
55
 
56
+ def serialize_attribute_ids(klass)
57
+ (klass.attributes || []).sort_by { |a| a.name || "" }
58
+ .map { |attr| @id_generator.attribute_id(attr, klass) }
59
+ end
60
+
59
61
  def find_class_associations(klass)
60
62
  associations = @repository.associations_of(klass)
61
63
  associations.map { |assoc| @id_generator.association_id(assoc) }
@@ -65,18 +67,26 @@ inheritance_resolver)
65
67
 
66
68
  def sort_associations(assoc_ids, klass)
67
69
  assoc_ids.sort_by do |assoc_id|
68
- assoc = @repository.associations_index.find do |a|
69
- @id_generator.association_id(a) == assoc_id
70
- end
70
+ assoc = find_assoc_by_id(assoc_id)
71
71
  next "" unless assoc
72
72
 
73
- if assoc.owner_end_xmi_id == klass.xmi_id
74
- assoc.owner_end_attribute_name || assoc.owner_end || ""
75
- elsif assoc.member_end_xmi_id == klass.xmi_id
76
- assoc.member_end_attribute_name || assoc.member_end || ""
77
- else
78
- ""
79
- end
73
+ resolve_assoc_role(assoc, klass)
74
+ end
75
+ end
76
+
77
+ def find_assoc_by_id(assoc_id)
78
+ @repository.associations_index.find do |a|
79
+ @id_generator.association_id(a) == assoc_id
80
+ end
81
+ end
82
+
83
+ def resolve_assoc_role(assoc, klass)
84
+ if assoc.owner_end_xmi_id == klass.xmi_id
85
+ assoc.owner_end_attribute_name || assoc.owner_end || ""
86
+ elsif assoc.member_end_xmi_id == klass.xmi_id
87
+ assoc.member_end_attribute_name || assoc.member_end || ""
88
+ else
89
+ ""
80
90
  end
81
91
  end
82
92