lutaml 0.10.4 → 0.10.6

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 (119) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +8 -0
  3. data/.rubocop.yml +10 -0
  4. data/.rubocop_todo.yml +218 -94
  5. data/TODO.cleanups/01-resolve-production-todos.md +65 -0
  6. data/TODO.cleanups/02-reduce-metrics-offenses.md +37 -0
  7. data/TODO.cleanups/03-reduce-rspec-multiple-expectations.md +54 -0
  8. data/TODO.cleanups/04-reduce-rspec-example-length.md +45 -0
  9. data/TODO.cleanups/05-replace-marshal-load.md +37 -0
  10. data/TODO.cleanups/06-replace-eval-in-tests.md +41 -0
  11. data/TODO.cleanups/07-fix-lint-offenses.md +74 -0
  12. data/TODO.cleanups/08-reduce-memoized-helpers-and-nesting.md +43 -0
  13. data/TODO.cleanups/09-reduce-verified-doubles-and-rspec-style.md +57 -0
  14. data/TODO.cleanups/10-split-large-files.md +47 -0
  15. data/bin/console +0 -1
  16. data/exe/lutaml +1 -0
  17. data/lib/lutaml/cli/element_identifier.rb +3 -6
  18. data/lib/lutaml/cli/interactive_shell/bookmark_commands.rb +88 -0
  19. data/lib/lutaml/cli/interactive_shell/command_base.rb +32 -0
  20. data/lib/lutaml/cli/interactive_shell/export_handler.rb +67 -0
  21. data/lib/lutaml/cli/interactive_shell/help_display.rb +114 -0
  22. data/lib/lutaml/cli/interactive_shell/navigation_commands.rb +135 -0
  23. data/lib/lutaml/cli/interactive_shell/query_commands.rb +185 -0
  24. data/lib/lutaml/cli/interactive_shell.rb +116 -802
  25. data/lib/lutaml/cli/uml/build_command.rb +5 -5
  26. data/lib/lutaml/cli/uml/verify_command.rb +0 -1
  27. data/lib/lutaml/converter/xmi_to_uml.rb +3 -153
  28. data/lib/lutaml/converter/xmi_to_uml_generalization.rb +193 -0
  29. data/lib/lutaml/formatter/graphviz.rb +1 -2
  30. data/lib/lutaml/qea/database.rb +1 -47
  31. data/lib/lutaml/qea/factory/association_builder.rb +188 -0
  32. data/lib/lutaml/qea/factory/base_transformer.rb +0 -1
  33. data/lib/lutaml/qea/factory/class_transformer.rb +40 -590
  34. data/lib/lutaml/qea/factory/diagram_transformer.rb +0 -3
  35. data/lib/lutaml/qea/factory/generalization_builder.rb +211 -0
  36. data/lib/lutaml/qea/factory/package_transformer.rb +1 -2
  37. data/lib/lutaml/qea/factory/stereotype_loader.rb +34 -0
  38. data/lib/lutaml/qea/lookup_indexes.rb +54 -0
  39. data/lib/lutaml/qea/models/ea_datatype.rb +0 -2
  40. data/lib/lutaml/qea/validation/validation_engine.rb +0 -2
  41. data/lib/lutaml/uml/has_members.rb +0 -1
  42. data/lib/lutaml/uml/inheritance_walker.rb +92 -0
  43. data/lib/lutaml/uml/model_helpers.rb +129 -0
  44. data/lib/lutaml/uml/node/attribute.rb +3 -1
  45. data/lib/lutaml/uml/node/class_node.rb +3 -3
  46. data/lib/lutaml/uml/operation.rb +2 -0
  47. data/lib/lutaml/uml_repository/class_lookup_index.rb +40 -0
  48. data/lib/lutaml/uml_repository/exporters/markdown/class_page_builder.rb +179 -0
  49. data/lib/lutaml/uml_repository/exporters/markdown/formatting.rb +36 -0
  50. data/lib/lutaml/uml_repository/exporters/markdown/index_page_builder.rb +73 -0
  51. data/lib/lutaml/uml_repository/exporters/markdown/link_resolver.rb +40 -0
  52. data/lib/lutaml/uml_repository/exporters/markdown/package_page_builder.rb +107 -0
  53. data/lib/lutaml/uml_repository/exporters/markdown_exporter.rb +26 -538
  54. data/lib/lutaml/uml_repository/index_builder.rb +3 -271
  55. data/lib/lutaml/uml_repository/index_builders/association_index.rb +141 -0
  56. data/lib/lutaml/uml_repository/index_builders/class_index.rb +94 -0
  57. data/lib/lutaml/uml_repository/index_builders/package_index.rb +57 -0
  58. data/lib/lutaml/uml_repository/package_exporter.rb +10 -20
  59. data/lib/lutaml/uml_repository/package_loader.rb +37 -17
  60. data/lib/lutaml/uml_repository/repository/deprecated.rb +39 -0
  61. data/lib/lutaml/uml_repository/repository/loader.rb +112 -0
  62. data/lib/lutaml/uml_repository/repository.rb +7 -57
  63. data/lib/lutaml/uml_repository/static_site/association_serialization.rb +142 -0
  64. data/lib/lutaml/uml_repository/static_site/configuration.rb +0 -2
  65. data/lib/lutaml/uml_repository/static_site/data_transformer.rb +52 -873
  66. data/lib/lutaml/uml_repository/static_site/generator.rb +29 -8
  67. data/lib/lutaml/uml_repository/static_site/search_index_builder.rb +1 -4
  68. data/lib/lutaml/uml_repository/static_site/serializers/attribute_serializer.rb +78 -0
  69. data/lib/lutaml/uml_repository/static_site/serializers/class_serializer.rb +124 -0
  70. data/lib/lutaml/uml_repository/static_site/serializers/diagram_serializer.rb +60 -0
  71. data/lib/lutaml/uml_repository/static_site/serializers/inheritance_resolver.rb +258 -0
  72. data/lib/lutaml/uml_repository/static_site/serializers/metadata_builder.rb +48 -0
  73. data/lib/lutaml/uml_repository/static_site/serializers/operation_serializer.rb +57 -0
  74. data/lib/lutaml/uml_repository/static_site/serializers/package_serializer.rb +94 -0
  75. data/lib/lutaml/uml_repository/static_site/serializers/package_tree_builder.rb +93 -0
  76. data/lib/lutaml/version.rb +1 -1
  77. data/lib/lutaml/xmi/liquid_drops/association_drop.rb +13 -35
  78. data/lib/lutaml/xmi/liquid_drops/attribute_drop.rb +12 -18
  79. data/lib/lutaml/xmi/liquid_drops/cardinality_drop.rb +14 -6
  80. data/lib/lutaml/xmi/liquid_drops/connector_drop.rb +0 -3
  81. data/lib/lutaml/xmi/liquid_drops/constraint_drop.rb +1 -3
  82. data/lib/lutaml/xmi/liquid_drops/data_type_drop.rb +13 -70
  83. data/lib/lutaml/xmi/liquid_drops/dependency_drop.rb +2 -5
  84. data/lib/lutaml/xmi/liquid_drops/diagram_drop.rb +5 -11
  85. data/lib/lutaml/xmi/liquid_drops/enum_drop.rb +8 -16
  86. data/lib/lutaml/xmi/liquid_drops/enum_owned_literal_drop.rb +3 -9
  87. data/lib/lutaml/xmi/liquid_drops/generalization_attribute_drop.rb +11 -13
  88. data/lib/lutaml/xmi/liquid_drops/generalization_drop.rb +27 -85
  89. data/lib/lutaml/xmi/liquid_drops/klass_drop.rb +39 -91
  90. data/lib/lutaml/xmi/liquid_drops/operation_drop.rb +3 -9
  91. data/lib/lutaml/xmi/liquid_drops/package_drop.rb +16 -44
  92. data/lib/lutaml/xmi/liquid_drops/root_drop.rb +3 -11
  93. data/lib/lutaml/xmi/liquid_drops/source_target_drop.rb +2 -5
  94. data/lib/lutaml/xmi/parsers/xmi_base.rb +2 -749
  95. data/lib/lutaml/xmi/parsers/xmi_class_members.rb +45 -0
  96. data/lib/lutaml/xmi/parsers/xmi_connector.rb +251 -0
  97. data/lib/lutaml/xmi/parsers/xml.rb +7 -120
  98. data/lib/lutaml/xmi/xmi_lookup_service.rb +42 -0
  99. data/lib/lutaml.rb +0 -1
  100. metadata +48 -21
  101. data/lib/lutaml/cli/commands/base_command.rb +0 -118
  102. data/lib/lutaml/command_line.rb +0 -272
  103. data/lib/lutaml/sysml/allocate.rb +0 -9
  104. data/lib/lutaml/sysml/allocated.rb +0 -9
  105. data/lib/lutaml/sysml/binding_connector.rb +0 -9
  106. data/lib/lutaml/sysml/block.rb +0 -32
  107. data/lib/lutaml/sysml/constraint_block.rb +0 -14
  108. data/lib/lutaml/sysml/copy.rb +0 -8
  109. data/lib/lutaml/sysml/derive_requirement.rb +0 -9
  110. data/lib/lutaml/sysml/nested_connector_end.rb +0 -13
  111. data/lib/lutaml/sysml/refine.rb +0 -9
  112. data/lib/lutaml/sysml/requirement.rb +0 -44
  113. data/lib/lutaml/sysml/requirement_related.rb +0 -9
  114. data/lib/lutaml/sysml/satisfy.rb +0 -9
  115. data/lib/lutaml/sysml/test_case.rb +0 -25
  116. data/lib/lutaml/sysml/trace.rb +0 -9
  117. data/lib/lutaml/sysml/verify.rb +0 -8
  118. data/lib/lutaml/sysml/xmi_file.rb +0 -486
  119. data/lib/lutaml/sysml.rb +0 -11
@@ -1,33 +1,27 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "id_generator"
4
+ require_relative "../../uml/model_helpers"
5
+ require_relative "../class_lookup_index"
6
+ require_relative "association_serialization"
7
+ require_relative "serializers/metadata_builder"
8
+ require_relative "serializers/package_tree_builder"
9
+ require_relative "serializers/package_serializer"
10
+ require_relative "serializers/class_serializer"
11
+ require_relative "serializers/attribute_serializer"
12
+ require_relative "serializers/operation_serializer"
13
+ require_relative "serializers/diagram_serializer"
14
+ require_relative "serializers/inheritance_resolver"
4
15
 
5
16
  module Lutaml
6
17
  module UmlRepository
7
18
  module StaticSite
8
- # Transforms a UmlRepository into a normalized JSON data structure
9
- # optimized for client-side navigation and search.
10
- #
11
- # The output follows a normalized structure with:
12
- # - Flat maps for packages, classes, attributes, associations
13
- # - References by stable IDs
14
- # - Hierarchical package tree for navigation
15
- #
16
- # @example
17
- # repository = UmlRepository.from_package("model.lur")
18
- # transformer = DataTransformer.new(repository)
19
- # json_data = transformer.transform
20
19
  class DataTransformer
20
+ include AssociationSerialization
21
+ include Lutaml::Uml::ModelHelpers
22
+
21
23
  attr_reader :repository, :id_generator, :options
22
24
 
23
- # Initialize transformer
24
- #
25
- # @param repository [UmlRepository] The repository to transform
26
- # @param options [Hash] Transformation options
27
- # @option options [Boolean] :include_diagrams Include diagram
28
- # information
29
- # @option options [Boolean] :format_definitions Format definitions
30
- # as markdown
31
25
  def initialize(repository, options = {})
32
26
  @repository = repository
33
27
  @options = default_options.merge(options)
@@ -35,19 +29,27 @@ module Lutaml
35
29
  @generalization_map = build_generalization_map
36
30
  end
37
31
 
38
- # Transform repository to JSON structure
39
- #
40
- # @return [Hash] Normalized JSON data structure
41
32
  def transform
42
33
  {
43
- metadata: build_metadata,
44
- packageTree: build_package_tree,
45
- packages: build_packages_map,
46
- classes: build_classes_map,
47
- attributes: build_attributes_map,
34
+ metadata: Serializers::MetadataBuilder.new(repository).build,
35
+ packageTree: Serializers::PackageTreeBuilder.new(repository,
36
+ id_generator).build,
37
+ packages: Serializers::PackageSerializer.new(repository,
38
+ id_generator, options).build_map,
39
+ classes: Serializers::ClassSerializer.new(repository, id_generator,
40
+ options, inheritance_resolver).build_map,
41
+ attributes: Serializers::AttributeSerializer.new(repository,
42
+ id_generator, options).build_map,
48
43
  associations: build_associations_map,
49
- operations: build_operations_map,
50
- diagrams: (@options[:include_diagrams] ? build_diagrams_map : {}),
44
+ operations: Serializers::OperationSerializer.new(repository,
45
+ id_generator).build_map,
46
+ diagrams: (if options[:include_diagrams]
47
+ Serializers::DiagramSerializer.new(
48
+ repository, id_generator, options
49
+ ).build_map
50
+ else
51
+ {}
52
+ end),
51
53
  }
52
54
  end
53
55
 
@@ -61,32 +63,29 @@ module Lutaml
61
63
  }
62
64
  end
63
65
 
64
- # Build generalization map for multiple inheritance
65
- def build_generalization_map # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
66
+ def inheritance_resolver
67
+ @inheritance_resolver ||= Serializers::InheritanceResolver.new(
68
+ repository, id_generator, options, @generalization_map
69
+ )
70
+ end
71
+
72
+ def build_generalization_map
66
73
  map = Hash.new { |h, k| h[k] = [] }
67
74
 
68
- # Scan all classes for generalization relationships
69
75
  repository.classes_index.each do |klass|
70
76
  next unless klass.respond_to?(:association_generalization)
71
- unless klass.association_generalization &&
72
- !klass.association_generalization.empty?
77
+ unless klass.association_generalization && !klass.association_generalization.empty?
73
78
  next
74
79
  end
75
80
 
76
- # Each class has an association_generalization array with
77
- # AssociationGeneralization objects
78
81
  klass.association_generalization.each do |assoc_gen|
79
- # Access lutaml-model object attributes directly
80
82
  next unless assoc_gen.respond_to?(:parent_object_id)
81
83
 
82
84
  parent_object_id = assoc_gen.parent_object_id
83
85
  next unless parent_object_id
84
86
 
85
- # Find the parent class by object_id and get its XMI ID
86
- parent_class = find_class_by_object_id(parent_object_id)
87
+ parent_class = class_lookup.by_object_id(parent_object_id)
87
88
  if parent_class&.xmi_id
88
- # Skip self-referential generalization
89
- # (class can't be its own parent)
90
89
  next if parent_class.xmi_id == klass.xmi_id
91
90
 
92
91
  unless map[klass.xmi_id].include?(parent_class.xmi_id)
@@ -99,545 +98,22 @@ module Lutaml
99
98
  map
100
99
  end
101
100
 
102
- # Build metadata section
103
- def build_metadata
104
- {
105
- generated: Time.now.utc.iso8601,
106
- generator: "LutaML Static Site Generator",
107
- version: "1.0",
108
- statistics: build_statistics,
109
- }
110
- end
111
-
112
- # Build statistics
113
- def build_statistics
114
- {
115
- packages: repository.packages_index.size,
116
- classes: repository.classes_index.size,
117
- associations: repository.associations_index.size,
118
- attributes: count_total_attributes,
119
- operations: count_total_operations,
120
- }
121
- end
122
-
123
- def count_total_attributes
124
- repository.classes_index.sum do |klass|
125
- klass.attributes&.size || 0
126
- end
127
- end
128
-
129
- def count_total_operations
130
- repository.classes_index.sum do |klass|
131
- (klass.respond_to?(:operations) ? klass.operations&.size : 0) || 0
132
- end
133
- end
134
-
135
- # Build hierarchical package tree
136
- def build_package_tree # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
137
- # Get root packages from document.packages (not from index)
138
- root_packages = if repository.document.respond_to?(:packages) &&
139
- repository.document.packages
140
- repository.document.packages
141
- else
142
- # Fallback: find packages without parent namespace
143
- repository.packages_index.select do |pkg|
144
- !pkg.respond_to?(:namespace) ||
145
- pkg.namespace.nil? ||
146
- !pkg.namespace.is_a?(Lutaml::Uml::Package)
147
- end
148
- end
149
-
150
- if root_packages.size == 1
151
- build_tree_node(root_packages.first)
152
- else
153
- # Multiple roots - create virtual root
154
- {
155
- id: "root",
156
- name: "Model",
157
- path: "",
158
- classCount: 0,
159
- children: root_packages.map { |pkg| build_tree_node(pkg) },
160
- }
161
- end
162
- end
163
-
164
- def build_tree_node(package) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
165
- pkg_id = @id_generator.package_id(package)
166
-
167
- # Sort child packages by name
168
- sorted_children = (package.packages || []).sort_by do |p|
169
- p.name || ""
170
- end
171
-
172
- # Sort classes by name, filtering out unnamed classes
173
- # This prevents unnamed classes from appearing in the tree or being
174
- # counted
175
- sorted_classes = (package.classes || [])
176
- .reject { |c| c.name.nil? || c.name.empty? }
177
- .sort_by(&:name)
178
-
179
- # Build child nodes first to get their counts
180
- child_nodes = sorted_children.map do |child|
181
- build_tree_node(child)
182
- end
183
-
184
- # Calculate total count including nested packages
185
- # Only counts named classes (unnamed classes are already filtered out)
186
- total_class_count = sorted_classes.size + child_nodes.sum do |child|
187
- child[:classCount] || 0
188
- end
189
-
190
- {
191
- id: pkg_id,
192
- name: package.name,
193
- path: package_path(package),
194
- stereotypes: normalize_stereotypes(
195
- package.respond_to?(:stereotype) ? package.stereotype : nil,
196
- ),
197
- classCount: total_class_count,
198
- classes: sorted_classes.map do |c|
199
- {
200
- id: @id_generator.class_id(c),
201
- name: c.name,
202
- stereotypes: normalize_stereotypes(
203
- c.respond_to?(:stereotype) ? c.stereotype : nil,
204
- ),
205
- }
206
- end,
207
- children: child_nodes,
208
- }
101
+ def class_lookup
102
+ @class_lookup ||= ClassLookupIndex.new(repository.classes_index)
209
103
  end
210
104
 
211
- # Build packages map
212
- def build_packages_map
213
- packages = {}
214
-
215
- repository.packages_index.each do |package|
216
- id = @id_generator.package_id(package)
217
- packages[id] = serialize_package(package, id)
218
- end
219
-
220
- packages
221
- end
222
-
223
- def serialize_package(package, id) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
224
- {
225
- id: id,
226
- xmiId: package.respond_to?(:xmi_id) ? package.xmi_id : nil,
227
- name: package.name,
228
- path: package_path(package),
229
- definition: format_definition(
230
- package.respond_to?(:definition) ? package.definition : nil,
231
- ),
232
- stereotypes: normalize_stereotypes(
233
- package.respond_to?(:stereotype) ? package.stereotype : nil,
234
- ),
235
- classes: (package.classes || []).map do |c|
236
- @id_generator.class_id(c)
237
- end,
238
- subPackages: (package.packages || []).map do |p|
239
- @id_generator.package_id(p)
240
- end,
241
- diagrams: package_diagrams(package).map do |d|
242
- @id_generator.diagram_id(d)
243
- end,
244
- parent: if package.respond_to?(:namespace) &&
245
- package.namespace.is_a?(Lutaml::Uml::Package)
246
- @id_generator.package_id(package.namespace)
247
- end,
248
- }
249
- end
250
-
251
- # Build classes map
252
- def build_classes_map
253
- classes = {}
254
-
255
- repository.classes_index.each do |klass|
256
- id = @id_generator.class_id(klass)
257
- classes[id] = serialize_class(klass, id)
258
- end
259
-
260
- classes
261
- end
262
-
263
- def serialize_class(klass, id) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
264
- # Get associations and sort by local role
265
- class_associations = find_class_associations(klass)
266
- sorted_associations = class_associations.sort_by do |assoc_id|
267
- assoc = repository.associations_index.find do |a|
268
- @id_generator.association_id(a) == assoc_id
269
- end
270
- next "" unless assoc
271
-
272
- # Determine local role for this class
273
- if assoc.owner_end_xmi_id == klass.xmi_id
274
- assoc.owner_end_attribute_name || assoc.owner_end || ""
275
- elsif assoc.member_end_xmi_id == klass.xmi_id
276
- assoc.member_end_attribute_name || assoc.member_end || ""
277
- else
278
- ""
279
- end
280
- end
281
-
282
- {
283
- id: id,
284
- xmiId: klass.xmi_id,
285
- name: klass.name,
286
- qualifiedName: qualified_name(klass),
287
- type: class_type(klass),
288
- package: package_id_for_class(klass),
289
- stereotypes: normalize_stereotypes(
290
- if klass.respond_to?(:stereotype)
291
- klass.stereotype
292
- end,
293
- ),
294
- definition: format_definition(klass.definition),
295
- attributes: (klass.attributes || []).sort_by do |a|
296
- a.name || ""
297
- end.map do |attr|
298
- @id_generator.attribute_id(attr, klass)
299
- end,
300
- operations: serialize_class_operations(klass),
301
- associations: sorted_associations,
302
- generalizations: find_generalizations(klass),
303
- specializations: find_specializations(klass),
304
- isAbstract: if klass.respond_to?(:is_abstract)
305
- klass.is_abstract
306
- else
307
- false
308
- end,
309
- literals: serialize_literals(klass),
310
- inheritedAttributes: compute_inherited_attributes(klass),
311
- inheritedAssociations: compute_inherited_associations(klass),
312
- }
313
- end
314
-
315
- # Build attributes map
316
- def build_attributes_map
317
- attributes = {}
318
-
319
- repository.classes_index.each do |klass|
320
- next unless klass.attributes
321
-
322
- klass.attributes.each do |attr|
323
- id = @id_generator.attribute_id(attr, klass)
324
- attributes[id] = serialize_attribute(attr, klass, id)
325
- end
326
- end
327
-
328
- attributes
329
- end
330
-
331
- def serialize_attribute(attribute, owner, id) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
332
- {
333
- id: id,
334
- name: attribute.name,
335
- type: attribute.type,
336
- visibility: attribute.visibility,
337
- owner: @id_generator.class_id(owner),
338
- ownerName: owner.name,
339
- cardinality: serialize_cardinality(attribute.cardinality),
340
- definition: format_definition(attribute.definition),
341
- stereotypes: normalize_stereotypes(
342
- attribute.respond_to?(:stereotype) ? attribute.stereotype : nil,
343
- ),
344
- isStatic: if attribute.respond_to?(:is_static)
345
- attribute.is_static
346
- else
347
- false
348
- end,
349
- isReadOnly: if attribute.respond_to?(:is_read_only)
350
- attribute.is_read_only
351
- else
352
- false
353
- end,
354
- defaultValue: if attribute.respond_to?(:default)
355
- attribute.default
356
- end,
357
- }
358
- end
359
-
360
- # Build associations map
361
- # Uses repository.associations_index which handles both XMI
362
- # and QEA formats
363
- def build_associations_map
364
- associations = {}
365
-
366
- # Repository.associations_index collects from both:
367
- # - Document-level associations (XMI format)
368
- # - Class-level associations (QEA/EA format)
369
- repository.associations_index.each do |assoc|
370
- id = @id_generator.association_id(assoc)
371
- associations[id] = serialize_association(assoc, id)
372
- end
373
-
374
- associations
375
- end
376
-
377
- def serialize_association(association, id) # rubocop:disable Metrics/MethodLength
378
- # Association model has member_end/owner_end as strings (class names)
379
- # Use member_end_xmi_id, member_end_type etc for more details
380
- # IMPORTANT: Do NOT generate synthetic names - use actual data only
381
-
382
- # If association.name is nil, use the role name as fallback
383
- # In EA models, the association name is often stored in the role
384
- # fields
385
- # IMPORTANT: Prioritize owner_end_attribute_name which has the
386
- # actual role name
387
- assoc_name = association.name
388
- if assoc_name.nil? || assoc_name.empty?
389
- # Try owner_end_attribute_name first (this is the role from owner's
390
- # perspective)
391
- assoc_name = association.owner_end_attribute_name
392
- # Fallback to member_end_attribute_name (but this often contains
393
- # class name, not role)
394
- assoc_name = if assoc_name.nil? || assoc_name.empty?
395
- association.member_end_attribute_name
396
- end
397
- end
398
-
399
- {
400
- id: id,
401
- xmiId: association.xmi_id,
402
- name: assoc_name,
403
- type: "Association",
404
- definition: format_definition(
405
- if association.respond_to?(:definition)
406
- association.definition
407
- end,
408
- ),
409
- source: build_association_source(association),
410
- target: build_association_target(association),
411
- }
412
- end
413
-
414
- def build_association_source(association)
415
- return nil unless association.owner_end
416
-
417
- {
418
- class: association.owner_end_xmi_id,
419
- className: association.owner_end,
420
- role: association.owner_end_attribute_name,
421
- cardinality: serialize_cardinality(
422
- association.owner_end_cardinality,
423
- ),
424
- aggregation: association.owner_end_type,
425
- }
426
- end
427
-
428
- def build_association_target(association)
429
- return nil unless association.member_end
430
-
431
- {
432
- class: association.member_end_xmi_id,
433
- className: association.member_end,
434
- role: association.member_end_attribute_name,
435
- cardinality: serialize_cardinality(
436
- association.member_end_cardinality,
437
- ),
438
- aggregation: association.member_end_type,
439
- }
440
- end
441
-
442
- def serialize_association_end(end_obj) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
443
- return nil unless end_obj
444
- return nil unless end_obj.respond_to?(:type) && end_obj.type
445
-
446
- # end_obj.type can be a String (class name) or a Class object
447
- type_value = end_obj.type
448
-
449
- if type_value.is_a?(String)
450
- # Type is a string reference (class name)
451
- {
452
- class: nil, # Can't generate ID without class object
453
- className: type_value,
454
- role: end_obj.respond_to?(:name) ? end_obj.name : nil,
455
- cardinality: serialize_cardinality(
456
- if end_obj.respond_to?(:cardinality)
457
- end_obj.cardinality
458
- end,
459
- ),
460
- navigable: if end_obj.respond_to?(:navigable?)
461
- end_obj.navigable?
462
- else
463
- false
464
- end,
465
- aggregation: if end_obj.respond_to?(:aggregation)
466
- end_obj.aggregation
467
- end,
468
- visibility: if end_obj.respond_to?(:visibility)
469
- end_obj.visibility
470
- end,
471
- }
472
- else
473
- # Type is a class object
474
- {
475
- class: @id_generator.class_id(type_value),
476
- className: if type_value.respond_to?(:name)
477
- type_value.name
478
- else
479
- type_value.to_s
480
- end,
481
- role: end_obj.respond_to?(:name) ? end_obj.name : nil,
482
- cardinality: serialize_cardinality(
483
- if end_obj.respond_to?(:cardinality)
484
- end_obj.cardinality
485
- end,
486
- ),
487
- navigable: if end_obj.respond_to?(:navigable?)
488
- end_obj.navigable?
489
- else
490
- false
491
- end,
492
- aggregation: if end_obj.respond_to?(:aggregation)
493
- end_obj.aggregation
494
- end,
495
- visibility: if end_obj.respond_to?(:visibility)
496
- end_obj.visibility
497
- end,
498
- }
499
- end
500
- end
501
-
502
- # Build operations map
503
- def build_operations_map
504
- operations = {}
505
-
506
- repository.classes_index.each do |klass|
507
- next unless klass.respond_to?(:operations) && klass.operations
508
-
509
- klass.operations.each do |op|
510
- id = @id_generator.operation_id(op, klass)
511
- operations[id] = serialize_operation(op, klass, id)
512
- end
513
- end
514
-
515
- operations
516
- end
517
-
518
- def serialize_operation(operation, owner, id) # rubocop:disable Metrics/MethodLength
519
- {
520
- id: id,
521
- name: operation.name,
522
- visibility: operation.visibility,
523
- returnType: operation.return_type,
524
- owner: @id_generator.class_id(owner),
525
- ownerName: owner.name,
526
- parameters: serialize_parameters(operation),
527
- isStatic: if operation.respond_to?(:is_static)
528
- operation.is_static
529
- else
530
- false
531
- end,
532
- isAbstract: if operation.respond_to?(:is_abstract)
533
- operation.is_abstract
534
- else
535
- false
536
- end,
537
- }
538
- end
539
-
540
- def serialize_parameters(operation) # rubocop:disable Metrics/MethodLength
541
- unless operation.respond_to?(:owned_parameter) &&
542
- operation.owned_parameter
543
- return []
544
- end
545
-
546
- operation.owned_parameter.map do |param|
547
- {
548
- name: param.name,
549
- type: param.type,
550
- direction: param.respond_to?(:direction) ? param.direction : "in",
551
- }
552
- end
553
- end
554
-
555
- # Build diagrams map
556
- def build_diagrams_map
557
- diagrams = {}
558
-
559
- repository.diagrams_index.each do |diagram|
560
- id = @id_generator.diagram_id(diagram)
561
- diagrams[id] = serialize_diagram(diagram, id)
562
- end
105
+ def find_class_by_xmi_id(xmi_id)
106
+ return nil unless xmi_id
563
107
 
564
- diagrams
108
+ class_lookup.by_xmi_id(xmi_id)
565
109
  rescue StandardError
566
- # Diagrams may not be available in all repositories
567
- {}
568
- end
569
-
570
- def serialize_diagram(diagram, id)
571
- {
572
- id: id,
573
- xmiId: diagram.xmi_id,
574
- name: diagram.name,
575
- type: diagram.diagram_type,
576
- package: find_diagram_package(diagram),
577
- }
578
- end
579
-
580
- # Helper methods
581
-
582
- def package_path(package)
583
- unless package.respond_to?(:namespace) && package.namespace
584
- return package.name
585
- end
586
- return package.name unless package.namespace.is_a?(Lutaml::Uml::Package)
587
-
588
- "#{package_path(package.namespace)}::#{package.name}"
589
- end
590
-
591
- def qualified_name(klass) # rubocop:disable Metrics/MethodLength
592
- path_parts = []
593
- current = klass
594
-
595
- # Walk up the namespace chain
596
- while current
597
- if current.is_a?(Lutaml::Uml::TopElement)
598
- path_parts.unshift(current.name)
599
- current = current.namespace if current.respond_to?(:namespace)
600
- elsif current.is_a?(Lutaml::Uml::Package)
601
- path_parts.unshift(current.name)
602
- current = current.namespace
603
- else
604
- break
605
- end
606
- end
607
-
608
- path_parts.join("::")
609
- end
610
-
611
- def class_type(klass)
612
- klass.class.name.split("::").last
613
- end
614
-
615
- def package_id_for_class(klass)
616
- ns = klass.respond_to?(:namespace) ? klass.namespace : nil
617
- return nil unless ns.is_a?(Lutaml::Uml::Package)
618
-
619
- @id_generator.package_id(ns)
110
+ nil
620
111
  end
621
112
 
622
- def package_diagrams(package)
623
- return [] unless @options[:include_diagrams]
624
-
625
- # Use the package's direct diagrams attribute instead of querying
626
- package.diagrams || []
627
- rescue StandardError => e
628
- warn "Error getting diagrams for #{package.name}: #{e.message}"
629
- []
630
- end
113
+ def find_class_by_object_id(object_id)
114
+ return nil unless object_id
631
115
 
632
- def find_diagram_package(diagram)
633
- # Try to find which package owns this diagram
634
- repository.packages_index.each do |pkg|
635
- diagrams = package_diagrams(pkg)
636
- if diagrams.any? { |d| d.xmi_id == diagram.xmi_id }
637
- return @id_generator.package_id(pkg)
638
- end
639
- end
640
- nil
116
+ class_lookup.by_object_id(object_id)
641
117
  rescue StandardError
642
118
  nil
643
119
  end
@@ -651,316 +127,19 @@ module Lutaml
651
127
  }
652
128
  end
653
129
 
654
- def format_definition(definition) # rubocop:disable Metrics/MethodLength
130
+ def format_definition(definition)
655
131
  return nil if definition.nil? || definition.empty?
656
132
 
657
133
  formatted = definition.strip
658
-
659
- # Optionally truncate
660
134
  if @options[:max_definition_length] &&
661
135
  formatted.length > @options[:max_definition_length]
662
136
  formatted = "#{formatted[0...@options[:max_definition_length]]}..."
663
137
  end
664
-
665
- # Optionally format as markdown (basic)
666
138
  if @options[:format_definitions]
667
- formatted = format_as_markdown(formatted)
139
+ formatted = formatted.gsub(%r{(https?://[^\s]+)}, '[\1](\1)')
668
140
  end
669
-
670
141
  formatted
671
142
  end
672
-
673
- def format_as_markdown(text)
674
- # Basic markdown formatting
675
- # - Preserve line breaks
676
- # - Convert URLs to links
677
- text.gsub(%r{(https?://[^\s]+)}, '[\1](\1)')
678
- end
679
-
680
- def serialize_class_operations(klass)
681
- return [] unless klass.respond_to?(:operations) && klass.operations
682
-
683
- klass.operations.map { |op| @id_generator.operation_id(op, klass) }
684
- end
685
-
686
- def find_class_associations(klass)
687
- associations = repository.associations_of(klass)
688
- associations.map { |assoc| @id_generator.association_id(assoc) }
689
- rescue StandardError
690
- []
691
- end
692
-
693
- def find_generalizations(klass) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
694
- # Use the pre-built generalization map for multiple inheritance
695
- parent_xmi_ids = @generalization_map[klass.xmi_id]
696
-
697
- if parent_xmi_ids && !parent_xmi_ids.empty?
698
- # Map each parent XMI ID to class ID
699
- parents = parent_xmi_ids.filter_map do |parent_xmi_id|
700
- # Skip self-referential generalization
701
- next if parent_xmi_id == klass.xmi_id
702
-
703
- parent = find_class_by_xmi_id(parent_xmi_id)
704
- parent ? @id_generator.class_id(parent) : nil
705
- end
706
-
707
- return parents unless parents.empty?
708
- end
709
-
710
- # Fallback: single parent via repository query
711
- parent = repository.supertype_of(klass)
712
- # Skip if parent is self (self-referential generalization)
713
- return [] if parent && parent.xmi_id == klass.xmi_id
714
-
715
- parent ? [@id_generator.class_id(parent)] : []
716
- rescue StandardError => e
717
- warn "Error finding generalizations for #{klass.name}: #{e.message}"
718
- []
719
- end
720
-
721
- def find_specializations(klass)
722
- children = repository.subtypes_of(klass)
723
- # Filter out self if somehow included
724
- children.reject do |child|
725
- child.xmi_id == klass.xmi_id
726
- end.map { |child| @id_generator.class_id(child) }
727
- rescue StandardError
728
- []
729
- end
730
-
731
- def serialize_literals(klass)
732
- return [] unless klass.is_a?(Lutaml::Uml::Enum) && klass.owned_literal
733
-
734
- klass.owned_literal.map do |literal|
735
- {
736
- name: literal.name,
737
- definition: format_definition(literal.definition),
738
- }
739
- end
740
- rescue StandardError
741
- []
742
- end
743
-
744
- def serialize_generalization(klass, visited = Set.new) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
745
- unless klass.respond_to?(:generalization) && klass.generalization
746
- return nil
747
- end
748
- return nil if visited.include?(klass.xmi_id) # Prevent infinite loops
749
-
750
- visited.add(klass.xmi_id)
751
- gen = klass.generalization
752
-
753
- {
754
- generalId: gen.general_id,
755
- generalName: gen.general_name,
756
- generalUpperKlass: if gen.respond_to?(:general_upper_klass)
757
- gen.general_upper_klass
758
- end,
759
- hasGeneral: gen.respond_to?(:has_general) ? gen.has_general : false,
760
- name: gen.name,
761
- type: gen.type,
762
- definition: format_definition(gen.definition),
763
- stereotype: gen.respond_to?(:stereotype) ? gen.stereotype : nil,
764
- ownedProps: (gen.respond_to?(:owned_props) ? gen.owned_props : [])
765
- .map { |attr| serialize_general_attribute(attr) },
766
- assocProps: (gen.respond_to?(:assoc_props) ? gen.assoc_props : [])
767
- .map { |attr| serialize_general_attribute(attr) },
768
- inheritedProps: (
769
- gen.respond_to?(:inherited_props) ? gen.inherited_props : []
770
- ).map { |attr| serialize_general_attribute(attr) },
771
- inheritedAssocProps: (
772
- if gen.respond_to?(:inherited_assoc_props)
773
- gen.inherited_assoc_props
774
- else
775
- []
776
- end
777
- ).map { |attr| serialize_general_attribute(attr) },
778
- }
779
- rescue StandardError => e
780
- warn "Error serializing generalization: #{e.message}"
781
- nil
782
- end
783
-
784
- def serialize_general_attribute(attr)
785
- return nil unless attr
786
-
787
- {
788
- name: attr.name,
789
- type: attr.type,
790
- cardinality: serialize_cardinality(attr.cardinality),
791
- definition: format_definition(attr.definition),
792
- upperKlass: attr.respond_to?(:upper_klass) ? attr.upper_klass : nil,
793
- nameNs: attr.respond_to?(:name_ns) ? attr.name_ns : nil,
794
- typeNs: attr.respond_to?(:type_ns) ? attr.type_ns : nil,
795
- }
796
- end
797
-
798
- def find_generalization(parent_id)
799
- parent = find_class_by_xmi_id(parent_id)
800
- return nil unless parent
801
-
802
- # Recursively find all ancestors
803
- find_all_ancestors(parent, []) || []
804
- end
805
-
806
- def find_all_ancestors(klass, ancestors = [])
807
- return ancestors if klass.nil?
808
-
809
- unless ancestors.include?(klass.xmi_id)
810
- ancestors << klass.xmi_id
811
- if klass.generalization&.general_classierarchy
812
- find_all_ancestors(klass.generalization&.general_class,
813
- ancestors)
814
- end
815
- end
816
- ancestors
817
- end
818
-
819
- # Compute inherited attributes from generalization chain
820
- def compute_inherited_attributes(klass, visited = Set.new) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
821
- unless klass.respond_to?(:generalization) && klass.generalization
822
- return []
823
- end
824
- return [] if visited.include?(klass.xmi_id) # Prevent infinite loops
825
-
826
- visited.add(klass.xmi_id)
827
- inherited = []
828
- current_gen = klass.generalization
829
- parent_order = 0
830
-
831
- while current_gen
832
- parent_class = find_class_by_xmi_id(current_gen.general_id)
833
- break unless parent_class
834
- break if visited.include?(parent_class.xmi_id) # Prevent cycles
835
-
836
- visited.add(parent_class.xmi_id)
837
-
838
- if parent_class.attributes
839
- # Sort attributes by name within this parent
840
- sorted_attrs = parent_class.attributes.sort_by do |a|
841
- a.name || ""
842
- end
843
- sorted_attrs.each do |attr|
844
- attr_id = @id_generator.attribute_id(attr, parent_class)
845
- inherited << {
846
- attributeId: attr_id,
847
- attribute: serialize_attribute(attr, parent_class, attr_id),
848
- inheritedFrom: @id_generator.class_id(parent_class),
849
- inheritedFromName: parent_class.name,
850
- parentOrder: parent_order, # Track hierarchy order
851
- }
852
- end
853
- end
854
-
855
- # Move to parent's parent
856
- parent_order += 1
857
- current_gen = if current_gen.respond_to?(:general)
858
- current_gen.general
859
- end
860
- end
861
-
862
- # Already sorted by parent hierarchy, then by name within parent
863
- inherited
864
- rescue StandardError => e
865
- warn "Error computing inherited attributes: #{e.message}"
866
- []
867
- end
868
-
869
- # Compute inherited associations from generalization chain
870
- def compute_inherited_associations(klass, visited = Set.new) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
871
- unless klass.respond_to?(:generalization) && klass.generalization
872
- return []
873
- end
874
- return [] if visited.include?(klass.xmi_id) # Prevent infinite loops
875
-
876
- visited.add(klass.xmi_id)
877
- inherited = []
878
- current_gen = klass.generalization
879
- parent_order = 0
880
-
881
- while current_gen
882
- parent_class = find_class_by_xmi_id(current_gen.general_id)
883
- break unless parent_class
884
- break if visited.include?(parent_class.xmi_id) # Prevent cycles
885
-
886
- visited.add(parent_class.xmi_id)
887
-
888
- parent_associations = find_class_associations(parent_class)
889
-
890
- # Get association details and determine local role from parent's
891
- # perspective
892
- assoc_with_roles = parent_associations.filter_map do |assoc_id|
893
- assoc = repository.associations_index.find do |a|
894
- @id_generator.association_id(a) == assoc_id
895
- end
896
- next unless assoc
897
-
898
- # Determine which role is the "local" one for the parent class
899
- # This becomes the inherited local role
900
- local_role = if assoc.owner_end_xmi_id == parent_class.xmi_id
901
- assoc.owner_end_attribute_name || assoc.owner_end || ""
902
- elsif assoc.member_end_xmi_id == parent_class.xmi_id
903
- assoc.member_end_attribute_name || assoc.member_end || ""
904
- else
905
- ""
906
- end
907
-
908
- { id: assoc_id, role: local_role }
909
- end
910
-
911
- # Sort by local role within this parent
912
- assoc_with_roles.sort_by { |a| a[:role] }.each do |item|
913
- inherited << {
914
- associationId: item[:id],
915
- inheritedFrom: @id_generator.class_id(parent_class),
916
- inheritedFromName: parent_class.name,
917
- parentOrder: parent_order,
918
- localRole: item[:role], # Include for template use
919
- }
920
- end
921
-
922
- # Move to parent's parent
923
- parent_order += 1
924
- current_gen = if current_gen.respond_to?(:general)
925
- current_gen.general
926
- end
927
- end
928
-
929
- inherited
930
- rescue StandardError => e
931
- warn "Error computing inherited associations: #{e.message}"
932
- []
933
- end
934
-
935
- # Find class by XMI ID
936
- def find_class_by_xmi_id(xmi_id)
937
- return nil unless xmi_id
938
-
939
- repository.classes_index.find { |c| c.xmi_id == xmi_id }
940
- rescue StandardError
941
- nil
942
- end
943
-
944
- # Find class by object ID (EA object ID)
945
- def find_class_by_object_id(object_id)
946
- return nil unless object_id
947
-
948
- repository.classes_index.find do |c|
949
- c.respond_to?(:ea_object_id) && c.ea_object_id == object_id
950
- end
951
- rescue StandardError
952
- nil
953
- end
954
-
955
- # Normalize stereotype to always be an array
956
- # @param stereotype [String, Array, nil] Stereotype value
957
- # @return [Array<String>] Array of stereotypes
958
- def normalize_stereotypes(stereotype)
959
- return [] if stereotype.nil?
960
- return stereotype if stereotype.is_a?(Array)
961
-
962
- [stereotype]
963
- end
964
143
  end
965
144
  end
966
145
  end